mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-19 07:54:38 +00:00
5805 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3cc66f9ae5
|
ci(deps): bump docker/* actions to Node 24 majors (#4131)
GitHub Actions deprecates the Node 20 runtime; older versions of every docker/* action run on Node 20 and emit the "Node.js 20 actions are deprecated" warning in every release / e2e / image-build run today. Each action shipped a "Node 24 as default runtime" major: - docker/setup-buildx-action v3 → v4 (2026-03-05) - docker/setup-qemu-action v3 → v4 - docker/metadata-action v5 → v6 - docker/login-action v3 → v4 - docker/build-push-action v6 → v7 None of our usages touch the deprecated inputs removed in the bumps — release.yml / e2e.yml call setup-buildx with no `with:` block, and build-and-publish-image.yml only passes the universally-supported `images` / `tags` / `registry` / `username` / `password` / `context` / `platforms` / `push` / `labels` / `build-args` inputs. ESM internal refactor of each action is transparent to consumers. Ratchet-pinned bumps use the v4.0.0 commit SHA `4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd` (release.yml + e2e.yml). Other action references in build-and-publish-image.yml use `# ratchet:exclude` per existing convention, so version-string bumps suffice there. Verified runtime hosts (`actions/runner` v2.327.1+) are already in use on github-hosted runners as of 2026-03; no infra bump required. Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> |
||
|
|
c512427f93
|
feat(core): strip inline media before chat compaction summary (#4101)
* feat(core): strip inline media before chat compaction summary
Compaction's side-query previously shipped historyToCompress verbatim.
Two related issues degraded summary quality and accuracy:
- Inline image / document bytes (from MCP tool results) leaked into the
summary model's prompt where they could not be interpreted and merely
inflated payload.
- findCompressSplitPoint apportioned chars via JSON.stringify(content),
so a single 1 MB base64 image looked like ~350K tokens and biased
the split point. Real Qwen-VL token cost is at most a few thousand.
This change adds a new compactionInputSlimming module that replaces
inlineData / fileData parts with short [image: <mime>] / [document:
<mime>] placeholders before the side-query, leaving live history
unchanged. The same constant feeds estimateContentChars so the
split-point algorithm sees the budget the summary model actually
consumes downstream. Microcompact is also extended to clear stale
inline images alongside old tool results.
A previous draft of the design also externalized large pastes to a
content-addressable on-disk cache, but it was withdrawn after surveying
claude-code's 2026-03 to 2026-05 releases - upstream consensus is to
keep user input visible to the model and amortize cost via prompt
caching rather than externalize. See the Out-of-scope section of the
design doc for the full rationale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): recurse into functionResponse.parts when stripping media
E2E exposed that `read_file` (and any tool that surfaces an image)
wraps the result in `functionResponse.parts` via
`coreToolScheduler.createFunctionResponsePart`. The slimming module
only walked top-level `part.inlineData` / `part.fileData`, so the
nested base64 bytes leaked into the compaction side-query payload.
The previous design doc incorrectly claimed that no recursive walk
was needed.
Three changes:
- `slimCompactionInput.transformPart` recurses into the nested
`functionResponse.parts` array and replaces each entry via the
same image/document placeholder logic.
- `estimatePartChars` walks the nested array too, so the split-point
algorithm doesn't fall back to `JSON.stringify` and over-count the
base64 bytes.
- `microcompactHistory` drops `functionResponse.parts` when clearing
an old tool result; the previous spread of `...part.functionResponse`
silently carried the original media through.
New unit tests cover (a) nested image / document stripping, (b) the
estimator no longer being skewed by nested base64. The previously
failing E2E now PASSES: side-query payload contains zero `data:image/`
occurrences, zero long base64 runs, and exactly one
`[image: image/png]` placeholder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): address review findings on compaction image stripping
Addresses 8 valid findings from PR review:
- [Critical] estimatePartTokens now handles `fileData` parts (both
top-level and nested under functionResponse.parts). Without this,
microcompact's `tokensSaved === 0` short-circuit silently discarded
every fileData clear.
- estimatePartTokens for binary parts now uses a fixed
MEDIA_PART_TOKEN_ESTIMATE constant (1,600) instead of base64-length
divided by 4. The old formula billed a 1 MB image as ~250K tokens
rather than its actual ~1,280 visual tokens on Qwen-VL, inflating
the saved-token metric by orders of magnitude.
- mimeType values from MCP tool servers are now run through
sanitizeMimeForPlaceholder before being embedded in `[image: …]` /
`[document: …]` placeholders. An adversarial server could otherwise
craft `image/png]\n\n[SYSTEM: …` and inject instructions into the
summary side-query.
- collectCompactablePartRefs now recognizes a third 'nested-media'
kind: functionResponse parts from non-compactable tools (e.g. MCP
screenshots whose names aren't in COMPACTABLE_TOOLS) that carry
images on functionResponse.parts. The nested media is dropped while
the tool's text output is preserved. Previously such media
accumulated forever in live history.
- keepRecent budgets are now per-kind (tool / media / nested-media).
Setting `toolResultsNumToKeep: 1` keeps 1 of each kind rather than 1
entry total across the merged list — matches the natural reading of
the setting name.
- findCompressSplitPoint's `precomputedCharCounts` fallback path is
now documented as test-only; production callers MUST pass the
precomputed array.
- The text-based branch of isAlreadyCleared is gone: with the new
nested-media handling (drops `parts`) and existing media handling
(replaces with `{ text: … }` that is no longer collected) it was
unreachable.
- OpenAI converter (createToolMessage) now passes text parts inside
functionResponse.parts through as text content. The slimmer writes
`{ text: '[image: image/png]' }` placeholders into the nested array;
without this fix the converter dropped them when serializing to the
OpenAI wire format, leaving the summary model with empty tool
responses instead of the placeholder.
Two findings deferred with rationale (see design doc Open Questions):
MIN_COMPRESSION_FRACTION still uses pre-slim counts (acceptable —
"user shared an image" is itself worth summarizing); SlimResult is not
re-exported (round-3 simplify decided to keep core's public surface
minimal).
E2E re-verified end-to-end: side-query payload contains 0 data:image/
occurrences, 0 long base64 runs, and 1 `[image: image/png]` placeholder
in the expected position. 185/185 collocated unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(core): tidy compaction slimming after self-review
Three small polishes from a follow-up code review pass:
- `stripNestedMedia` no longer re-casts its return value: after
destructuring `parts` out of the widened input type, TypeScript
infers the original `FunctionResponse` shape without help.
- `isAlreadyCleared` shed a 10-line comment block — the body is now
one line, so one descriptive line above it is enough.
- OpenAI converter's nested-part text check switched from
`(part as { text?: unknown }).text` to
`'text' in part && typeof part.text === 'string'`, dropping the
cast and letting `in` narrow the type.
No behavior change. 185/185 unit tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): wire slim stats to debug log; split MicrocompactMeta tools vs media
Addresses two follow-up review suggestions:
- `slimCompactionInput` returned `stats.imagesStripped` and
`stats.documentsStripped` but the orchestrator never consumed them.
Now logged at debug level whenever non-zero so operators can confirm
the slimming pipeline actually fires on image-heavy compactions.
- `MicrocompactMeta.toolsCleared` lost meaning after the recent
refactor: it had grown to count both tool-result clears AND
inline-media / nested-media clears. Renamed:
- `toolsCleared` → only `tool`-kind clears (compactable tool output)
- `mediaCleared` → `media` + `nested-media` clears (new)
- `toolsKept` / `mediaKept` mirror the split, replacing the prior
`toolsKept` that was actually a combined count.
The single non-test consumer (`client.ts` debug log) updated to use
both fields.
185/185 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f72a156e13
|
fix(anthropic): allow cache_control on tool_result blocks so the per-turn breakpoint advances (#4121)
The Anthropic adapter places three cache_control breakpoints (system, last tool, last user message), but `addCacheControlToMessages` only attached the third when the last block of the last user message was a non-empty text block. After turn 1 of any agentic conversation, the last user message is a tool_result, so the breakpoint was silently dropped and the cacheable region collapsed back to system+tools. Per-turn history was never cached. Anthropic's docs explicitly list tool_result as a cacheable block type (https://docs.claude.com/en/docs/build-with-claude/prompt-caching). Accepting both text and tool_result keeps the breakpoint moving forward as the conversation grows. |
||
|
|
d419a92672
|
chore(release): v0.15.11 [skip ci]
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
93c5ce162f
|
fix(search): make empty-query exit synchronous and normalize Windows Backspace (#3981)
* fix(search): make empty-query exit synchronous and normalize Windows Backspace
Two fixes to useSessionSearchInput that caused 'Backspace edits the query;
emptying it returns to list mode' to fail on Windows:
1. Replaced the useEffect-based onExitToList mechanism with a ref-backed
setSearchQuery that detects the non-empty → empty transition
synchronously inside the state updater. The previous useEffect approach
introduced a one-frame delay where the component rendered in search
mode with an empty query before the effect fired, causing the
'Press / to search' hint to be absent on Windows.
2. Added isDeletionKey() helper that recognizes Backspace from both the
key.name field ('backspace'/'delete') and the raw sequence bytes
(\x7f DEL and \b BS), so Windows terminals that deliver Backspace
without normalizing the key name are handled correctly.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
* fix(search): address review feedback on PR #3981
- Update onExitToList JSDoc: describe synchronous ref-backed detection instead of stale useEffect reference
- Update test suite comments: replace useEffect references with ref-backed setter description
- Add assertion that implicit entry (empty→non-empty via setSearchQuery) does NOT trigger onExitToList
- Add tests for Backspace/Esc/Ctrl+U/Ctrl+L on already-empty query: must not trigger onExitToList
* fix(search): address remaining review feedback on PR #3981
- Add isDeletionKey byte-fallback tests (\x7f DEL and \b BS)
- Update setSearchQuery JSDoc to document synchronous onExitToList side-effect
- Stabilize callback deps: read onExitToList via ref so setSearchQuery
and handleSearchKey are not recreated on parent re-render
- Clarify onExitToList JSDoc: fires before React re-renders, ref is
current but state variable is stale
- Add test for setSearchQuery('') direct-empty path (non-empty → empty)
- Add test for setSearchQuery('') on already-empty query (no spurious exit)
* test: stabilize flaky export format cycling test on Windows CI
Increase wait from 50ms to 350ms after typing '/export md' to allow
the useEffect in useExportCompletion to fire and set cyclingActiveRef
before the Down keypress arrives. This matches the pattern used by other
tests in this file that depend on useEffect timing.
* fix(search): guard isDeletionKey byte fallback against ctrl/meta modifiers
The byte fallback in isDeletionKey (key.sequence === '\x7f' / '\b')
previously fired even when ctrl or meta modifiers were active.
Ctrl+H delivers name:'h', ctrl:true, sequence:'\b' on many terminals
and was incorrectly treated as Backspace, deleting the last query char.
Add !key.ctrl && !key.meta guard so the byte fallback only activates
when the terminal truly did not normalize the key name.
Also add two tests:
- Ctrl+H (BS byte with ctrl) must not be treated as deletion key
- Meta+DEL byte must not be treated as deletion key
---------
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
|
||
|
|
d343e2c15e
|
feat(perf): progressive MCP availability — MCP no longer blocks first input (#3994)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(perf): progressive MCP availability — MCP no longer blocks first input
Today `Config.initialize()` runs MCP discovery synchronously and the cli
can't accept input until every configured MCP server finishes its
discover handshake. One slow or hung server bottlenecks every user with
MCP configured. Validated by the profiler instrumentation added in this
PR (set `QWEN_CODE_PROFILE_STARTUP=1` to reproduce):
| User scenario | Time to first prompt input |
| ------------------------- | -------------------------- |
| No MCP | ~480 ms |
| 1 fast MCP | ~875 ms |
| 2 fast + 1 slow MCP | **~7.1 s** |
| 1 hung MCP server | **~10.5 s** |
(Measured on macOS arm64 / Node 24.15, n=30/fixture, p50.)
`Config.initialize()` now passes `{ skipDiscovery: true }` to
`createToolRegistry` by default and kicks off MCP discovery in a
fire-and-forget background path. As each server completes discover,
the cli's `AppContainer` debounces `setTools()` calls into one-frame
(16 ms) batches so the model sees the consolidated tool list shortly
after each server settles. Rollback: `QWEN_CODE_LEGACY_MCP_BLOCKING=1`.
- `packages/core/src/config/config.ts` — `Config.initialize` switches
to `skipDiscovery: true` + new `startMcpDiscoveryInBackground()`
(defensive against partially-stubbed `ToolRegistry` in tests). Adds
`MCPServerConfig.discoveryTimeoutMs` (last positional ctor param —
doesn't shift existing call sites). Tool-call timeout is untouched.
- `packages/core/src/tools/tool-registry.ts` — new
`getMcpClientManager()` getter so the background path can call the
incremental discover directly without going through `discoverMcpTools`
(which would wipe already-registered tools).
- `packages/core/src/tools/mcp-client-manager.ts` —
`discoverAllMcpToolsIncremental` now: emits `mcp-client-update`
after IN_PROGRESS transition, wraps each per-server discover in a
discovery-only timeout (stdio 30s, remote 5s), emits trailing
`mcp-client-update` after COMPLETED so UI subscribers see the
terminal state.
- `packages/cli/src/ui/AppContainer.tsx` — new `useEffect` (gated on
`isConfigInitialized`) subscribes to `mcp-client-update` and
16ms-batches `setTools()` calls. Same effect also defers
`finalizeStartupProfile` until MCP settles (or 35s hard cap), so
startup-perf profiles capture the full MCP timeline.
Activated only by `QWEN_CODE_PROFILE_STARTUP=1`; when unset every
profiler entry point short-circuits in a single null/flag check and
returns. Heisenberg overhead measured at -1.12% Δp50 between
profile-on vs profile-off (Welch p=0.092, n=30/config × 3 configs) —
within statistical noise.
- `packages/cli/src/utils/startupProfiler.ts` — extended with
`events` array (multi-fire), `recordStartupEvent`,
`setInteractiveMode`, `derivedPhases`, per-checkpoint heap snapshots,
`MAX_EVENTS` cap, and `QWEN_CODE_PROFILE_STARTUP_OUTER` / NO_HEAP
env opt-ins. + 7 new tests.
- `packages/core/src/utils/startupEventSink.ts` (new) — minimal
cross-package sink so `core` can emit profiler events without
reverse-depending on `cli`. No-op when no sink registered. + 4 tests.
- `packages/core/src/index.ts` — export `setStartupEventSink` /
`recordStartupEvent` / type aliases.
- `packages/cli/src/gemini.tsx` — registers the sink at `main()`
entry, adds `first_paint` checkpoint after Ink render, calls
`setInteractiveMode(true)` in the interactive branch.
- `packages/core/src/config/config.ts` — emits
`tool_registry_created`.
- `packages/core/src/core/client.ts` — emits `gemini_tools_updated`
at the end of `setTools()`.
- `packages/core/src/tools/mcp-client-manager.ts` — emits
`mcp_discovery_start`, `mcp_server_ready:<name>`,
`mcp_first_tool_registered`, `mcp_all_servers_settled`.
- `packages/cli/src/ui/AppContainer.tsx` — emits
`config_initialize_start`, `config_initialize_end`, `input_enabled`.
`Config.initialize()` now returns BEFORE MCP discovery completes.
Things to check:
- Any code path that assumed "after `config.initialize()`, all MCP
tools exist in the registry" — these will see only built-in tools
initially; new tools appear via `mcp-client-update` events.
- `MCPDiscoveryState.COMPLETED` is now set asynchronously instead of
synchronously after `initialize()` resolves.
- Model requests issued before MCP settles see only built-in tools;
subsequent requests see the full set as servers come online.
- Tests that assert MCP tool count immediately after
`config.initialize()` should wait for the `mcp-client-update` with
COMPLETED discoveryState instead.
- 313 impacted-area tests green (config / mcp-client-manager / client
/ startupProfiler 18 / startupEventSink 4).
- `tsc --noEmit` clean for `packages/core` and `packages/cli`.
- `eslint` clean on touched files.
- Manual: `QWEN_CODE_PROFILE_STARTUP=1 SANDBOX=1` interactive run
produces a JSON profile in `~/.qwen/startup-perf/` containing
`first_paint`, `config_initialize_start/end`, `input_enabled`,
MCP per-server events, and `gemini_tools_updated`. See PR
description's "How to validate" section.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): harden progressive MCP discovery against silent regressions
Addresses review feedback on PR #3994:
- Skip user-disabled servers in discoverAllMcpToolsIncremental. The new
incremental path used to iterate Object.entries(servers) without
consulting isMcpServerDisabled, so a server the user had explicitly
turned off would still get connected and its tools registered.
Mirrors the existing protection in discoverAllMcpTools.
- Disconnect the underlying client when runWithDiscoveryTimeout fires.
Without this, the inner discoverMcpToolsForServer kept running after
the timeout rejected the outer promise — if discover() eventually
succeeded it would register the late server's tools into the live
toolRegistry (a silent registration vector, especially exploitable
with a 0/negative discoveryTimeoutMs override).
- Clamp discoveryTimeoutMs to [100ms, 300_000ms]. 0/negative/Infinity
values previously passed through to setTimeout unvalidated and made
the silent-registration bug above trivially reachable.
- Classify the `tcp` (WebSocket) transport field as remote so hung WS
handshakes use the 5s default instead of the 30s stdio default.
- Defensive delete of serverDiscoveryPromises[name] in the per-server
catch so a doomed/orphan entry can't briefly short-circuit a
subsequent discoverMcpToolsForServer call.
Adds focused tests for each fix.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): restore runtime.json sidecar and harden non-interactive MCP visibility
Addresses review feedback on PR #3994:
- Restore writeRuntimeStatus + markRuntimeStatusEnabled in
startInteractiveUI. The progressive-MCP diff inadvertently dropped
the runtime.json sidecar write from the interactive entry point,
leaving Config.refreshSessionId()'s session-swap refresh as dead
code and silently breaking external integrations (terminal
multiplexers, IDE integrations, status daemons) that map PID →
sessionId via runtime.json.
- Add Config.getFailedMcpServerNames() and surface a stderr warning
in --prompt / stream-json / ACP entry points when one or more MCP
servers failed during background discovery. Per-server errors are
caught inside discoverAllMcpToolsIncremental and never reached a
TTY otherwise, so a script using non-interactive mode with broken
MCP config would silently run with only built-in tools — a
regression vs the legacy synchronous path.
- Pass the parsed `settings` object through to
runNonInteractiveStreamJson. The new call site dropped the
argument, falling back to createMinimalSettings() and losing any
user-configured permission / approval / hook setup for stream-json
sessions. Added regression assertion to gemini.test.tsx.
- Move finalizeStartupProfile out of gemini.tsx's stream-json branch
and into Session.ensureConfigInitialized so it runs AFTER
config.initialize() / waitForMcpReady() in stream-json. Previously
the profile was finalized before any MCP / config_initialize_*
events were emitted, producing empty stream-json profiles.
- Gate setStartupEventSink registration on isStartupProfilerEnabled()
so core-side recordStartupEvent calls short-circuit at the first
null-check when profiling is disabled, instead of going through an
arrow wrapper and the profiler's own enabled gate.
- Tighten the type-unsafe ToolRegistry cast in
startMcpDiscoveryInBackground to preserve the typed return signature
so a rename of getMcpClientManager would be flagged at this call
site (kept the optional-chain guard for tests that stub
ToolRegistry as a plain object).
- Re-document first_paint as "render call returned" so consumers don't
confuse Ink's synchronous render() return with literal pixel paint.
Kept the checkpoint name for backward compatibility with collected
profiles.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): restore resize repaint and pin gemini_tools_lag capture in AppContainer
Addresses review feedback on PR #3994:
- Restore the terminal-resize useEffect that calls
repaintStaticViewport() when terminalWidth changes. The progressive-
MCP diff removed previousTerminalWidthRef + the repaint useCallback
+ the resize useEffect, so tmux pane resizes and fullscreen toggles
leave the static region rendered at the old width — header content
visibly tears until something else triggers refreshStatic.
- Pin the gemini_tools_lag startup metric. The previous onMcpUpdate
handler called finalizeOnce() synchronously when discovery reached
COMPLETED, but the pending setTools() batch was still 16ms away.
setTools() emits `gemini_tools_updated` — when finalize ran first
the profile's `finalized` guard suppressed that event, so
gemini_tools_lag came out undefined in interactive mode. New
onMcpUpdate flushes setTools() NOW on COMPLETED and only finalizes
after the flush resolves, guaranteeing the event lands.
- Log setTools() batch-flush errors via debugLogger instead of
silently swallowing them. GeminiClient.setTools() has no try/catch
around warmAll() / getFunctionDeclarations() / getChat().setTools();
the previous `.catch(() => {})` would have hidden production
tool-registration regressions completely.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): correct MCP failure visibility and incremental cleanup
Addresses three review findings on PR #3994:
- McpClient.discover() now flips the client status to DISCONNECTED before
re-throwing. Previously, a server that connected successfully but whose
discoverPrompts / discoverTools then rejected (or that returned no
prompts and no tools) would remain CONNECTED in the global status
registry. Config.getFailedMcpServerNames() filters by
`status !== CONNECTED`, so such servers were silently omitted from the
non-interactive failure banner and the Footer's MCP health pill kept
counting them as healthy.
- discoverAllMcpToolsIncremental no longer records `outcome: 'ready'`
for servers whose connect/discover threw. The inner
discoverMcpToolsForServerInternal catches errors without re-throwing
(best-effort discovery semantics), so the try block resolved even for
failures — only the runWithDiscoveryTimeout path reached the catch.
Auth errors, server crashes, and missing-tools responses were therefore
recorded as success in the startup profile. We now consult the actual
server status (now correctly DISCONNECTED after the first fix) before
emitting `ready`, and emit `outcome: 'failed'` otherwise.
`mcp_first_tool_registered` is gated on the same check so a failed
server can't pollute that user-facing metric.
- discoverAllMcpToolsIncremental tears down enabled→disabled mid-session
transitions. When a previously-connected server is disabled (e.g. via
`/mcp disable foo` or by editing settings), the incremental path used
to just `continue` past it, leaving its client, tools, health check,
and global status entry in place. Now calls removeServer() for any
already-known client we encounter in the disabled branch.
Adds focused tests for each fix.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* docs(core): clarify ToolRegistry cast comment in startMcpDiscoveryInBackground
Addresses review feedback on PR #3994. The previous comment claimed the
call site uses "no defensive cast" but the code still casts via
`as ToolRegistry & { getMcpClientManager?: ... }`. Reword to explain
the cast's actual purpose: it exists only because some tests stub
ToolRegistry as a plain object, so we use optional chaining to avoid
crashing the init path when those tests run. Also note that the inner
shape now uses `ReturnType<ToolRegistry['getMcpClientManager']>` — a
future rename of the production method still surfaces as a type error
at this call site rather than silently falling through to the
`if (!manager)` branch.
Comment-only change; no behavior diff.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): close MCP timeout TOCTOU race and propagate disconnect status
Addresses two critical findings on PR #3994 round 6:
- runWithDiscoveryTimeout no longer uses fire-and-forget disconnect. The
prior `void client.disconnect()` returned before `transport.close()`
landed, leaving a window where an in-flight `discover()` could pump
`tools/list` through the transport and synchronously register tools
into the live registry BEFORE the close took effect. The earlier fix
comment described this as a "remote-exploitable silent-tool-registration
vector"; the await closes the timing window but doesn't help if tools
already landed, so we also drop them with `removeMcpToolsByServer()`
after the disconnect resolves. No-op when discover hadn't reached
registration yet.
- McpClient.disconnect() now writes DISCONNECTED to the global registry
directly. Previously, `isDisconnecting = true` was set BEFORE the
internal `updateStatus(DISCONNECTED)` call, and `updateStatus`'s guard
(designed to suppress LATE writes from a stale `connect()` catch)
silently swallowed the write. The global stayed CONNECTED forever for
timeout-disconnected servers, so `Config.getFailedMcpServerNames()`
(which filters `status !== CONNECTED`) omitted them from the
non-interactive failure banner and the Footer's MCP health pill kept
counting them as healthy. This invalidated the round-5
`getMCPServerStatus === CONNECTED` gate, which would always pass the
"ready" check for timed-out servers. The guard stays in place for its
original purpose; the legitimate disconnect→DISCONNECTED notification
now bypasses it by writing the registry directly.
Also adds the `config_initialize_start` / `_end` profiler checkpoints
to `Session.ensureConfigInitialized()` so stream-json startup profiles
include the same derived `config_initialize_dur` phase as the
non-stream-json branch in gemini.tsx (round 6 [Suggestion]).
Tests cover (a) the disconnect-and-cleanup path on timeout and (b) the
intentional-disconnect global registry propagation regression.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(mcp): surface failures + prevent health-check resurrection of timed-out servers
Round-7 review follow-ups:
- AppContainer (interactive): MCP startup failures now route through
debugLogger.warn on COMPLETED. Was silent — only debug logs / profile
events surfaced failures, so regular interactive users got no
indication their MCP servers failed. Mirrors the non-interactive
stderr warning, adjusted to debugLogger so it doesn't collide with
Ink's rendered output.
- acpAgent per-session: `QwenAgent.initializeConfig()` now emits the
same `Warning: MCP server(s) failed to start` stderr line as the
top-level `runAcpAgent` path. Previously per-session ACP configs
with failed MCP servers silently fell back to built-in tools.
- mcp-client-manager timeout handler: after disconnecting an
intentionally timed-out server, also drop it from `this.clients` and
stop any pending health-check timer. Without this the discovery
`finally` block would arm a health-check that detected DISCONNECTED
status and called `reconnectServer()` → `discoverMcpToolsForServer()`
directly — bypassing `runWithDiscoveryTimeout` entirely and silently
resurrecting the slow server. `startHealthCheck` also early-returns
for unknown servers so the trailing finally-block call is a no-op.
- startupEventSink: silent `catch {}` now logs via `debugLogger.error`
so a corrupted sink doesn't silently drop every subsequent event.
Quiet by default; visible under `QWEN_CODE_DEBUG=1`.
Tests:
- mcp-client-manager.test.ts: regression for the timeout → no-reconnect
invariant (clients map purged + health-check timer absent).
- acpAgent.test.ts: per-session newSession surfaces failures to stderr,
and stays safe when Config lacks `getFailedMcpServerNames`.
Declines (with reasoning in PR reply):
- [Critical] AppContainer batch-flush useEffect untested → re-flag of
the round-5 deferral that wenshao acknowledged at the time. Lower-
layer invariants (this PR's mcp-client-manager + mcp-client tests)
pin the dependent contracts. The component-test harness for timers +
event emitters in this file is non-trivial and out of scope; tracked
for a follow-up.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
||
|
|
4bb8dc894a
|
fix(telemetry): address PR #3847 review follow-ups for trace correlation (#4058)
* fix(telemetry): address PR #3847 review follow-ups for trace correlation Addresses unresolved review feedback from PR #3847: - Respect OTEL_TRACES_SAMPLER env var when setting TraceFlags on the synthetic session root, so custom samplers (e.g. traceidratio) are not bypassed by forced SAMPLED flag - Store current session ID in session-context.ts and use it as a fallback in LogToSpanProcessor when the OTel Resource session.id attribute is stale after /clear or /resume - Wrap for-await loop body in runInSpan() so debug logs emitted during stream iteration see the stream span as active - Add autoOkOnSuccess option to withSpan, eliminating the need for the load-bearing setStatus(UNSET) hack in cancellation paths - Add defensive 5-minute timeout for stream spans to prevent leaks from abandoned generators 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(telemetry): address review issues in PR #4058 - Fix sdk.test.ts assertion to match new two-arg setSessionContext call - Add spanEnded guard to prevent double span.end() when timeout fires - Add .trim() to OTEL_TRACES_SAMPLER env var for robustness - Pass sessionId in debugLogger.test.ts for signature completeness - Clarify log-to-span-processor comment: fallback covers "missing" not "stale" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(telemetry): adopt review feedback for sampler and idle timeout - Fix shouldForceSampled to force SAMPLED for all parentbased_* samplers, not just parentbased_always_on — parentbased samplers delegate to localParentNotSampled (default AlwaysOff) when parent has NONE, which silently drops all traces - Convert stream span timeout from fixed wall-clock to idle timeout: reset timer on each chunk so legitimately long streams are never affected; timeout only fires when no chunks arrive for 5 minutes - Add test for parentbased_traceidratio → TraceFlags.SAMPLED * fix(telemetry): guard resetSpanTimeout against already-ended span Prevent zombie timer accumulation when chunks arrive after idle timeout has already ended the span. * fix(telemetry): handle parentbased_always_off sampler and fill test gaps - shouldForceSampled() now returns false for parentbased_always_off, preventing silent over-sampling that contradicts user intent. - Updated JSDoc to document the always_on exception for non-parentbased samplers. - Added test for parentbased_always_off → TraceFlags.NONE. - Added test for success path resilience when safeSetStatus throws in coreToolScheduler. * fix(telemetry): harden span timeout ordering and session ID fallback - Swap spanEnded/span.end() order so finally block can retry if end() throws. - Clear idle timeout immediately after for-await loop exits, before post-loop processing. - Use || instead of ?? for session.id fallback to handle empty strings. * fix(telemetry): address review round 4 — docs, cleanup, and test gaps - Expand shouldForceSampled JSDoc: document env-var assumption, parentbased_traceidratio 100% sampling semantics. - Remove unnecessary runInSpan wrapper around chunk field assignments. - Add stream.timed_out span attribute when idle timeout fires. - Deduplicate config.getSessionId() call in initializeTelemetry. - Add tests for always_on and always_off sampler values. * fix(telemetry): harden timeout callback ordering and add || comment - Move safeSetStatus before setAttribute in timeout callback so ERROR status is set even if setAttribute throws. - Set spanEnded=true before span.end() so finally block never overwrites ERROR with OK if end() throws. - Add comment explaining || vs ?? choice for session.id fallback. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
97ac766405
|
fix(core): improve runtime fetch options error handling and documentation (#3997)
* fix(core): improve runtime fetch options error handling and documentation - Add debug logging for dispatcher creation failures to prevent silent proxy bypass - Skip preconnect dispatcher creation when no proxy is configured (optimization) - Update outdated comments about timeout behavior (was "always disabled", now conditional) - Add tests for catch block error handling fallback behavior - Remove peer flag from @vscode/windows-process-cycles in package-lock.json The original change skipped custom dispatcher when no proxy to avoid Node/undici version mismatch (Node v26 ships undici v8 while project bundles v6). This follow-up addresses code review findings: 1. Critical: catch block silently bypassed proxy on dispatcher failure 2. Suggestion: preconnect warmed unused dispatcher pool when no proxy 3. Suggestion: outdated comments about "always disabled" timeouts * fix(core): address PR #3997 review comments - Fix 14 failing apiPreconnect tests by passing proxy parameter - Add console.error log for proxy dispatcher creation failure visibility - Redact credentials from proxy URL in error messages - Update outdated dashscope.ts timeout comment - Add mockWarn assertions to dispatcher failure tests * test(core): add console.error and credential redaction tests for dispatcher failure - Add console.error call verification in dispatcher failure tests - Add dedicated test for credential redaction from proxy URLs - Mock console.error to prevent noise during test runs * chore: improve code quality and documentation clarity - Remove hardcoded version numbers from undici compatibility comment - Add dual-logging rationale explaining debugLogger + console.error usage - Document implicit coupling between apiPreconnect and buildFetchOptionsWithDispatcher - Explain preconnectFired = true rationale for no-proxy case - Add comment for preconnectFired before async fire (fire-and-forget semantics) - Fix global.fetch mock leak by using vi.stubGlobal/unstubAllGlobals - Remove duplicate test and rename misleading test name * fix(core): align dashscope.ts comment and remove duplicate test - Update dashscope.ts comment to match default.ts and anthropicContentGenerator.ts - Remove misplaced duplicate test in getOrCreateSharedDispatcher describe block (functionally covered by existing test at line 72) * fix(cli): restore URL validation and error path test coverage - Add proxy parameter to 3 URL validation tests that were hitting no-proxy early return instead of exercising isDefaultBaseUrl logic - Add proxy parameter to 2 error handling tests to ensure they exercise fetch rejection and dispatcher error paths - Add mockFetch.mockResolvedValue(undefined) to beforeEach to prevent mock state leakage between tests - Add mockDebugLogger.debug.mockClear() to beforeEach to prevent debug log assertions from being polluted by previous tests * fix(core): harden proxy error handling and add timeout evaluation note - Use greedy regex match (.+@) instead of non-greedy ([^@]*) for credential redaction to handle edge cases like user@domain:pass@proxy.local - Add rejectedProxyCache Set to prevent duplicate error logging for the same broken proxy config across multiple API requests in long conversations - Add resetRejectedProxyCache() export for test isolation - Add timeout evaluation note documenting that Node.js built-in fetch uses default 300s bodyTimeout which is sufficient for all current model streaming - Update test beforeEach to call resetRejectedProxyCache() * fix(core): address final review feedback on credential redaction and JSDoc - Restore safe regex /\/\/[^/\s]*@/g for credential redaction to avoid over-consuming hostname when error messages contain unrelated '@' chars and to redact all occurrences in multi-line error chains via /g flag - Fix misplaced JSDoc: resetRejectedProxyCache and resetDispatcherCache now each have their own correct docstrings - Add 2 tests to verify rejectedProxyCache dedup behavior: same proxy URL logs once, different proxy URLs each log separately * fix(core): extract redactProxyCredentials helper with dedicated tests - Extract redactProxyCredentials() as an exported pure function for testability and reusability - Add 4 dedicated tests: single URL redaction, multi-URL /g coverage, non-over-redaction of unrelated @ chars, no-op for clean messages - Add mockWarn/mockConsoleError cleanup to getOrCreateSharedDispatcher beforeEach to prevent test state leakage * fix(core): harden credential redaction and cache key security - Use redacted proxyUrl as cache key to avoid storing plaintext credentials in process memory (heap dump protection) - Add fallback regex for scheme-less error messages (e.g., Node.js native 'connect ECONNREFUSED user:pass@host:8080') - Add 4 tests: no-scheme redaction, double-redact prevention, cache key dedup with different credentials, mock expanded * fix(core): address remaining review findings — mock alignment, code quality, test lifecycle - Revert MockProxyAgent to fail only for syntactically invalid URIs (real ProxyAgent accepts credential URLs — includes('@') was wrong) - Update credential redaction test to use http://invalid-proxy whose mock error message contains credentials to redact - Extract NO_DISPATCHER_FALLBACK constant to replace duplicated ternary expression (sdkType === 'openai' ? undefined : {}) - Move vi.spyOn(console, 'error') into beforeEach with afterEach restoreAllMocks (consistent with apiPreconnect.test.ts pattern) - Add preconnectFired verification test: no-proxy call permanently skips subsequent preconnect attempts * fix(proxy): address PR #3997 review feedback - Add credential redaction in apiPreconnect.ts catch blocks to prevent proxy credentials from being written to ~/.qwen/debug/ log files - Export redactProxyCredentials from core package barrel for CLI reuse - Fix environment variable tests (NODE_EXTRA_CA_CERTS, QWEN_CODE_DISABLE_PRECONNECT, SANDBOX) that were masked by no-proxy early return by adding proxy parameter - Remove rejectedProxyCache deduplication to allow administrators to see each credential change failure; log only hostname to avoid credential leakage - Add extractHostnameFromProxyUrl helper for safe hostname extraction Verified: lint + build + typecheck + tests all pass * fix(core): harden proxy credential redaction Cover token-only and colon-containing proxy credentials in scheme-less error messages, keep hostname fallback logging credential-safe, and add failure counts for repeated proxy dispatcher failures. Also align preconnect tests with the shared redaction helper and restore the unrelated fsevents lockfile peer marker. * test(core): prevent proxy redaction overreach Tighten scheme-less proxy credential redaction so ordinary email-like text is preserved while token and userinfo proxy credentials remain covered. * fix(core): address proxy review edge cases * docs(core): clarify proxy dispatcher behavior * fix(core): redact proxy credentials from SDK errors |
||
|
|
15e6222546
|
fix(dashscope): use URL hostname check instead of regex to avoid ReDoS (#4112)
CodeQL flagged a polynomial regular-expression alert on the unanchored /([\w-]+\.)?dashscope(-intl)?\.aliyuncs\.com/i pattern introduced in #3991 — repeated '-' in the baseUrl could trigger catastrophic backtracking. The unanchored regex was also too permissive: it would incidentally match the dashscope domain appearing anywhere in a URL path, not just in the hostname. Parse the baseUrl with the URL constructor and compare hostname suffixes instead. This eliminates the ReDoS surface entirely and is strictly tighter — only real dashscope[-intl].aliyuncs.com hostnames (and their subdomains) match. Co-authored-by: Qwen-Coder <noreply@alibaba-inc.com> |
||
|
|
5a1c427d6a
|
feat(subagents): use fastModel for Explore subagent (#4086)
Adds a "fast" keyword to the subagent model selector. When set, the runtime resolves it via Config.getFastModel() under the parent authType, falling back to inheriting the parent model when fastModel is unset or invalid for the current authType. The built-in Explore subagent opts in, so a configured fastModel automatically powers codebase searches without affecting other subagents or the main session. |
||
|
|
613fc42c89
|
fix(core): tag subagent OpenAI JSON logs (#4099) | ||
|
|
870bdf2a9d
|
feat(cli,sdk): qwen serve daemon (Stage 1) (#3889)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(cli): scaffold `qwen serve` HTTP daemon (Stage 1, #3803) Adds a `serve` subcommand that boots an Express 5 listener with bearer auth, host allowlist, and CORS modeled on `vscode-ide-companion/src/ ide-server.ts`. Ships only `/health` and `/capabilities` to begin with; session/prompt/event routes will land in follow-up PRs once the per- session ACP child-process bridge in `httpAcpBridge.ts` is wired. Defaults to 127.0.0.1 with auth disabled so local development needs no configuration. Binding beyond loopback (e.g. `--hostname 0.0.0.0`) refuses to start without a token (`--token` or `QWEN_SERVER_TOKEN`). Capabilities envelope versioned at v=1 with a `features` array — clients should gate UI off `features`, never off `mode`, so subsequent PRs can add capability tags without breaking older clients. Per design issue's Stage 1 scope (~700-1000 LOC). Adds ~430 LOC of implementation + tests in this scaffold; the remaining budget belongs to the route wiring + bridge implementation in follow-ups. * feat(cli): wire HttpAcpBridge + POST /session for `qwen serve` (#3803) Stage 1 follow-up to the scaffold. Implements the bridge between the HTTP daemon and the existing ACP child agent, plus the first session endpoint. `HttpAcpBridge.spawnOrAttach`: - Spawns `node $cliEntry --acp` per workspace via an injectable `ChannelFactory` (default uses `process.argv[1]`; tests use an in-memory `TransformStream` pair so they don't fork real processes). - Drives the ACP `initialize` + `newSession` handshake via the SDK's `ClientSideConnection`, with a 10s timeout that kills the channel. - Under `sessionScope: 'single'` (default), reuses the live session when the same canonical workspace cwd is requested again — backs the `attached: true` flag. - The `Client` impl on the bridge side proxies file reads/writes to local fs (daemon and agent share the host) and buffers `sessionUpdate` notifications for the SSE wiring in the next PR. `requestPermission` returns `cancelled` until the `/permission/:requestId` route lands. `POST /session`: - 400 on missing or relative `cwd`. - 200 with `{sessionId, workspaceCwd, attached}` on success. - 500 on bridge failure (the failing channel is killed, not leaked). `runQwenServe` constructs the bridge and ties `bridge.shutdown()` into the listener-close path so SIGINT/SIGTERM drain children before the socket closes. Tests (14 new, 0 regressions in the 4967-test baseline): - 9 bridge cases over an in-memory channel — fresh spawn, single-scope reuse, cross-workspace isolation, thread-scope independence, path canonicalization, relative-path rejection, init failure cleanup, init timeout, multi-channel shutdown. - 4 route cases for /session (missing/relative/200/500). - 1 lifecycle case asserting `runQwenServe.close()` calls `bridge.shutdown()` before closing the listener. Verified end-to-end: `qwen serve` boots, `POST /session` spawns a real `qwen --acp` child and returns the SDK-assigned `sessionId`, repeat calls under the same cwd return `attached: true`, `SIGTERM` reaps the child along with the listener. * feat(cli): wire POST /session/:id/prompt + /cancel for `qwen serve` (#3803) Stage 1 follow-up after the bridge scaffold. Adds the two routes a client needs to actually run a turn against the daemon. Bridge: - `sendPrompt(sessionId, req)` looks up the session, FIFO-queues the call against the per-session prompt queue, and forwards through the SDK `ClientSideConnection.prompt`. Concurrent calls observe ACP's "one active prompt per session" invariant — second waits for first. - A failed prompt does NOT poison the queue; the tail catches and keeps draining so the next caller still runs (the original caller still sees its own rejection). - `cancelSession(sessionId, req?)` bypasses the queue and forwards the ACP notification immediately. ACP semantics: the agent winds down the *currently active* prompt; queued work is unaffected. - Both methods throw `SessionNotFoundError` (a typed Error subclass) when the id is unknown so route handlers can map cleanly to 404 without brittle message matching. - Both methods overwrite the `sessionId` field in the request body with the routing id — a stale or spoofed body would otherwise be dispatched to the wrong agent process. Routes: - `POST /session/:id/prompt` → 200 with PromptResponse, 400 on missing/non-array prompt, 404 on unknown session, 500 on agent error. - `POST /session/:id/cancel` → 204 always (cancel is a notification), 404 on unknown session. Tests (14 new — 7 bridge + 7 route, 0 regressions in the 4981 baseline): - sendPrompt: success forwards & returns response · routing-id overrides body sessionId · concurrent prompts FIFO-serialize (verified via per-prompt start/end ordering with a release latch) · failed prompt doesn't block subsequent prompts · 404 for unknown id. - cancelSession: forwards with routing id · 404 for unknown id. - Routes: 200/400/404/500 paths for prompt; 204 with body or empty + 404 for cancel. Verified end-to-end against a real `qwen --acp` child: - POST /session/:id/prompt with `[{type:'text',text:'hi'}]` → 200 `{"stopReason":"end_turn"}` in ~3.4s. - POST /session/:id/cancel → 204. - POST /session/does-not-exist/prompt → 404 with the unknown id surfaced in the body. * feat(cli): wire SSE streaming for `qwen serve` events (#3803) Stage 1 follow-up that turns prompt into a real streaming experience. Replaces the in-memory `notifications: SessionNotification[]` buffer on each session with a per-session EventBus and exposes it through `GET /session/:id/events` as an `text/event-stream` SSE feed. EventBus (`packages/cli/src/serve/eventBus.ts`): - Monotonic per-session ids (`v: 1` schema). Each `publish` chains an id, returning the materialized BridgeEvent. - Bounded ring (default 1000) backs `Last-Event-ID` reconnect — a consumer that drops can resume from `lastEventId` and replay any still-buffered events before live events flow. - Per-subscriber bounded queue (default 256). When a slow consumer overruns its queue, the bus appends a synthetic `client_evicted` terminal frame and closes that subscription so it can't hold the daemon hostage. Other subscribers are unaffected. - `subscribe()` returns an AsyncIterable — registration is synchronous so events `publish`ed immediately after the subscribe land in the queue (a generator-style implementation deferred registration to first `next()` and raced with publishes). - AbortSignal-aware: aborting the signal closes the iterator promptly. Bridge (`httpAcpBridge.ts`): - `BridgeClient.sessionUpdate` now publishes onto the session's EventBus instead of pushing to a plain array — every ACP notification the agent emits becomes a stream event automatically. - New `subscribeEvents(sessionId, opts?)` returns the bus's AsyncIterable; throws `SessionNotFoundError` for unknown ids. - Shutdown closes every live event bus before killing channels so pending consumers unwind cleanly. Route (`server.ts`): - `GET /session/:id/events` sets the SSE content type, advertises a 3s reconnect hint, and writes a 15s heartbeat comment frame to keep proxy/NAT connections alive. - Forwards the `Last-Event-ID` header to the bus. - `req.on('close')` triggers an AbortController that propagates into the bridge subscription so disconnects don't leak subscribers. - 404 when the bridge can't find the session. Capabilities envelope: `STAGE1_FEATURES` now advertises `session_create`, `session_prompt`, `session_cancel`, `session_events` in addition to `health`/`capabilities` so clients can light up UI for the routes that have actually shipped. Tests (16 new, 0 regressions in the 4995 baseline): - 9 EventBus unit cases — id sequencing, live delivery, replay, replay+live splice, fan-out to N subscribers, eviction on overflow, abort-signal unsubscribe, bus.close() drains subscribers, ring-size eviction. - 4 bridge subscribe cases — 404, sessionUpdate→event publishing via real ACP fake-agent, shutdown closes live subscriptions. - 4 SSE route cases against a live HTTP listener — frame format, Last-Event-ID forwarding, 404, abort propagation on disconnect. Verified end-to-end against a real `qwen --acp` child: - Subscribed to `/session/$SID/events`, fired `POST /session/$SID/prompt` with text content. Captured 13 distinct `event: session_update` SSE frames in real time during the model's response — `available_ commands_update` metadata, 9 `agent_thought_chunk` frames carrying the model's chain-of-thought, 3 `agent_message_chunk` frames with the actual reply, and a final usage frame with token totals. - Frames carry monotonic ids 1..13, the daemon-side counter, and are valid SSE per the EventSource spec. * feat(cli): wire POST /permission/:requestId for `qwen serve` (#3803) Stage 1 follow-up that turns `BridgeClient.requestPermission` from a hardcoded `cancelled` placeholder into a real first-responder vote loop, and ships the HTTP route any attached client uses to cast the deciding vote. Bridge: - `requestPermission` generates a UUID requestId, registers a pending entry on a daemon-wide map (and the owning session's `pendingPermissionIds` set), publishes a `permission_request` event onto the session's EventBus (so SSE subscribers see it), and awaits the resolution. - New `respondToPermission(requestId, response)` resolves the pending promise with the supplied outcome. First call wins — subsequent calls return false. On success the bridge publishes a `permission_resolved` event so other attached clients can update their UI when the race is decided. - `cancelSession` and `shutdown` both resolve every still-pending permission for the affected session(s) as `{ outcome: { outcome: 'cancelled' } }` per the ACP spec requirement that a cancelled prompt MUST resolve outstanding requestPermission calls with cancelled. - New `pendingPermissionCount` getter exposes inflight count for inspection / tests. Route (`server.ts`): - `POST /permission/:requestId` validates the body's `outcome` is either `{ outcome: 'cancelled' }` or `{ outcome: 'selected', optionId: string }`, then forwards to `bridge.respondToPermission`. - 200 on accepted vote, 404 when the requestId is unknown or already resolved (Stage 1 doesn't differentiate), 400 on a malformed outcome. Capabilities envelope: STAGE1_FEATURES gains `permission_vote`. Tests (14 new — 9 bridge + 5 route, 0 regressions in the 5011 baseline): - Bridge: publishes permission_request with a generated requestId and waits; respondToPermission first-responder wins; publishes permission_resolved on vote; respondToPermission false for unknown requestId; cancelSession resolves outstanding as cancelled; shutdown resolves outstanding as cancelled. - Route: 200 on selected outcome; 200 on cancelled outcome; 404 on unknown requestId; 400 on malformed outcome; 400 on missing outcome. Verified end-to-end against a real `qwen --acp` child: - Subscribed to /session/$SID/events, sent a prompt asking the agent to write a file at /tmp/qwen-serve-permission-e2e-test.txt. - The agent triggered a permission_request via the bus, surfacing the three options Qwen Code presents (Allow Always / Allow / Reject) with their option ids. - POSTed `{outcome:{outcome:"selected",optionId:"proceed_once"}}` to /permission/$requestId — got HTTP 200. - Bus published the matching permission_resolved event. - Agent proceeded with the writeTextFile tool call; file was actually created on disk with the expected content. * feat(sdk): add DaemonClient for the qwen serve HTTP API (#3803) Stage 1 follow-up that proves the cross-mode protocol-isomorphism design assumption: an SDK client can drive the daemon's HTTP routes end-to-end without going through ProcessTransport's stdio + stream-json path. DaemonClient is a sibling of ProcessTransport, not a replacement. The two speak different protocols (ACP NDJSON over HTTP vs stream-json over stdio). Existing `query()` users keep getting subprocess-mode unchanged; applications that want daemon-mode (cross-client attach, shared MCP pool, network reachability, first-responder permissions) opt in by constructing a DaemonClient against a running `qwen serve`. API surface (`packages/sdk-typescript/src/daemon/`): - `new DaemonClient({ baseUrl, token?, fetch? })`. The `fetch` override is for tests; defaults to `globalThis.fetch`. Trailing slashes on `baseUrl` are stripped. - `health()`, `capabilities()` — discovery. - `createOrAttachSession({ workspaceCwd, modelServiceId? })` — `attached: true` on the response indicates a session was reused under sessionScope:single. - `prompt(sessionId, { prompt: ContentBlock[] })` — returns PromptResult with stopReason. - `cancel(sessionId)` — tolerates 204; throws on 404. - `subscribeEvents(sessionId, { lastEventId?, signal? })` — async iterator over parsed SSE frames; AbortSignal-aware. Native Node AbortController only — jsdom polyfills are incompatible with undici. - `respondToPermission(requestId, response)` — first-responder vote; returns true on 200, false on 404 (lost the race or unknown id), throws on 400/500. `DaemonHttpError` is thrown for any non-2xx (besides the 404 "already-resolved" case on permission votes); carries `status` and `body` so callers can branch on standard daemon HTTP semantics. `parseSseStream(body)` is the underlying SSE parser; exported separately so applications can consume daemon SSE outside the DaemonClient surface. Handles split-chunk frames, comment/retry directives, malformed JSON (skip), trailing frame without final newline. Wire types live SDK-side (no SDK→CLI dep); the capabilities envelope's `v` field signals breaking changes. Tests (26 new, 0 regressions in the 201 baseline): - 7 SSE parser cases — single frame, multiple frames, comments, chunked-split frame, malformed JSON skip, trailing frame on close, empty stream. - 19 DaemonClient cases — health success/error, capabilities, bearer auth presence/absence, createOrAttachSession success/400, prompt body shape + sessionId url-encoding, cancel 204/404, permission 200/400/404, subscribeEvents header forwarding + 404, baseUrl normalization. Verified end-to-end against a real `qwen serve` daemon driving a real `qwen --acp` child: - `client.capabilities()` returned `{v:1, mode:"http-bridge", features: [...7 tags]}`. - First `createOrAttachSession` returned `attached:false`; second returned `attached:true` with the same sessionId. - `client.prompt(...)` with text content yielded `{stopReason: "end_turn"}` while the parallel `subscribeEvents` iterator streamed 10 distinct frames during the same turn. - AbortController on the events iterator cleanly severed the SSE connection. * feat(cli,sdk): list workspace sessions + set session model (#3803) Closes the §04 Stage-1 routes table for `qwen serve` with the two remaining endpoints, plus matching SDK methods. `GET /workspace/:id/sessions` - `:id` is the URL-encoded canonical absolute workspace path (Express decodes path params automatically; clients pass `encodeURIComponent(cwd)`). - Returns `{ sessions: [{ sessionId, workspaceCwd }, ...] }` for live sessions whose canonical workspace matches. - Empty array (not 404) when the workspace is idle so picker UIs don't have to special-case "no sessions yet". - 400 when the decoded path isn't absolute. `POST /session/:id/model` - Body: `{ modelId: string, ... }`. The route's `:id` overrides any spoofed sessionId in the body. - Forwards to ACP's `unstable_setSessionModel` and publishes a `model_switched` event onto the session bus so cross-client UIs update. - 200 with the agent's response on success, 400 on missing/empty modelId, 404 on unknown session. - The SDK method is currently unstable; documented in the bridge comment in case the spec renames the method when it stabilizes. Bridge: - New `listWorkspaceSessions(workspaceCwd)` iterates `byId.values()` and filters by canonical workspace path; works for both `single` and `thread` session scopes. - New `setSessionModel(sessionId, req)` forwards through `connection.unstable_setSessionModel`, normalizes sessionId, publishes `model_switched`, throws SessionNotFoundError on unknown ids. `STAGE1_FEATURES` capabilities envelope grows to 9 tags, adding `session_list` and `session_set_model`. SDK (`DaemonClient`): - `listWorkspaceSessions(workspaceCwd)` URL-encodes the cwd and returns the parsed `sessions` array directly. - `setSessionModel(sessionId, modelId)` POSTs the body and returns the agent response (currently opaque per ACP unstable spec). - Wire types `DaemonSessionSummary` and `SetModelResult` exported from the SDK barrel. Tangential cleanup: `sendBridgeError` now extracts a useful message from non-Error values via a small `errorMessage` helper. JSON-RPC errors from the agent (`{code, message, data}`) used to surface as `"[object Object]"` in the 500 response body; they now show the inner `message` field. Caught while running the model-set e2e. Tests (17 new — 9 bridge + 7 route + 4 SDK, 0 regressions in the 5022 + 227 baselines): - Bridge listWorkspaceSessions: matching cwd returns the live sessions; canonicalizes the lookup; empty for relative paths. - Bridge setSessionModel: forwards modelId + overrides body sessionId; publishes model_switched event; 404 unknown session. - Route /workspace/:id/sessions: returns the bridge list; empty for idle workspace; 400 for relative path. - Route /session/:id/model: 200 success; 400 missing modelId; 400 empty modelId; 404 unknown session. - SDK listWorkspaceSessions: URL-encodes the cwd; throws on 400. - SDK setSessionModel: posts body; throws on 404. Verified end-to-end against a real `qwen serve`: - SDK reports 9 capability features, list returns the existing session, attached:true on repeat create, and `setSessionModel` rejects with HTTP 500 when the modelId isn't registered (with the daemon now surfacing "Internal error" instead of "[object Object]"). - 404 path through SDK on unknown sessionId works. * fix(cli,sdk): audit round 1 follow-ups for `qwen serve` (#3803) Self-review pass on PR #3889. Two real correctness bugs and an ergonomics gap, plus the test-coverage holes the audit surfaced. The loudest finding ("host allowlist no-op when bind=localhost") was a false positive — the conditional was misread; existing tests already prove the validator is active on `localhost` binds. Real fixes: - Bearer-auth timing-attack: `parts[1] !== token` short-circuits per byte, leaking which prefix is correct via response latency. Replace with SHA-256 of both sides + `crypto.timingSafeEqual` so comparison is constant-time regardless of token length. - Concurrent `spawnOrAttach` race in single-scope: two parallel callers for the same workspace both passed the `byWorkspace.get` check, both spawned, and one entry ended up orphaned in `byId` while the other won `byWorkspace`. Violates the "at most one session per workspace" invariant. Coalesce via an `inFlightSpawns` map: parallel callers attach to the in-flight promise and report `attached: true`. The slot is cleared on both success and rejection so a failed spawn doesn't poison the workspace forever. New test asserts ONE channel spawns under parallel calls and that retry works after rejection. - `Number.parseInt('1.5e10z', 10)` returns 1, so a malformed `Last-Event-ID` header silently passes through. Tighten `parseLastEventId` to `^\d+$` so anything not a pure decimal integer is dropped. New test exercises 'abc', '-1', '1.5e10z'. Ergonomics: - `LOOPBACK_BINDS` and `LOOPBACK_HOST_BINDS` now include `::1` and `[::1]`. IPv6 loopback users no longer have to set a token. Host-allowlist allows `[::1]:port` Host headers. Documentation: - `BridgeClient` doc-comment now states the Stage 1 trust model explicitly: agent runs as the same UID, the file-proxy methods are NOT a workspace-cwd sandbox, restricting them would be theatre. The audit flagged this as a "design gap" but the daemon-and-agent-on-same-host posture makes a sandbox here redundant — Stage 4+ remote-sandbox swaps the Client for a sandbox-aware variant. SDK fix: - `DaemonClient.failOnError` previously called `res.json()`, which consumes the body even on parse-failure; the subsequent `res.text()` returned empty. New impl reads once as text and attempts JSON-parse; raw text is the fallback. New test asserts a `text/plain` 502 surfaces the body verbatim. Test gap fills (audit-flagged): - Bridge: in-memory file-proxy tests for `BridgeClient.{read,write} TextFile` including line/limit slicing. - SSE route: `stream_error` synthetic frame on iterator throw mid-stream; numeric Last-Event-ID forwarded; malformed Last-Event-ID dropped. - DaemonClient: text/plain error body coerced to `body` field; `respondToPermission` 5xx throws; `subscribeEvents` null-body throws; `cancel`/`respondToPermission` URL-encode session/request ids that contain slashes. Verified end-to-end with a token-required daemon: right token → 200, wrong/missing/malformed → 401. All paths return uniform 401 messages so a side-channel can't distinguish between "no header", "bad scheme", and "wrong token". Test counts: cli serve **89** (was 81, +8), sdk daemon **35** (was 30, +5). Full suites still green. * fix(cli): audit round 2 follow-ups for `qwen serve` (#3803) Second self-review pass on PR #3889. Three real bugs (one correctness, one resource-cleanup, one cosmetic) plus consolidation of the loopback bindings into a single source of truth. Real fixes: - Shutdown could hang forever on a long-lived SSE consumer: `server.close` waits for every in-flight connection to drain, and a paused EventSource client never disconnects. Added a `SHUTDOWN_FORCE_CLOSE_MS` (5s) timer that calls `server.closeAllConnections()` to force-destroy stuck sockets, then resolves so `process.exit(0)` can run. New test asserts close completes well under 5.5s even when an SSE GET is in flight. - Signal-handler race during shutdown: round 1 detached the SIGINT/SIGTERM listeners *up front* in `handle.close()`. If a second SIGTERM arrived during the drain, no handler existed and Node's default termination ran, orphaning agent children. Switch to detaching at the *end* of the close path (in `finish()`): during the drain window the handler is still attached and the `if (shuttingDown) return` guard makes a second signal a no-op; after drain completes we can safely remove the listeners (this also fixes a test-suite MaxListenersExceededWarning that fired once we ran the runQwenServe tests >10 times in a single process). - SSE response had no `error` listener. When the underlying TCP socket died (RST, kill -9 on the client), the next `res.write` threw EPIPE and Express forwarded it to the default error handler, logging noisily. Added `res.on('error', cleanup)` so the failure is absorbed and triggers the same teardown path the `req.on('close')` handler uses. Validation: - `createHttpAcpBridge` now throws on invalid `sessionScope` (anything other than `'single'` or `'thread'`) and on `initializeTimeoutMs <= 0`. Misconfigured callers used to silently degrade to thread behavior; now they fail loudly. Cleanup: - The `LOOPBACK_BINDS` set was duplicated between `auth.ts` and `runQwenServe.ts` (round 1 missed this). Extracted into `packages/cli/src/serve/loopbackBinds.ts` with a single `isLoopbackBind(hostname)` helper. Both files now import; drift is impossible. - `res.flushHeaders?.()` lost the optional chaining. The method is on `http.ServerResponse` since Node 1.6; our `engines` floor is 20. Tests added: - bridge: `sessionScope` validation, `initializeTimeoutMs` validation. - server: shutdown force-close timeout, SIGINT/SIGTERM listener detach-after-drain. False positives from the round 2 audit (verified and dismissed): - "EventBus nextId overflow at 2^53" — theoretical only (would require ~9 quadrillion publishes per session). No code change. - "Subscribe-during-close race" — JS is single-threaded; the close() flag is set synchronously before the loop touches state. - "Queued prompts on shutdown" — by design; documented via the promptQueue tail comment. - "10MB body parser limit" — design choice for Stage 1's in-memory buffering model; revisit if ACP streaming lands in Stage 2. - "Unbounded body read in DaemonClient.failOnError" — daemon is local in Stage 1; the threat surface for adversarial-large error bodies is the same as the daemon's other unbounded buffers. Test counts: cli serve **93** (was 89, +4), full cli **5047** (no regressions), sdk **236** (no regressions). * docs(cli): audit rounds 3 + 4 follow-ups for `qwen serve` (#3803) Two more self-review passes on PR #3889. No correctness bugs surfaced this time — round 3 found a HIGH-severity Windows-path claim that turned out to be a false positive (`path.win32.isAbsolute('/foo/bar')` returns true; verified against Node 20). Round 4 confirmed every prior decision and surfaced one latent-but-not-currently-triggered concurrency note. Changes are pure documentation + a tiny optional-chain cleanup: - Drop `?.` on `server.closeAllConnections()` in runQwenServe.ts — the method exists since Node 18.2 and our `engines` floor is 20. The optional chain dated from before round 2's force-close timer landed; clean it up. - Help text for `qwen serve --port` now documents that port 0 means "OS-assigned ephemeral port" (which the implementation has always supported but never advertised). - `defaultSpawnChannelFactory` gains a comment near the spawn site documenting the FD-budget implication (~3 FDs per session, bump `ulimit -n` for many concurrent sessions) and the `stdio: ['pipe', 'pipe', 'inherit']` choice (child stderr lands in the daemon's stderr, interleaved across sessions). Both are Stage-1-accepted; Stage 2/4+ revisit each. - Comment on the bridge's `byWorkspace`/`byId` Maps documenting the known gap that a child crashing between requests leaves a garbage SessionEntry until daemon shutdown — surfaced as a per-prompt failure when the dead session is touched, not a hang. Stage 2's in-process bridge eliminates the spawned-child failure mode entirely so this gap goes away naturally. - `EventBus.subscribe` doc-comment now states explicitly that the returned iterator is NOT safe to drive from concurrent `.next()` callers — the underlying queue isn't atomic. Daemon usage is the sequential `for await ... of` inside the SSE route, so this is safe in production. Documented so a future fan-out consumer doesn't accidentally rely on undefined behavior. False positives verified and dismissed (round 3 + 4 combined): - `path.isAbsolute('/foo/bar')` Windows breakage — `path.win32. isAbsolute('/foo/bar')` is true; verified empirically. - "Windows drive divergence" causing duplicate sessions — different drives are different on-disk paths; sessions intentionally differ. - "parseSseStream early-break leaks reader" — `for await ... break` triggers `iterator.return()` which runs the generator's `finally` that calls `releaseLock`. Standard JS semantics. - "Promise executor sync-throw fragility in requestPermission" — sync throws inside `new Promise(executor)` reject the outer promise; functionally correct, just stylistic. - "Force-close timeout test elapsed assertion flakiness" — assertion is `< 5500ms` but the natural happy-path is sub-100ms. Generous headroom; not flake-prone in practice. - "fetch reference stale after polyfill" — `globalThis.fetch.bind` captures at construction; tests inject `opts.fetch` instead of polyfilling, which is the correct pattern. Test counts unchanged (cli serve **93**, sdk **236**); typecheck + lint clean. STAGE1_FEATURES still matches every implemented route 1:1, fakeBridge in tests implements every HttpAcpBridge method. * fix(cli): PR #3889 review round 1 — critical correctness (#3803) Addresses the four critical findings from the PR #3889 reviewer pass: 1. ACP `ReadTextFileRequest.line` is 1-based per spec, but the bridge's `BridgeClient.readTextFile` was treating it as a 0-based slice index. A client asking for `{line:1, limit:2}` ("first two lines") was getting lines 2-3 — a sign-off-by-one bug that breaks every editor / SDK client following the ACP schema. Convert to 0-based via `Math.max(0, line - 1)`. The existing slice test was asserting the wrong behavior; updated to expect the spec-correct result and added a second `line:3, limit:2` case to lock in the offset. 2. `modelServiceId` was accepted by the SDK + server `POST /session` path, forwarded into `bridge.spawnOrAttach`, and then silently dropped: `doSpawn` never wired it into the agent. Callers requesting a specific model got the agent's default and no indication anything was wrong. Now `doSpawn` issues `unstable_setSessionModel` immediately after `newSession`. If the agent rejects the model id, the half-initialized session is torn down and the spawn rejects so the caller can retry cleanly instead of inheriting silent drift. Three new bridge tests: happy path, omit-when-undefined, agent-rejection cleanup. 3. The CORS middleware used `cors({ origin: (o, cb) => cb(new CORSError(...), false) })` for browser-Origin requests. `cors` flows the Error into Express's error chain; without an explicit error handler that produces a 500 + HTML body, which is misleading for what is really a deterministic 403 denial. Replace with a tiny `RequestHandler` that checks `req.headers.origin` directly and returns `403 { error: 'Request denied by CORS policy' }` JSON. Drops the `cors` and `@types/cors` dependencies — there's no other consumer in the cli package. 4. The SSE `stream_error` synthetic frame hard-coded `id: 0`, which would regress the client's `Last-Event-ID` tracker and trigger duplicate replays on reconnect. The frame is terminal and daemon-emitted — it has no place in the per-session monotonic sequence. Refactor `formatSseFrame` to omit the `id:` line when the input event has no id field, and emit `stream_error` without one. Test updated to assert `frames[1].id === undefined` while the preceding `session_update` still carries its monotonic id. Tangential cleanup: `errorMessage` now formats the SSE error body (was `err.message` only — would have shown `[object Object]` for JSON-RPC errors mid-stream, mirroring the round-1 SDK fix). Test counts: cli serve **96** (was 93, +3 modelServiceId cases); existing readTextFile slice test rewritten in place. Full typecheck + lint + suite green. * fix(cli,sdk): PR #3889 review round 2 — SSE robustness + EventBus polish (#3803) Second batch of reviewer-flagged fixes for PR #3889. Addresses 7 robustness issues across the daemon's SSE pipeline + the bus + the SDK's stream parser. Daemon SSE (`server.ts`): - SSE writes now respect backpressure. `res.write` returns false when the kernel send buffer is full; the previous code ignored that and Node accumulated payloads in user-space memory unboundedly. A slow consumer on a chatty session could balloon daemon RSS. New `writeWithBackpressure` helper awaits `drain` (or `close`/`error`) before scheduling the next write — for both per-frame writes and heartbeats. - `parseLastEventId` rejects values > `Number.MAX_SAFE_INTEGER`. With the prior `^\d+$` regex a malicious 25-digit value would parse to a number that loses precision and confuses replay comparisons. EventBus (`eventBus.ts`): - `Last-Event-ID` replay events now `forcePush` past `maxQueued`. A client reconnecting with a 1000-event gap on a subscriber whose cap is 256 was silently losing entries 257-1000 — a sign-off-by- nothing breakage of the resume contract. Live publishes still go through the normal cap (slow live consumer must be evictable); historical replay is bypassed. - `onAbort` now disposes the subscription immediately instead of only closing the queue. An aborted-but-never-iterated subscriber used to linger in `bus.subs` until the consumer drove `next()` / `return()`. New tests cover both abort-after-subscribe and already-aborted-at-subscribe paths. - `BoundedAsyncQueue.next` now checks `buf.length > 0` before shifting instead of `buf.shift() !== undefined`. The bus never pushes `undefined` today but the queue is generic — the prior pattern would mis-handle a queue whose element type legitimately includes undefined. SDK SSE parser (`sse.ts`): - Now flushes the TextDecoder on stream close. Without the final `decoder.decode()`, an incomplete multi-byte UTF-8 sequence at the tail of the last chunk was silently dropped — corrupting any frame whose JSON ended mid-character. New test feeds a stream split mid-byte through "中" (3-byte UTF-8) and asserts the character round-trips. - Frame separators now accept both `\n\n` and `\r\n\r\n`. SSE spec allows CRLF, and intermediaries (corporate proxies, some Node http servers) sometimes normalize. Frame field splitter also accepts `\r?\n`. Two new tests cover pure CRLF + mixed-LF/CRLF. Test counts: cli serve **99** (was 96, +3 EventBus); sdk daemon-sse **10** (was 7, +3). Full typecheck + lint + suite green. * docs(cli,sdk): PR #3889 review round 3 — minor + docs (#3803) Last batch from the PR #3889 reviewer pass: mostly docs + a ReDoS-tooling-silencing rewrite + a yargs-key cleanup. - `commands/serve.ts` ServeArgs interface dropped the camelCase `httpBridge` mirror; the handler now reads `argv['http-bridge']` matching the declared option name. The dual surface relied on yargs's camelCase expansion behavior — fragile if yargs config ever changes. - `DaemonClient` constructor's `baseUrl.replace(/\/+$/, '')` (which is end-anchored and linear, but CodeQL's polynomial-regex detector flags any `\/+$` pattern on attacker-controlled input) swapped for a hand-rolled `stripTrailingSlashes` loop. Same behavior, no rule trigger. - `defaultSpawnChannelFactory`'s `cwd: workspaceCwd` flow into `spawn` is the second CodeQL finding ("uncontrolled data used in path expression"). It IS user-controlled, by design — that's the Stage 1 trust model. Added a `// lgtm[js/shell-command- constructed-from-input]` suppression with a comment explaining the model and pointing at issue #3803 §11 for the Stage 4+ remote- sandbox replacement. - Stale doc comment on `createServeApp` that still listed only `/health`, `/capabilities`, `POST /session` as shipped — now enumerates all 9 routes that match §04 of the design. - Stale doc comment on `HttpAcpBridge` saying "Stage 1 buffers them in-memory; SSE wiring lands in the next PR" — SSE wiring landed in commit |
||
|
|
fd53527aad
|
feat(cli): support batch deletion of sessions in /delete (#3733)
* feat(cli): support batch deletion of sessions in /delete Closes #3706 Add multi-select mode to the session picker so /delete can remove multiple sessions at once. Space toggles a checkbox on the cursor item; Enter commits the checked set, falling back to single-select when nothing is checked. The current active session is rendered disabled with a "current — cannot delete" hint and is also stripped defensively before the service call. Core gains a `removeSessions(ids)` batch API that returns `{ removed, notFound, errors }` so the CLI can surface partial failures with a single toast. Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com> * fix(cli): address /delete batch-delete review comments - useSessionPicker: when checked items are all hidden by the branch filter, do not silently fall through to single-deleting the cursor row (data-loss path); stay in multi-select mode instead. - SessionPicker footer: count only the checked-and-visible-and-committable rows so "N selected" matches what Enter would actually delete. - useDeleteCommand: partial-failure toast switches to type=error and surfaces failing session ids (truncated, capped at 3 with overflow) plus the first underlying error message, instead of just an aggregate count masquerading as info. - Docs: fix Tab→Space JSDoc / inline-comment drift across the picker surface (Space is the actual binding). Co-Authored-By: Qwen-Coder <noreply@qwenlm.com> * docs(cli): clarify disabledIds is multi-select-only on session picker Reviewer flagged that disabledIds is silently inert in single-select mode because both its visual dim and Space no-op gate on enableMultiSelect. Spell that out on both prop JSDocs and point future callers at filtering initialSessions for single-select use cases. Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com> * fix(cli): address /delete batch-delete review feedback (round 2) - useSessionPicker: throw when enableMultiSelect is on without onConfirmMulti (footer would otherwise read "N selected" while Enter silently fell through to single-select on the cursor row) - useSessionPicker: single-select Enter fallback now respects disabledIdSet so a stray Enter on the dimmed active-session row no longer closes the dialog and bounces back with an error - useDeleteCommand: stop swallowing the outer catch — log + surface the underlying error message so on-call has something to grep - useDeleteCommand: full-failure branch now mirrors the partial- failure branch (failing ids + first error reason) instead of a generic "Failed to delete sessions." - useDeleteCommand: emit a "Deleting N session(s)..." progress toast before awaiting removeSessions so slow filesystems don't leave the user staring at a closed dialog - sessionService: JSDoc {@link removed}=false → {@link notFound} Tests: regression for the disabled-row single-select fallback, two invariant-throw tests for the picker, a pre-await progress toast test, and align the StandaloneSessionPicker branch-filter tests with main's Ctrl+B-only binding (plain 'B' silently entered search mode after the merge, masking the assertion). Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * test(cli): drop wait-based multi-select picker tests, cover at hook level The Multi-select describe block in StandaloneSessionPicker.test.tsx was 7 ink-rendering tests that all relied on `await wait(N)` to sync with stdin events — the same flaky shape #3978 already purged from the search suite. CI flaked on them once and they're gone for good. Critical invariants are re-asserted at hook level in useSessionPicker.test.tsx (toggleChecked add/remove + no-op on disabled ids), which exercise pure state without keypress sim. Footer-rendering and keypress-driven flows are intentionally left to manual verification rather than carried as wait-based integration tests. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * test(cli): cover batch delete keyboard and failure paths * fix(cli): address /delete batch-delete review feedback (round 3) - useSessionPicker: Enter now commits *every* checked id (minus disabled), not just the filtered intersection. Filter is a navigation aid; gating the commit on it would silently drop checks the user explicitly made (check A-E, type a query matching only C-E, lose A and B). Order by sessionState.sessions so the receiver sees display order even for filter-hidden items. - SessionPicker: footer count switches from visibleCheckedCount to committableCheckedCount (all checkedIds minus disabled), so it can no longer say "0 selected" while Enter is about to delete three hidden checks. - useDeleteCommand: hoist sampleIds/overflow/firstError/reason above the three-way branch so partial- and full-failure paths can't drift out of sync on a future tweak. - useDeleteCommand: in-flight ref guard wrapped in try/finally drops re-entrant /delete invocations (closeDeleteDialog runs synchronously, so without this the user can re-open /delete and queue an overlapping batch). Guard releases on early return too, otherwise a "only current selected" rejection would lock out the rest of the session. - useDeleteCommand: surface "Current active session skipped." info toast when the picker forwarded the active session, otherwise the progress toast lies about the count. Tests: - useDeleteCommand: full-failure branch (removed=0), re-entrant drop, guard-released-on-early-return, stripped-current info toast - useSessionPicker: hook-level keypress suite covering Space toggle, Space-disabled no-op, Enter→onConfirmMulti, Enter→onSelect fallback, Enter-disabled no-op, Enter commits hidden checks, Enter refuses when every check is disabled. MockStdin pattern cloned from useKeypress.test.ts, no ink rendering and no wait(). Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * chore(cli): trim delete-many comments * fix(cli): guard delete actions during batch delete * test(cli): cover batch delete guard release * fix(cli): address session delete review feedback * fix(cli): address batch delete review follow-ups --------- Co-authored-by: Qwen-Coder <noreply@alibabacloud.com> Co-authored-by: Qwen-Coder <noreply@qwenlm.com> Co-authored-by: Qwen-Coder <noreply@qwen.ai> Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> |
||
|
|
faf646b081
|
docs(auth): add custom API key wizard PRD (#3583)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
d07daa3e69
|
fix(cli): auto-restore prompt and preserve queue on cancel (#4023)
* fix(cli): auto-restore prompt and preserve queue on cancel; align with Claude Code
When a user pressed ESC immediately after submitting a prompt (before the
model produced any meaningful output), qwen-code left the cancelled prompt
stranded in the transcript and in cross-session ↑-history. Cancelling
during tool execution also silently dropped any queued follow-up input.
Mirror Claude Code's auto-restore-on-interrupt:
- Drain the queue back into the input buffer on EVERY cancel path,
including tool-execution cancels (replaces the unconditional
clearQueue() that motivated #3204 with a non-destructive pop).
- When the user cancels with no draft text, no queued input, and no
meaningful pending/committed assistant content, truncate the user
item and trailing INFO from history and pull the prompt text back
into the input box for editing.
- Add Logger.removeLastUserMessage so the disk-backed cross-session
↑-history (getPreviousUserMessages) is also cleaned on cancel.
The "meaningful content" check matches Claude Code's
messagesAfterAreOnlySynthetic: gemini text and tool runs are meaningful;
info/error/warning/retry/notification/tool_use_summary/thoughts are
synthetic. truncateToItem uses functional setState so it batches with
the INFO addItem from cancelOngoingRequest in the same render pass —
no flicker.
Tests cover all five guard branches and the logger undo across normal,
no-op, one-shot, MODEL_SWITCH-interleaved, disk-rotation, and
uninitialized cases.
* fix(core): clear lastLoggedUserEntry on logMessage write failure
Without this, a transient writeFile error during a USER logMessage left
the undo tracker pointing at the previous successful entry. A subsequent
removeLastUserMessage (e.g., from auto-restore on cancel) would then
silently delete an unrelated earlier row from disk-backed history.
Add a regression test that mocks a writeFile rejection and asserts the
tracker is null and the prior entry survives.
Reported in PR review.
* fix(cli, core): share Logger across AppContainer/useGeminiStream and serialize writes
PR-review follow-up addressing two issues in the cancel-undo path.
1. Logger instance mismatch (Critical):
`useGeminiStream` and `AppContainer` each called `useLogger()`, which
instantiates a fresh `Logger` per call. `lastLoggedUserEntry` lives on
the instance, so the undo invoked from `AppContainer` was always a
no-op — the cancelled prompt still surfaced via cross-session
`getPreviousUserMessages`. Move the `useLogger` ownership to
`AppContainer` and pass the same instance into `useGeminiStream` via a
new optional `logger` parameter.
2. Logger write ordering:
Both `logMessage` and `removeLastUserMessage` do read → splice/append
→ writeFile without a lock. A fast cancel-then-resubmit could let
`removeLast` clobber a just-appended new entry. Add a per-instance
`serialize()` helper (a Promise-chained write queue) and route both
mutating ops through it. Reset the queue on `close()`. New regression
test fires removeLast and a fresh logMessage in parallel and asserts
the resubmitted entry survives.
3. Stale React-state race in cancel guard (Suggestion):
The auto-restore guard read `pendingGeminiHistoryItems` from React
state, which can lag a stream chunk that just set
`pendingHistoryItemRef.current`. Snapshot the pending item at the
start of `cancelOngoingRequest` and pass it through the new
`onCancelSubmit({ pendingItem })` info parameter. The guard combines
it with the React-state items so any meaningful in-flight content
blocks auto-restore even before re-render. New test covers the case
where pendingHistoryItems is empty but info.pendingItem carries
`gemini_content`.
All touched-area suites pass: 64 cli AppContainer, 9 historyUtils,
85 useGeminiStream, 46 core logger.
* fix(cli): unbreak build after import-merge regression and tighten cancel-handler test types
The pre-commit eslint --fix on the previous commit collapsed the two
consecutive `import { ... } from '@qwen-code/qwen-code-core'` blocks in
useGeminiStream.ts into a single statement, but kept the `import type`
modifier from the first block — silently turning every runtime symbol
(SendMessageType, MessageSenderType, GitService, ApprovalMode, …) into
type-only imports. tsc rejected with TS2206 + a wave of TS1361 errors
that only surfaced on CI.
Restore the two separate imports: pure-type symbols (Logger included)
in `import type { ... }`, runtime symbols in plain `import { ... }`.
Also: the AppContainer cancel-handler tests captured `onCancelSubmit`
as `() => void`, but the hook signature now takes an optional info
arg. Widen the captured-callback type so passing `{ pendingItem }`
typechecks (TS2554 on line 1053).
* fix(cli, core): tighten cancel-undo robustness from PR review batch 3
Four follow-ups from a /review pass on the auto-restore-on-cancel path.
* logger.ts — only invalidate `lastLoggedUserEntry` when the failed
write was itself a USER attempt. A failed non-USER write (MODEL_SWITCH
on a transient disk error, etc.) doesn't change which row was the
most recent user prompt, so the prior undo target is still valid.
Without this, MODEL_SWITCH disk hiccups silently disabled cancel-undo.
* useGeminiStream.ts — wrap `onCancelSubmit` in try/finally so a throw
in AppContainer's cancel handler can't strand the stream in
Responding (the UI would lock — Esc would no-op until process
restart). `setIsResponding(false)` and `setShellInputFocused(false)`
always run.
* useGeminiStream.ts — also document the three-way coupling between
the INFO `addItem` here and AppContainer's auto-restore guard:
the guard reads `historyRef.current` which doesn't yet contain
this INFO (React batches), and the guard's correctness depends on
the items added here staying synthetic.
* historyUtils.ts — make `isSyntheticHistoryItem` exhaustive over the
35-member `HistoryItemWithoutId` union. Every case is explicit; the
default branch carries a `_exhaustive: never` so adding a new
HistoryItem variant without classifying it triggers a compile-time
error rather than silently disabling auto-restore. Runtime fallback
is "meaningful" (safe — bail rather than wipe content).
Tests: +1 logger case (non-USER failure preserves the USER tracker),
+1 useGeminiStream case (throwing handler still flushes Responding).
All touched suites pass: 47 logger, 9 historyUtils, 86 useGeminiStream,
64 AppContainer.
* docs(core): clarify Logger writeQueue scope (log-history only, not checkpoints)
Reword the comment above `writeQueue` and the `serialize()` JSDoc to
state explicitly that the queue only serializes log-history mutations
(`logMessage` / `removeLastUserMessage`). Checkpoint ops
(saveCheckpoint / deleteCheckpoint / loadCheckpoint) touch separate
files and intentionally don't share this queue, so the previous
"every disk-mutating op chains here" wording overstated the
guarantee.
* fix(cli): flush buffered stream events before snapshotting pendingItem on cancel
Stream content/thought events are throttled into a per-turn `bufferedEvents`
array; only when `flushBufferedStreamEvents` runs do they reach
`pendingHistoryItemRef.current`. Snapshotting BEFORE the flush meant cancels
that fired inside the throttle window (60ms) saw a null `pendingItem` even
when meaningful text was sitting in the buffer. AppContainer's auto-restore
guard then read null, decided "model produced nothing", and called
`truncateToItem` — which silently wiped the very content that the
subsequent `addItem(pendingHistoryItemRef.current)` had just committed.
Move the snapshot to AFTER the flush so it sees the same value as the
addItem call directly below it.
Regression test: yields a content event and cancels without advancing
fake timers, asserts `info.pendingItem` carries the buffered "partial
response" text rather than null.
* fix(core): apply Logger.removeLastUserMessage in-memory removal synchronously
AppContainer's `userMessages` effect calls `getPreviousUserMessages()`
on the same render that history truncation fires (it depends on
`historyManager.history`). The previous implementation only updated
`this.logs` after `await fs.writeFile(...)` settled, so the effect
read stale logs and ↑-history surfaced the cancelled prompt until
some unrelated future history change re-ran the effect.
Move the cache filter ahead of the serialize queue so consumers see
the removal immediately. The async serialize op continues to read,
splice, and write disk, then re-syncs `this.logs` from disk on
success or rotation.
Regression test fires removeLast without awaiting, then asserts the
very next `getPreviousUserMessages()` returns [] (no cancelled
prompt), and that the background promise still resolves to true.
* docs(core, cli): clarify removeLastUserMessage contract; observability for cancel-undo
* logger.ts — extend the JSDoc on `removeLastUserMessage` to spell
out the two-phase semantics (sync optimistic in-memory removal +
async serialized disk reconciliation), and explicitly document that
the boolean return value reflects the *disk* outcome while the
in-memory cache is updated unconditionally. Also explain why disk
failures are NOT rolled back: rolling back would resurrect the
cancelled prompt in ↑-history, which is worse UX than a temporary
cache/disk divergence (which converges on next op or on
`initialize()` of the next session).
* AppContainer.tsx — wrap the fire-and-forget
`logger.removeLastUserMessage()` in `.catch(debugLogger.debug)`.
The Logger's internal try/catches mean the Promise should never
reject today, but a future code-path change shouldn't surface as
an UnhandledPromiseRejection — and a debug-level log is the right
observability hook for "cancel succeeded in UI but disk-undo
failed silently".
* fix(core,cli): #4023 review wave — logger atomicity + observable undo failure
3 #4023 review threads addressed:
- core/logger.ts: `removeLastUserMessage` now ROLLS BACK the
optimistic in-memory removal when the disk read or write fails.
Previously the JSDoc/return contract was violated: the method
returned `false` on failure but `this.logs` already showed the
entry removed — callers (AppContainer's `userMessages` effect)
saw the inconsistency and the cancelled prompt vanished from
↑-history despite the disk still carrying it. The rollback
re-inserts the target at its original index when no concurrent
mutation took its place, and restores `lastLoggedUserEntry` so
a follow-up retry has a target. Regression test pinned: spy on
fs.writeFile to throw, assert `removed === false` AND
getPreviousUserMessages() still surfaces the entry.
- cli/AppContainer.tsx: `void logger?.removeLastUserMessage()` no
longer silently swallows failures. Added `.catch` that routes
through `debugLogger.debug` so a disk-write failure leaves a
diagnostic trail; without it the cancelled prompt would
resurrect next session via ↑-history with no observability into
why.
- cli/historyUtils.ts: `gemini_thought` / `gemini_thought_content`
classification reaffirmed as SYNTHETIC with explicit JSDoc on
WHY (Claude Code parity + auto-restore is most valuable in the
cancel-during-thinking case which is exactly the case where
thoughts have appeared but no committed `gemini_content`).
Future readers won't re-litigate the classification by accident.
Tests: 49/49 logger.test.ts pass; tsc + ESLint clean.
* docs(core): align removeLastUserMessage JSDoc with rollback-on-failure behaviour
The previous commit added a rollback path to `removeLastUserMessage`
(re-insert the optimistically-removed entry and restore
`lastLoggedUserEntry` when the disk read or write throws), but the
JSDoc still said the in-memory removal is "intentionally NOT rolled
back" — a copy-paste leftover from the earlier design that picked
optimistic-and-diverge. Rewrite the failure-handling paragraph and
`@returns` line to describe the rollback contract instead.
No code change.
* fix(cli, core): scope auto-restore to the cancelled turn + tighten typings/tests
Three follow-ups from PR #4023 review batch 5.
* cli — `CancelSubmitInfo` gains `lastTurnUserItem` carrying the user
prompt text that THIS turn's `prepareQueryForGemini` added (or
`null` for paths that don't push a user history item: Cron /
Notification / slash `submit_prompt`). `cancelOngoingRequest`
snapshots `lastTurnUserItemRef.current` and ships it through. The
AppContainer auto-restore guard now requires
`info.lastTurnUserItem` to be present AND match the candidate
user item's text before truncating/rewinding — closing the case
where an older user item happens to be followed by only-synthetic
trailing content and the current cancelled turn never owned a
user item to begin with.
Two new regression tests pin both halves: cancel of a non-USER
turn bails despite trailing-synthetic, and a deliberate text
mismatch also bails.
* cli — `.catch((err)` widened to `(err: unknown)` on the
fire-and-forget `logger.removeLastUserMessage()` call. Belt-and-
braces: `Promise.catch`'s lib typing is `(reason: any) =>` so
this is not currently TS7006, but tightening keeps the codebase
ready for `@typescript-eslint/no-implicit-any-catch`-style rules
and matches the rest of the codebase's strict-error patterns.
* core — Added a `removeLastUserMessage` regression test pinning
the `_readLogFile` failure branch (mocks `fs.readFile` to throw
Permission denied). The symmetric `writeFile` failure case was
already covered; this closes the gap on the read leg.
Tests: AppContainer 67/67 (+2), useGeminiStream 87/87, historyUtils 11/11,
logger 50/50 (+1). Type-check and lint clean.
* chore(cli): add debug observability for each auto-restore-on-cancel bail-out
The cancel handler in AppContainer has seven independent guards that
silently `return` when auto-restore is unsafe (buffer non-empty, queue
non-empty, pending meaningful content, no last-turn user item, no user
in history, trailing items not all synthetic, candidate-text mismatch).
Until now, users reporting "I pressed ESC but my prompt didn't come
back" had no way to know which guard tripped without a debugger.
Log a specific `debugLogger.debug(...)` line at each bail-out and one
on the success path. Debug level keeps production output silent;
re-enableable by running with `DEBUG=1` (per existing convention in
this file). No control-flow change.
* docs(core): scope removeLastUserMessage's "false ⇒ observable in-memory" guarantee
The previous JSDoc implied the guarantee held for every `false` return,
but it only really holds on the disk read/write THROW path (where we
roll back the optimistic in-memory removal). Two other `false`-paths
behave differently:
- Initial guards (logger uninitialized / no tracked entry): nothing
was ever removed, nothing to restore — entry stays in whatever
state it was already in.
- Disk read succeeds but the tracked row is missing on disk (e.g. a
concurrent rotation/clear): we adopt disk state into `this.logs`,
so both sides agree the entry is gone — `false` is returned but
the entry is NOT observable in-memory either.
Rewrite the failure-handling paragraph and `@returns` line to spell
out both branches explicitly. No code change.
* fix(core): shift lastLoggedUserEntry on USER logMessage duplicate-skip
When `_updateLogFile` detects another instance already wrote an
identical (sessionId, messageId, timestamp, message) row and returns
null, the previous logMessage code path left `lastLoggedUserEntry`
pointing at the prior USER entry. A subsequent cancel/auto-restore
would then call `removeLastUserMessage()` and silently delete the
wrong row — typically an older prompt that the user did not intend
to undo.
The fix: when the duplicate skip happens on a USER attempt, advance
`lastLoggedUserEntry` to the entry object we just tried to write.
`_updateLogFile` mutates that object's `messageId` in-place to align
with the disk row before the duplicate check, so the 5-tuple matches
the row that's actually on disk and an undo correctly targets it.
The natural race (`max+1` colliding with an existing `messageId`)
is not reachable by sequential awaits — the snapshot used for the
duplicate check is always max+1-strict. The regression test drives
the contract directly by mocking `_updateLogFile` to resolve to
null and asserting `lastLoggedUserEntry` shifts to the new entry.
* fix(cli, core): strip orphan user entry from chat history on auto-restore
The auto-restore branch was cleaning up two of the three places a
cancelled prompt lives — the UI transcript via `truncateToItem` and
the disk-backed ↑-history via `Logger.removeLastUserMessage`. The
third — the in-memory chat history on `GeminiChat` — was left
untouched. `sendMessageStream` appends the user content to
`chat.history` BEFORE the stream generator runs and the abort path
doesn't pop it. After a successful auto-restore the next request's
wire payload still carried the cancelled prompt as a leading user
turn alongside the new prompt, so the model saw context the user
believed had been undone (and in some shapes the API would reject
two consecutive user turns).
Mirror the existing strip the Retry submit path uses
(`GeminiClient.sendMessageStream` at the `Retry` branch): make
`GeminiClient.stripOrphanedUserEntriesFromHistory` public and call
it from the auto-restore success path, sitting next to the UI
truncate and the disk-log undo. The method already pops trailing
user entries and clears the `FileReadCache` (which can otherwise
hold dangling `read_file` results from the stripped turn).
End-to-end reproduction from the PR review:
1. Submit `what time is it?` → ESC during pre-token delay →
auto-restore (UI rewound, buffer pre-filled).
2. Edit buffer to `what year is it?` → submit.
3. Pre-fix: outbound `messages` carried both prompts as consecutive
user turns. Post-fix: only the new prompt.
Test: extend the auto-restore-success AppContainer test with a
mock `stripOrphanedUserEntriesFromHistory` spy and assert it fires.
The non-restore branches don't install the spy (it's optionally
chained at the call site).
* fix(core, cli): tighten Logger.serialize signature + pin lastTurnUserItem and dup-skip identity contracts
Three follow-ups from PR review batch:
* core/logger.ts — `serialize()` was `this.writeQueue.then(op, op)`.
The second callback was dead code: `writeQueue` is seeded with
`Promise.resolve()` and reassigned through `.catch(() => undefined)`,
so the queue tail can never reject. Worse, `then(op, op)` reads as
"retry op on rejection" — wrong intent. Switch to `.then(() => op())`
with a comment spelling out the no-reject invariant.
* cli/useGeminiStream.test.tsx — add ownership-contract tests at the
PRODUCER side of `info.lastTurnUserItem`. Until now only the
AppContainer tests pinned the contract, and they fabricate the
value, so a regression that drops `lastTurnUserItemRef.current = {
text: trimmedQuery }` in `prepareQueryForGemini` would slip
through. New tests:
- normal `UserQuery` submit → cancel → assert
`info.lastTurnUserItem === { text: 'what time is it?' }`.
- `SendMessageType.Notification` submit → cancel → assert
`info.lastTurnUserItem === null` (path doesn't push a user
history item, the ref reset at the top of
prepareQueryForGemini must keep it null).
* core/logger.test.ts — strengthen the duplicate-skip regression.
The previous test only checked the tracker advanced text; the
important identity contract is that the recalculated 5-tuple
matches the disk row, so a subsequent `removeLastUserMessage()`
removes the duplicate-skipped row rather than the older USER.
New test seeds disk with [first, second], stubs `_updateLogFile`
for the second call to mimic the duplicate-skip branch (mutate
newEntryObject's messageId+timestamp to align with the disk row,
return null), then asserts removeLastUserMessage() leaves
['first'] on disk and removes 'second'.
* fix(cli, core): close four cancel-auto-restore correctness gaps from PR review
Four critical findings from gpt-5.5 /review pass:
1. **Retry skipped the lastTurnUserItem reset** (useGeminiStream.ts)
`Retry` bypasses `prepareQueryForGemini`, which is where the
`lastTurnUserItemRef.current = null` reset lived. A retry that
followed a normal `UserQuery` carried the stale ownership snapshot
into `onCancelSubmit`, and cancelling the retry before any
meaningful output let `AppContainer` auto-restore truncate the
original failed prompt. Move the reset (and the new content-seen
reset, see #4) to the top of `submitQuery`, gated only on
"this is a top-level submit" — covers Retry, Cron, Notification,
and ordinary UserQuery alike.
2. **Text-only ownership matched dedup'd duplicates** (AppContainer.tsx,
useGeminiStream.ts) `useHistoryManager.addItem` skips inserting a
consecutive-duplicate user message while still returning a freshly
generated id. The text-only ownership check would match the OLDER
identical-text USER row, so a re-submitted same prompt + cancel
would wrongly truncate the prior turn. Carry id+text in
`CancelSubmitInfo.lastTurnUserItem` (using `addItem`'s return
value) and require both id AND text to match before truncating.
3. **stripOrphan left IDE context state advanced** (client.ts) Other
history-mutating paths (`setHistory`, `truncateHistory`) set
`forceFullIdeContext = true` after mutating; the orphan-strip
didn't, so a subsequent request could send a diff against a
removed baseline. Gate cache-clear + IDE-context invalidation on
an actual before/after length drop, so no-op strips don't churn
state.
4. **Flush-then-thought race let auto-restore wipe committed content**
(useGeminiStream.ts, AppContainer.tsx) `cancelOngoingRequest`'s
pre-cancel flush can `addItem` a meaningful `gemini_content` (via
handleContentEvent's split path) and then a later thought event
overwrites `pendingHistoryItem` with a synthetic value. The
AppContainer guard's React history snapshot is stale, so the
trailing-only-synthetic check passes and the just-committed text
gets truncated. Track a synchronous `turnSawContentEventRef` set
in handleContentEvent, ship it through `CancelSubmitInfo`, and
make the guard bail when set.
Tests:
- core/client.test.ts: stripOrphan only forces full IDE context on
actual removal; existing retry tests updated to mock
`getHistoryLength`.
- cli/useGeminiStream.test.tsx: ownership uses { id, text }, Retry
reset works after a prior UserQuery cancel,
turnProducedMeaningfulContent flips true when content lands.
- cli/AppContainer.test.tsx: guard bails on `turnProducedMeaningfulContent: true`,
guard bails on id mismatch (catches addItem dedup case).
cli 162/162 + core client 99/99 + core logger 51/51.
* fix(cli): repaint static transcript after auto-restore truncate
Reported by @tanzhenxin: auto-restore truncated React `history` state
but the cancelled `> prompt` and `Request cancelled.` lines stayed
printed in the terminal — Ink's `<Static>` region is append-only, so
shrinking the underlying array doesn't unprint already-flushed lines.
On the PR's golden path (type prompt → Enter → ESC) the user sees the
prompt twice: once in scrollback, once pre-filled in the input buffer.
Confirmed at multiple Enter-to-ESC delays, so it's not a timing fluke.
Call `refreshStatic()` immediately after `truncateToItem(...)` in the
auto-restore success path. `refreshStatic` writes the ANSI
clear-terminal escape AND bumps the static remount key — the exact
recipe `/clear` (`handleClearScreen`) already uses for the same
reason. The targeted-repaint helper used for terminal resizes is
intentionally NOT used here: it preserves scrollback, which would
leave the cancelled prompt visible above the new viewport.
Test: extend the existing auto-restore happy-path AppContainer test
to assert `mockStdout.write` was called with `ansiEscapes.clearTerminal`.
The other auto-restore-bail tests don't install the assertion so they
naturally verify the negative case (no clear when guard rejects).
|
||
|
|
720ccaccb7
|
fix(core): normalize cumulative OpenAI stream deltas to suffixes (#3896)
Some OpenAI-compatible upstreams (notably specific DashScope / 阿里云百炼 Coding Plan paths) send `delta.content` as accumulated full text instead of incremental suffixes. The Gemini stream pipeline appended every chunk verbatim, so the same content was concatenated repeatedly during streaming. Adds a per-stream `normalizeStreamingTextDelta` that detects cumulative-mode chunks (current chunk starts with previously emitted text) and emits only the suffix. Once cumulative mode is locked in, exact-repeat (≥64 chars) and prefix-overlap chunks are silenced. Normal incremental streams flow through unchanged. The fix applies to both `delta.content` and `delta.reasoning_content` / `delta.reasoning`, with separate state slots so a switch between text and reasoning channels does not cause false-positive suppression. A separate `emittedLength` counter tracks user-visible bytes so an incremental-then-cumulative hybrid stream slices the correct suffix even after the 1024-byte detection-window cap kicks in. Fixes #3279, #1184, #3838, #3806. |
||
|
|
533daac316
|
feat(cli): wrap markdown links in OSC 8 so wrapped URLs stay clickable (#4037)
* feat(cli): wrap markdown links in OSC 8 so wrapped URLs stay clickable Long URLs the model emits inside `[label](url)` or as bare `https://...` get line-wrapped by the terminal, which prevents most emulators from detecting them as a single clickable region. OSC 8 hyperlinks decouple the link target from the visible label so the entire label remains one clickable target regardless of where it wraps. - Extract the existing OSC 8 helpers from AuthenticateStep into a shared packages/cli/src/ui/utils/osc8.ts util, plus a dependency-free capability detector that honors NO_COLOR / FORCE_COLOR=0 / CI / non-TTY stdout, with FORCE_HYPERLINK=1 and QWEN_DISABLE_HYPERLINKS=1 overrides for explicit opt-in / opt-out. - Wire InlineMarkdownRenderer to wrap markdown link labels and bare autolinks in an OSC 8 envelope when supported. Wrapping happens after the inline link token has been fully matched, so streamed partial chunks cannot split an envelope across flushes. - Fall back to the legacy `label (url)` rendering byte-for-byte when the host terminal does not advertise OSC 8 support. Closes #3954 * fix(cli): harden OSC 8 markdown wrapping after multi-round audit Address findings from a multi-round design and code audit of the OSC 8 hyperlink feature: Design fixes: - Keep the visible `(url)` suffix in supported terminals too — preserves copy-paste UX and lets users preview suspicious URLs before clicking. OSC 8 is now purely additive (byte-identical unsupported output, plus envelope on supported terminals). - Restrict OSC 8 wrapping to http/https/mailto/ftp/sftp/ssh schemes; javascript:/data:/file:/vbscript: fall through unwrapped so the user can read the target. Prompt-injection defense for LLM output. - Reject URLs with whitespace — every terminal treats whitespace in an OSC 8 target as truncation/rejection, which would turn the whole region into an un-clickable trap. - Block OSC 8 inside tmux/screen by default; require `FORCE_HYPERLINK=1` opt-in. The multiplexer hides the host terminal's capabilities, so emitting passthrough escapes on a host without OSC 8 prints garbage. - Version-gate `supportsHyperlinks()` (iTerm ≥3.1, vscode ≥1.72, WezTerm ≥20200620, VTE ≥0.50 with 0.50.0 segfault carve-out), block CI / TEAMCITY / win32 (modulo WT_SESSION/Kitty/Ghostty/DOMTERM), mirror `supports-hyperlinks` semantics. - Extend the link regex to allow one level of balanced parens in the URL group so `[wiki](https://en.wikipedia.org/wiki/Foo_(bar))` isn't truncated at the inner `)`. - Trim trailing sentence punctuation off the OSC 8 *target* for bare URLs (`.`, `,`, `;`, `:`, `!`, `?`, `'`, `"`, `` ` ``) and unbalanced trailing `)]}` so the clickable URL resolves to a real page. - Catch VTE 0.50.0 reported in packed form (`'5000'`) — the original string compare missed it and let the segfault through. Code fixes: - Consolidate `wrapForMultiplexer` with the pre-existing `packages/cli/src/utils/osc.ts` — no more duplicate helpers. - Drop the `supportsHyperlinks` memoization cache so runtime env changes (NO_COLOR / theme toggles) take effect immediately. - Extract `MD_LINK_PATTERN`, `MD_LINK_CAPTURE`, `shouldWrapMarkdownLink`, and `HYPERLINK_ENV_KEYS` into `osc8.ts` so the React and ANSI renderers stay in lockstep. - Hoist `supportsHyperlinks()` once per render (both renderers). - Apply the same OSC 8 treatment to `TableRenderer` so markdown links inside tables are clickable too. - Rewrite `trimTrailingUrlPunctuation` to O(n) by pre-counting opens. Tests cover: balanced parens in URL, dangerous-scheme rejection, whitespace-URL rejection, trailing-punctuation trimming, tmux blocking, version gating (iTerm/WezTerm/vscode/VTE incl. packed form), platform fallbacks, mid-stream chunk balance, byte-identical legacy fallback. * feat(cli): detect Alacritty / Konsole / Warp / JetBrains / mintty for OSC 8 Expand supportsHyperlinks() to recognize five more capable terminals that the original detector silently treated as unsupported: - Alacritty ≥ 0.11 via TERM=alacritty (the issue explicitly calls this one out) - Konsole ≥ 21.04 via KONSOLE_VERSION - WarpTerminal via TERM_PROGRAM=WarpTerminal - JetBrains JediTerm (IDE integrated terminals) via TERMINAL_EMULATOR - mintty (Git Bash on Windows, etc.) via TERM_PROGRAM=mintty Hyper stays auto-detection-off (FORCE_HYPERLINK=1 override) because plugin chains have a long history of breaking escape passthrough. Apple_Terminal stays off because it has no OSC 8 support at all. KONSOLE_VERSION and TERMINAL_EMULATOR added to HYPERLINK_ENV_KEYS so the test isolation list stays in sync. * chore(cli): polish OSC 8 detector after another audit round Address findings from the final multi-round audit pass: - Document `FORCE_HYPERLINK` and `QWEN_DISABLE_HYPERLINKS` in the user-facing env-vars table at docs/users/configuration/settings.md so the new opt-in / opt-out surface is discoverable without grepping source. - Detect Alacritty even when the alacritty terminfo entry isn't installed (a common Linux distro scenario where Alacritty falls back to TERM=xterm-256color). Fall back to ALACRITTY_LOG / ALACRITTY_WINDOW_ID / ALACRITTY_SOCKET — Alacritty sets at least one of these unconditionally since 0.12. - Trim a trailing `>` off the OSC 8 target so CommonMark autolinks (`<https://example.com>`) produce a clickable target that actually resolves instead of 404-ing because of the captured delimiter. - Add OSC 8 / hyperlink env isolation to TableRenderer.test.tsx so a developer running the suite from iTerm2 / WezTerm / Kitty can't leak escape bytes into table output. - Symmetric `isTTY` reset in osc8.test.ts `beforeEach` so the early describes (sanitizer, scheme, trim) don't inherit residual TTY state from a prior test. - Document the deliberate security property of keeping the visible `(url)` suffix in OSC 8 mode (user always reads the destination before clicking) in the SAFE_OSC8_SCHEMES comment. - Collapse the `wrapForMultiplexer` import + re-export to a single `export { wrapForMultiplexer }` after the local import. - Add ALACRITTY_* keys to HYPERLINK_ENV_KEYS so test isolation lists stay complete. Tests cover the new autolink `>` trim, the Alacritty env-var fallbacks, and NBSP / Unicode-whitespace URL rejection. * fix(cli): tighten OSC 8 gating per PR review Two fixes from chiga0's review on PR #4037: 1. Move the non-TTY check above `FORCE_HYPERLINK` so a user with `FORCE_HYPERLINK=1` in their shell profile still gets a clean pipe when they run `qwen | cat` or `qwen > out.txt`. The "non-TTY stdout must suppress escapes" acceptance criterion now holds even under forced enable. 2. Version-gate the Konsole detection at `>= 21.04`. KONSOLE_VERSION is set by every Konsole release including ones that pre-date OSC 8 support, so the existence check alone false-positives on Konsole 20.x. Parse the packed integer (21.04 → 210400) and let older releases fall through to the legacy fallback. Updates the docs row for FORCE_HYPERLINK to make the non-TTY caveat explicit. Splits the prior "FORCE_HYPERLINK + isTTY=false" test into two — one verifying force works on a TTY, one asserting it never escapes the non-TTY guard. Adds a Konsole < 21.04 regression test. * fix(cli): stop auto-detecting Warp Terminal as OSC 8 capable Warp's current rendering engine doesn't honor OSC 8 envelopes — the escape sequence is printed as visible garbage rather than recognized as a clickable hyperlink. Falling through to the legacy `label (url)` rendering avoids the regression on Warp. Users on a Warp build that ever ships OSC 8 support can opt in with `FORCE_HYPERLINK=1`; the case will be reinstated in the switch when Warp lands real support upstream. Test flipped from "enabled" to "not auto-detected, FORCE_HYPERLINK opts in" to lock the new behavior. * feat(cli): drop visible (url) suffix when OSC 8 wrapping is active In the originally shipped renderer, `[label](url)` was rendered as `label (url)` even when OSC 8 wrapped the region. With long URLs that's clutter for no benefit — capable terminals already expose the target via hover / status bar / right-click "copy link" without needing the URL in the visible stream. When `shouldWrapMarkdownLink(url, canHyperlink)` returns true, the React renderer and the ANSI table renderer now emit only the markdown label (link-colored), with the OSC 8 envelope pointing at the full URL. Empty labels (`[](url)`) fall back to using the URL as the visible label so the link stays discoverable. When the predicate returns false (unsupported terminal, unsafe scheme, whitespace URL) the legacy `label (url)` rendering is preserved byte-for-byte — the scheme allowlist still guarantees the user sees the destination before any click on a `javascript:` / `data:` / etc. link. Tests updated to assert label-only visible bytes in wrap mode and an empty-label fallback case added. Comment block in `osc8.ts` updated to reflect the new visibility contract. * fix(cli): strip C1 controls in OSC 8 sanitizer sanitizeForOsc() only removed C0 + DEL, so 8-bit ST (\x9c) and 8-bit OSC (\x9d) bytes could still survive inside an OSC 8 target. On terminals that honor C1 controls, those bytes act as the same sequence boundaries as their two-byte ESC counterparts, which defeats the escape-injection hardening this helper is meant to provide. Extend the regex to also strip \x80-\x9f and cover the case with a test. * fix(cli): harden OSC 8 link sanitization and tighten gating Three independent issues found while auditing the markdown OSC 8 path: 1. sanitizeForOsc() previously left Unicode bidi controls (U+200E/F, U+202A-E, U+2066-9) and line/paragraph separators (U+2028/9) intact. A model-emitted RLO in a link label visually reverses trailing bytes, spoofing the host the user thinks they're clicking — exactly the click-deception attack the scheme allowlist is meant to block, just moved from the URL into the visible label. Extend the regex to strip those bytes too. 2. The visible label rendered inside the OSC 8 envelope went straight to the terminal without sanitization, so even with (1) the spoof would still land. Wire sanitizeForOsc() over the linkText in both InlineMarkdownRenderer and TableRenderer's OSC 8 branches. The legacy `label (url)` branches stay untouched so today's unsupported-terminal output remains byte-identical. 3. AuthenticateStep emitted osc8Hyperlink(authUrl) unconditionally, leaking escape bytes into pipes / non-OSC-8 terminals — inconsistent with the suppression contract documented for the rest of the PR. Gate it on supportsHyperlinks() so it falls back to the bare URL. Test coverage added: - sanitizeForOsc bidi/line-separator strip - bidi spoof in the rendered markdown label - byte-equality fallback on unsupported terminals - TableRenderer markdown link → OSC 8 (positive, fallback, unsafe scheme, bidi-spoof) — the table renderer had zero OSC 8 coverage before this. * fix(cli): keep `(url)` visible when an OSC 8 label looks like a different URL Adversarial round-2 audit identified a label-as-URL deception attack: when the OSC 8 branch elides the `(url)` suffix and shows only the clickable label, a model-emitted `[https://google.com](https://attacker.com)` renders a "google.com" link that resolves to attacker.com. Pre-OSC-8 rendering kept `(url)` visible so the user could see the real target; hiding it makes the click-deception case land. Mitigation: a new `labelMayDeceive(label, url)` predicate. When the label contains a URL-shaped substring AND it doesn't equal the actual target, both renderers keep the legacy `(url)` suffix while still emitting the OSC 8 envelope — the link stays clickable, the user still sees where the click goes. Heuristic is permissive on purpose: false positives are harmless (redundant `(url)` on niche labels), false negatives let a real spoof through. Tests: positive (mismatched URL labels), negative (label == url, plain text labels), in both InlineMarkdownRenderer and TableRenderer. * fix(cli): catch bare-host label deception in OSC 8 wrapping Round-3 audit caught a false-negative in labelMayDeceive: the `://` substring check only flagged labels with a fully-qualified URL shape. The most natural markdown spoof — `[google.com](https://evil.com)` — uses a bare host as the label and slipped past, so the OSC 8 branch elided the `(url)` suffix and rendered a clickable "google.com" that resolved to evil.com. Add a third detection pattern: extract host-like tokens from the label (`name.tld` with an alphabetic 2+ char TLD), and flag the link when any of them doesn't equal the URL's parsed hostname. Plain labels like `docs` / `click here` don't match the regex, version strings like `1.2.3` are skipped (last segment is numeric), and `[google.com](https://google.com)` is honest rendering — none of these get flagged. ASCII-only matching means an IDN-homograph attack on a bare-host label (Cyrillic `о`) still escapes this layer; the fully-qualified form of the same attack is still caught by the existing `://` rule, which is the only form an LLM is realistically likely to emit. Tests cover: bare-host mismatch, punycode IDN target, same-host / different-path, label==target negative, plain-text labels, version strings. * fix(cli): handle mailto: target in labelMayDeceive Round-4 audit caught a false positive: `new URL('mailto:x@y').hostname` is empty, so targetHostname() returned undefined and the defensive `return true` branch fired any time a mailto label contained an email-shaped string. A perfectly honest `[support@example.com](mailto:support@example.com)` was being flagged as deceptive and getting a redundant `(url)` suffix on capable terminals. Special-case mailto: by pulling the domain from after the `@` in the URL pathname, matching what the user would compare against. A mismatched mailto (e.g. `[support@example.com](mailto:abuse@evil.com)`) still flags correctly. Also drop a dead `HOST_LIKE_RE.lastIndex = 0` reset — `.match()` doesn't consult lastIndex, so the line was a no-op. * fix(cli): catch IPv4-literal label deception in OSC 8 wrapping Round-5 audit found another bare-host bypass: a label like `[1.1.1.1](https://attacker.com)` (or any other dotted-quad such as `[192.168.1.1]` / `[8.8.8.8]`) escaped labelMayDeceive because the existing host regex anchors on a 2+ alphabetic TLD. The user would see a clickable "1.1.1.1" that resolves to attacker.com with no visible target. Add a separate dotted-quad pattern and combine it with the host-token list before comparing against the URL's hostname. False-positive surface is small (over-permissive on octet ranges is harmless — worst case is an extra `(url)` suffix on a label like `999.999.999.999`). Tests cover mismatched IPv4, IPv4 spelled inside surrounding text, and label-equals-target IPv4 (which must NOT flag). * fix(cli): sanitize URL when rendered as visible text in OSC 8 path Two PR review findings: 1. config-utils.ts dropped the `resolvePath(...)` call (and its import) that origin/main introduced in #4045 for tilde / relative `cwd` paths in channel configs. The auto-merge silently reverted it the same way it did `packages/channels/base/src/index.ts`. Restore main's content. 2. Anti-spoof sanitization was only applied to `linkText`, but the OSC 8 render path emits the URL as visible text in two places that bypassed it: - empty-label fallback `safeLabel || url` — `[](https://x/aevil)` would print the URL with RLO intact even though the OSC target was sanitized. - deceptive-label `(url)` suffix. Compute `safeUrl = sanitizeForOsc(url)` once in the OSC 8 branch and use it for both visible-URL renderings. The OSC target inside `osc8Open` keeps the raw URL (sanitization happens inside the helper anyway). Same fix mirrored in `TableRenderer.tsx`. The legacy `label (url)` branch on unsupported terminals stays untouched so its byte-identical-fallback contract holds. Test added: `[](https://example.com/aevil)` round-trips through the renderer with the RLO stripped from both the OSC target and the visible URL fallback. |
||
|
|
7099165dae
|
fix(core): log internal OpenAI JSON requests (#4081)
* fix(core): log internal OpenAI JSON requests * fix(core): avoid duplicate OpenAI log metadata |
||
|
|
aecea70114
|
docs(telemetry): align config and docs semantics for target, outfile, and CLI flags (#4066)
* docs(telemetry): align config and docs semantics for target, outfile, and CLI flags - Remove stale warning note "This feature requires corresponding code changes" — the OTLP implementation is now complete (#3779, #4061) - Clarify that `target` is an informational destination label and does not control exporter routing; `otlpEndpoint` or `outfile` must be set to configure where data is sent - Mark `--telemetry-target` CLI flag as deprecated in the configuration table to match the deprecateOption() call in cli/src/config/config.ts - Fix `outfile` / `QWEN_TELEMETRY_OUTFILE` descriptions: remove the incorrect "when target is local" qualifier — outfile overrides OTLP export regardless of the target value - Simplify the file-based output example by removing the now-redundant `"target": "local"` and `"otlpEndpoint": ""` fields Closes the "Align telemetry config and docs semantics for target, useCollector, otlpEndpoint, otlpProtocol, and outfile" checklist item in #3731. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(telemetry): address Copilot review comments on outfile and target descriptions - Fix outfile table row in telemetry.md: "overrides `otlpEndpoint`" → "overrides OTLP export" (outfile disables all OTLP exporting, not just the base endpoint) - Use fully-qualified setting names (`telemetry.otlpEndpoint`, `telemetry.outfile`) in the target description in settings.md for consistency with the rest of the table 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(telemetry): update QWEN_TELEMETRY_TARGET env var description and add outfile note - Align QWEN_TELEMETRY_TARGET env var description with the updated telemetry.target setting semantics (informational label, not routing) - Add a note after the file-based output example clarifying that outfile automatically disables OTLP export 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) |
||
|
|
14512080ed
|
feat(telemetry): add hierarchical session tracing spans (#4071)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(telemetry): add hierarchical session tracing with interaction spans Add OpenTelemetry-based session tracing that creates hierarchical spans for user interactions. Interaction spans wrap the full sendMessageStream lifecycle and are properly ended at all exit points (ok, error, cancelled). - New session-tracing.ts module with concurrent-safe span management - Interaction spans started for UserQuery/Cron/Notification message types - Shutdown safety net ends active interaction span before SDK shutdown - Memory-safe span storage using WeakRef + strongRefs + 30-min TTL cleanup - NOOP_SPAN sentinel avoids leaked spans when SDK is not initialized * fix(telemetry): address review comments on session-tracing - TTL cleanup now sets ctx.ended=true before span.end() to prevent double-end - Interaction spans stored in strongSpans for GC-safe TTL cleanup - cancelled status uses SpanStatusCode.OK per codebase convention - endLLMRequestSpan treats missing metadata as OK, not ERROR - Module-level lastInteractionCtx fallback for shutdown outside ALS context - try/finally safety net in sendMessageStream for uncaught exceptions * fix: apply auto-fixes from /review - Fix next-speaker continuation span leak by overriding options.type to Hook - Add setStatus call to endToolExecutionSpan for consistent span status - Sanitize API error messages in interaction span status --------- Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> |
||
|
|
51ee87539c
|
revert(deps): downgrade ink 7 → 6 to fix Static-remount TUI regression from #3860 (#4083)
Some checks failed
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
SDK Python / Classify PR (push) Has been cancelled
SDK Python / SDK Python (3.10) (push) Has been cancelled
SDK Python / SDK Python (3.11) (push) Has been cancelled
SDK Python / SDK Python (3.12) (push) Has been cancelled
* revert(deps): downgrade ink 7.0.2 → 6.x to fix Static-remount regression from #3860 PR #3860 upgraded ink 6.2.3 → 7.0.2 with the claim of "no business code changes." In production this turns out to break the TUI: - After `/clear`, the next user message and AI response do not render to the static history area — only the dynamic spinner/input area is visible (#3860 + chore/upgrade-ink-7 branch reproduce this). - After Ctrl+O (TOGGLE_COMPACT_MODE), the screen is cleared and stays blank. - Any `refreshStatic()` call path (auth refresh, model change, render- mode switch, /clear, Ctrl+O) puts the UI into the same "muted" state. Root cause is an ink 7 regression: when `<Static>` is remounted by changing its `key` prop, the new instance's items are never written to stdout. A 30-line minimal repro (pure ink + Static + key++) confirms this independently of qwen-code. Closest upstream issue: vadimdemedes/ink#773 (useLayoutEffect-driven child stripping in <Static>). PR #905 ("Fix dangling staticNode reference") merged into ink 7 fixed the unmount-OOM path but not this remount path. No upstream issue yet matches the "remount loses content" case — we should file one and ship a re-upgrade once it is resolved. Scope of this revert (intentional partial revert of #3860): - ink ^7.0.2 → ^6.2.3 (cli + root hoist) - react / react-dom 19.2.4 pin → ^19.1.0 (cli direct, root overrides removed) - wrap-ansi ^10.0.0 → 9.0.2 (cli direct, root override restored) - react-devtools-core kept at ^6.1.5 (still ink-6 compatible — ink 6.8.0's peerOptional requires >=6.1.2; downgrading to 4.x would re-introduce a conflict) - @vitest/eslint-plugin pin "1.3.4" → "^1.3.4" - "@types/node" override removed (was only needed for ink 7's Node 22 type drift) What this revert keeps: - Node engines >=22 across root / cli / core / sdk / web-templates and the matching Dockerfile / .nvmrc / CI matrix work. PR #1876 followed up by adding Node 24 support to the matrix, and rolling those back would conflict with that work. The visible bug is the ink runtime regression, not the engine bump. - doctorChecks.ts MIN_NODE_MAJOR = 22 (matches engines). - The test gating that #3860 added for ink-7 input throttle (AuthDialog / AskUserQuestionDialog / InputPrompt). With ink 6 these tests would pass un-gated, but leaving the gate in place is harmless and a follow-up can un-gate them. Keeping this revert minimal. Verification (local, ink 6.8.0 single instance): - npm ls ink → single ink@6.8.0 - npm ls react → single react@19.2.4 (kept by vscode-ide-companion workspace pin; ink 6 is fine on 19.2) - npm run typecheck --workspace=packages/cli → clean - AppContainer.test.tsx 61/61 pass - MainContent.test.tsx 6/6 pass - clearCommand.test.ts 13/13 pass Re-upgrade path: once ink ships a fix for the Static-remount regression, redo this upgrade behind the feat/virtual-viewport-on-ink7 branch where the `<Static>` + clearTerminal combo is replaced by an overflowY=hidden self-managed viewport. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * ci(fix): keep wrap-ansi 10 + skip 1 ink-7-specific TableRenderer test The initial revert downgraded wrap-ansi to 9.0.2 (the pre-PR-#3860 state). After rebasing onto current main, PR #4050 (preserve table ANSI color across wrapped lines) brought in a new test ("does not preserve foreground after an explicit foreground reset") whose wrap point depends on ink 7's <Text> wrapping behavior. Two-part fix: 1. Restore wrap-ansi to 10 (cli direct dep). The wrap-ansi version is independent of the ink regression we're reverting — wrap-ansi 10 has no peer-dep tie to ink 7 — and #4050's TableRenderer code on main already assumes wrap-ansi 10. Keeping the wrap-ansi bump removes the root override for wrap-ansi (was forcing all transitives to 9.0.2) so cli's TableRenderer gets the wrap-ansi 10 it expects, while ink 6's transitive wrap-ansi naturally resolves to 9 (its own declared range) — no conflict. 2. Skip the one new test that asserts a specific wrap position. The other assertions in that test (foreground cleared, equal visible widths) still pass on ink 6 — only `expectWrappedContinuation` is ink-7-specific. The sibling test 'does not preserve foreground after an explicit reset' (using \\u001b[0m instead of \\u001b[39m) still passes unmodified on ink 6, so the ANSI-handling logic itself is verified end-to-end. The TODO marker references the re-upgrade path. Local verification: - TableRenderer.test.tsx: 54/54 pass + 1 skipped - AppContainer.test.tsx: 61/61 pass - MainContent.test.tsx: 6/6 pass - clearCommand.test.ts: 13/13 pass - npm run typecheck --workspace=packages/cli: clean - npm ls ink → single ink@6.8.0 - npm ls wrap-ansi → cli direct: 10.0.0; ink 6 transitive: 9.0.2 (no conflict, no override) Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
dc7a90c4ac
|
fix(cli): preserve table ANSI color across wrapped lines (#4050)
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> |
||
|
|
e59c7b8b68
|
fix(channels): expand tilde in channel cwd config (#4045)
Channel `cwd` from settings.json was passed verbatim to `child_process.spawn()` in AcpBridge, so `"cwd": "~/xomo"` made the kernel fail to chdir and surfaced as a misleading `spawn /usr/bin/node ENOENT`. Expand `~` / `~\` and resolve relative paths to absolute in `parseChannelConfig` so all downstream consumers (AcpBridge spawn, SessionRouter, WeixinAdapter, ChannelBase) only ever see absolute paths. Reuses the existing `resolvePath` helper from `@qwen-code/channel-base` (previously used internally by `getGlobalQwenDir`). Fixes #3998 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
1936420dcb
|
ci(e2e): stabilize MCP/CLI flows and cancel stale main runs (#4039)
* test(e2e): stabilize MCP tool message flow * ci(e2e): cancel stale main E2E runs * test(e2e): accept paired MCP tool results * test(e2e): stabilize monitor tool check * test(e2e): stabilize run_shell_command file-listing assertion The model consistently picks list_directory over run_shell_command for file-listing prompts. Make the prompt explicit about which tool to use, matching the approach taken for the MCP tool flow test. |
||
|
|
826f9fd126
|
doc[sdk-python] Expand Python SDK usage documentation (#3995)
* docs(sdk-python): expand usage examples Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(docs): correct file_path key and update session resume examples * fix(docs): add is_error handling and async iteration to SDK examples - Session Resume examples now check is_error before printing result, consistent with the print_result helper in Quick Start - Permission Callback examples now wrap query() in async def main() with async for iteration, so the CLI process actually starts 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(sdk-python): address review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
70eecdbdf4
|
Upgrade GitHub Actions for Node 24 compatibility (#1876)
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com> |
||
|
|
8e18e64594
|
feat(dashscope): support DASHSCOPE_PROXY_BASE_URL for prompt cache via API gateway (#3991)
Adds `DASHSCOPE_PROXY_BASE_URL` env var so DashScope prompt-cache headers fire when requests go through an internal API gateway proxy URL. - New constant `DASHSCOPE_PROXY_BASE_URL` in `openaiContentGenerator/constants.ts`, read once at module load from `process.env`. - `isDashScopeProvider()` now also returns true when `baseUrl` matches the configured proxy URL (case-insensitive, trailing slash on either side normalized). - Debug log fires on the configured-but-mismatched path with a static message (no URL interpolation, so embedded credentials in the proxy URL cannot leak to `QWEN_DEBUG_LOG_FILE`). - 9 new test cases covering match, mismatch, debug-log assertion, and trailing-slash normalization. Uses `vi.stubEnv` + `vi.unstubAllEnvs` in `beforeEach`. Closes #3991 |
||
|
|
3d664336df
|
fix(cli): improve rendering on narrow terminals (#3968)
* fix(cli): improve rendering on narrow terminals - TableRenderer: switch to vertical format when contentWidth < 60 cols, preventing wide horizontal tables from overflowing into scrollback on narrow terminals. - Composer: suppress bottom loading indicator when terminal width ≤ 30 cols during streaming, avoiding unnecessary redraws on ultra-narrow terminals. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): cover narrow-terminal rendering branches + tighten thresholds Address review feedback on #3968: - Composer: drop redundant `isStreaming &&` guard from `suppressBottomLoadingIndicator`; the trailing `=== StreamingState.Responding` already implies streaming, and the redundancy risked future drift if `isStreaming` were extended. - Composer.test: add four cases pinning the suppression contract — Responding @ 25/30 cols hides, @ 31 cols shows, and WaitingForConfirmation @ 25 cols still shows so confirmation prompts never disappear on narrow terminals. - TableRenderer: replace the content-agnostic 60-col floor with a column-aware threshold (`max(24, colCount * MIN_COLUMN_WIDTH + borderOverhead + SAFETY_MARGIN)`) so a 2-column table with short values renders horizontally on a ~30-col terminal instead of being forced into vertical mode. The existing `maxLineWidth` post-build check still catches actual overflow. - TableRenderer.test: add explicit horizontal-vs-vertical threshold cases (2 cols @ 60/30/20 and 5 cols @ 30) and bump the alignment tests to contentWidth=60 with `┌` guards so they fail loudly if the threshold ever pushes them back into vertical no-op mode. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): preserve esc-to-cancel on narrow terminals + boundary tests Address review feedback on the narrow-terminal rendering changes: - Composer: when the full LoadingIndicator is suppressed on ≤30-col terminals during Responding, render a minimal "(esc to cancel)" text fallback so users retain the cancel affordance. Suppressing the full indicator still avoids layout breakage, but the affordance now stays visible. - TableRenderer: clarify that `borderOverhead` is reused by the horizontal-vs-vertical layout threshold so a future change to the border-width formula does not silently shift the threshold. - TableRenderer tests: add equality boundary cases at `ABSOLUTE_MIN_HORIZONTAL_TABLE_WIDTH` (24) and at the 5-column column-budget threshold (35), plus one-below cases, so a future `<` → `<=` regression on the strict comparator is caught. - Composer tests: assert the fallback string is rendered when (and only when) the full indicator is suppressed. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): use existing 'Esc to cancel' i18n key for narrow-terminal fallback The |
||
|
|
664208c420
|
feat(core): replace fdir crawler with git ls-files + ripgrep fallback (#3214)
Replaces the fdir filesystem crawler with a three-tier strategy for `@` file mention autocomplete. Strategy: 1. `git ls-files --cached` + `git ls-files --others --exclude-standard` for git repos — reads from the git index, gitignore correctness delegated to git. 2. `ripgrep --files` for non-git directories when `rg` is available. 3. `fdir` as the original last-resort fallback. Additional improvements: - mtime-based change detection on `.git/index` skips re-crawl when nothing changed. - 5s refresh throttling avoids rebuilding the index on every keystroke. - Async chunked indexing yields to the event loop every 1000 entries to stay responsive on 200k+ file trees. - Background untracked-file merge with a 10s timeout. - `resolveGitDir()` correctly handles `.git` as a file (worktrees) including relative `gitdir:` pointers and walks up to parent repos. - POSIX-style path normalization on Windows. - Streaming `spawn()` with line-by-line `onLine` processing and an enforced `maxFiles` budget that terminates the child early, preventing the previous 20MB stdout buffer hazard on large repos. - `--exclude-standard` on the untracked listing so gitignored directories (e.g. `node_modules/`, `dist/`) are not enumerated. Closes #3137 |
||
|
|
55893875b0
|
feat(cli): add tools.toolSearch.enabled setting for prefix-caching models (#4069)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(cli): add tools.toolSearch.enabled setting to disable ToolSearch for prefix-caching models ToolSearch (PR #3589) defers MCP tool loading to reduce prompt size, but breaks prefix-based KV caching for models like DeepSeek V4 where cached token pricing is 1/120 of uncached. Users reported cache hit rates dropping from ~98% to ~81% and 3x cost increases (discussion #4065). Add a `tools.toolSearch.enabled` setting (default: true) that disables ToolSearch by adding tool_search to the deny list, triggering the existing eager-reveal fallback in client.ts. All deferred tools are then included in the initial declaration list, restoring prompt prefix stability. Auto-disable ToolSearch for deepseek-v4-* models when the setting is not explicitly configured, since their extreme cache discount makes prefix stability far more valuable than the ~15K token savings from deferral. Users can override with `tools.toolSearch.enabled: true`. * fix: address PR review — expand model detection, add tests, regenerate schema - Remove ^ anchor from regex to handle provider-prefixed model names (e.g. openrouter/deepseek/deepseek-chat) - Expand auto-detection to all DeepSeek models with prefix caching: deepseek-v3, deepseek-v4-*, deepseek-chat - Add 6 tests covering: explicit disable, auto-detect for v3/v4/chat with provider prefix, non-deepseek skip, explicit enable override - Regenerate settings.schema.json for vscode-ide-companion |
||
|
|
d6fe59a3b5
|
fix(test): repair stale --json-schema integration assertion (#4075)
The "fails fast at CLI parse time on invalid JSON Schema" integration
test stopped exercising the Ajv strict-compile path once the
`--json-schema` root-accepts-object precheck landed. The precheck rejects
`{type: "this-is-not-a-real-type"}` before Ajv runs, so the CLI exits
with the "root must accept object-typed values" error instead of the
"is not a valid JSON Schema" error the test expects.
Move the bogus `type` into a property so the root precheck passes and
Ajv catches the unknown type, restoring the test's original intent.
|
||
|
|
76d8c0ce83
|
chore(core): runtime.json sidecar follow-ups from PR #3714 review (#4030)
- Drop dead `typeof !== 'boolean'` check in the integer-type guard (Number.isInteger(true) is already false; rename to isFiniteInteger). - Drop dead utf-8-message branch in readRuntimeStatus's catch: fs.readFile(p, 'utf-8') returns replacement chars rather than throwing, so that branch never fires; the JSON.parse fallback already handles the truncated-UTF-8 case the test exercises. - Move runtimeStatus.js export from between fileUtils and filesearch into alphabetical position next to runtimeFetchOptions. - Add Config.startNewSession integration tests pinning the runtimeStatusEnabled gate: flag-off leaves sibling sidecars alone, flag-on swaps old->new on the same PID, no-op when sessionId is unchanged. No behavior change for the runtime-status sidecar itself; this just trims dead defensive code and adds the integration coverage that was missing in #3714. |
||
|
|
32a49b4ddb
|
refactor(telemetry): remove dead useCollector setting and unreachable TelemetryTarget.QWEN (#4061)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
useCollector was plumbed through config (interface, constructor, getter, env var resolution) but never consumed by the telemetry SDK — the setting had no runtime effect. TelemetryTarget.QWEN existed in the enum but parseTelemetryTargetValue() only accepted 'local' and 'gcp', making 'qwen' unreachable (it would throw FatalConfigError). Remove both dead code paths along with their tests and documentation. Part of #3731 |
||
|
|
4bba75f765
|
fix(cli): keep long model stats header on one line (#4032)
Some checks failed
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
SDK Python / Classify PR (push) Has been cancelled
SDK Python / SDK Python (3.10) (push) Has been cancelled
SDK Python / SDK Python (3.11) (push) Has been cancelled
SDK Python / SDK Python (3.12) (push) Has been cancelled
* fix(cli): keep long model stats header on one line * test(cli): cover fixed model stats columns |
||
|
|
bdd5b602de
|
feat(core): improve Anthropic proxy compatibility and enable global prompt cache scope (#4020)
* feat(core): improve Anthropic proxy compatibility and enable global prompt cache scope
- Use authToken instead of apiKey to send Authorization: Bearer header,
avoiding dual-header conflicts with IdeaLab-style proxies
- Set User-Agent to claude-cli format and add x-app header for proxy
Team rule compatibility
- Add adaptive thinking support for Claude 4.6+ models
- Enable prompt-caching-scope-2026-01-05 beta header and scope: global
on system prompt cache_control to improve cross-session cache hit rates
* test: update tests for User-Agent, beta header, and cache_control changes
* fix(core): scope anthropic proxy workarounds to non-native baseURLs only
Address review feedback on PR #4020 by narrowing each workaround to where
it actually applies, instead of shipping it globally.
- Gate `Authorization: Bearer` (`authToken`), `claude-cli` User-Agent, and
`x-app: cli` to non-Anthropic-native baseURLs. Direct `api.anthropic.com`
users keep the SDK-default `x-api-key` (`apiKey`) auth and a truthful
`QwenCode` User-Agent so usage isn't misattributed in Anthropic's
logs/quotas, and so a stricter Anthropic backend doesn't 401 on a
`Bearer`-shaped header.
- Gate the `prompt-caching-scope-2026-01-05` beta on `enableCacheControl`.
When the converter isn't attaching `cache_control` to the body the beta
is dead weight and risks 4xx responses from anthropic-compatible
backends that don't recognize it. Restores the `betas.length === 0`
early-return for the all-disabled case.
- Detect adaptive-thinking models with numeric major/minor compare instead
of `[6-9]`. The character class missed `claude-haiku-4-6` entirely and
would silently fall through to `budget_tokens` on `claude-opus-4-10` /
`claude-opus-5-1` once those ship — tripping HTTP 400 with a shape the
server no longer accepts.
- Honor explicit `reasoning.budget_tokens` before the adaptive branch.
Adaptive omits `budget_tokens` from the wire shape, so checking it
second silently dropped a user-supplied escape-hatch budget on Claude
4.6+ models.
- Add `scope: 'global'` on the tool `cache_control` entry so the largest,
slowest-changing prefix actually participates in cross-session caching
under the new beta — the system-only attachment was capturing maybe
half the available hit-rate improvement.
- Replace the misleading `as { type: 'ephemeral' }` cast on the system
block (which erased `scope` from the type while leaving it on the
wire) with a `AnthropicTextBlockParam` type that mirrors the existing
`AnthropicToolParam` widening, so types match the runtime shape.
* fix(core): keep enableCacheControl live in the converter
Follow-up on PR #4020 review: `Config.setModel()` mutates
`enableCacheControl` in place (it's in `MODEL_GENERATION_CONFIG_FIELDS`),
but the converter captured it once at construction. On a hot flip the
generator's per-request `prompt-caching-scope-2026-01-05` beta gate would
sample the new value while the converter still emitted the old body-side
`cache_control` — beta-header and body could disagree.
- Thread the live `contentGeneratorConfig.enableCacheControl` into the
converter via a per-call options override on both
`convertGeminiRequestToAnthropic` and `convertGeminiToolsToAnthropic`,
falling back to the constructor-time default when the caller doesn't
pass one. The generator samples the value once per `buildRequest` and
forwards it to both convert calls so the body and beta header always
agree within a request, even across `setModel` flips.
- Regression test: hot-flip `enableCacheControl` from `true` to `false`
on a live generator, verify the 2nd request drops both the beta header
AND the body-side `cache_control` in lockstep.
- Tighten two existing beta-header tests that used `toContain` only on
`interleaved-thinking` / `effort` — they now also assert
`prompt-caching-scope-2026-01-05` is present (per-request keep-default
and streaming paths), so accidental removal trips the test.
- Add coverage for the two previously-uncovered branches of
`isAnthropicNativeBaseUrl`: `*.anthropic.com` subdomains
(Anthropic-native) and a malformed baseURL (URL parse failure → proxy
fallthrough). Also add an `anthropic.com.evil.com` hostname-spoof case
mirroring the existing DeepSeek spoof test.
* refactor(core): consolidate anthropic generator shapes & document edge cases
Follow-up on PR #4020 review:
- Extract `AnthropicThinkingParam` type alias. The thinking union
`{ type: 'enabled'; budget_tokens } | { type: 'adaptive' }` was repeated
verbatim in three places: the `MessageCreateParamsWithThinking` field,
the streaming-request intersection, and `buildThinkingConfig`'s return
type. Once a third shape ships, forgetting one site would silently
narrow a runtime value — single alias keeps them locked.
- Compute `useProxyIdentity` once in the constructor and pass it into
`buildHeaders`. Previously `useBearerAuth` and `useProxyIdentity` named
the same predicate at two call sites; collapsing them clarifies that
Bearer auth + `claude-cli` UA + `x-app: cli` are one bundle that
should never be split.
- Document that `modelSupportsAdaptiveThinking`'s regex is intentionally
unanchored so reseller-prefixed names (`bedrock/claude-opus-4-7`,
`vertex_ai/claude-sonnet-4-6@…`, `idealab:claude-opus-4-6`, …) keep
matching. Tightening to `^claude-` would silently regress those.
- Soften the `prompt-caching-scope` beta comment so it describes what
the code enforces (gate on the `enableCacheControl` flag) rather than
promising a stronger "only ship when cache_control is on the body"
invariant — the converter still skips `cache_control` on niche shapes
(e.g. no system text, no tools, last user block isn't text). The
looser gate is intentional; Anthropic-native ignores unused betas.
- Pin the wire shape for the `reasoning: undefined` + 4.6+ model corner.
`resolveEffectiveEffort` returns undefined on `reasoning === undefined`,
so `buildThinkingConfig` ships `{ type: 'adaptive' }` with no
`output_config` and no `effort-2025-11-24` beta. If Anthropic ever
starts requiring `output_config.effort` alongside adaptive, this test
will fail at CI rather than at runtime as a server 400.
* fix(core): gate cache-scope on Anthropic-native baseURL, mirror auth gate
Follow-up on PR #4020 review: the `prompt-caching-scope-2026-01-05` beta
header and the body-side `scope: 'global'` field together comprise an
Anthropic-only wire-shape extension. Shipping them to non-Anthropic
backends (DeepSeek, IdeaLab) leaned on "unknown betas are ignored" —
true on Anthropic-native, but unverified for proxies and silently
inconsistent with the auth/identity gate, which already uses
`isAnthropicNativeBaseUrl` to bind Bearer / claude-cli / x-app to the
proxy path only.
- Add `useGlobalCacheScope` predicate on the generator. True iff
`enableCacheControl !== false` AND the resolved baseURL is
Anthropic-native. Plumbed per-request into both
`convertGeminiRequestToAnthropic` and `convertGeminiToolsToAnthropic`;
the same predicate also gates the `prompt-caching-scope-2026-01-05`
beta in `buildPerRequestHeaders` so beta + scope field always travel
together.
- Converter emits `cache_control: { type: 'ephemeral' }` (per-session)
when scope is off and `{ type: 'ephemeral', scope: 'global' }` when on.
Non-Anthropic baseURLs go back to their pre-PR per-session caching
shape; existing prompt caching keeps working with no new beta.
- Document the intentional `scope: 'global'` omission on
`addCacheControlToMessages`. The last user message changes every
turn (live prompt + immediate tool_results), so cross-session reuse
has effectively zero hit rate; cross-session caching is concentrated
on system + tool prefixes only.
Tests:
- DeepSeek baseURL pins the proxy auth/identity path
(`authToken` / claude-cli UA / `x-app: cli`). Documents the
contract assumption that DeepSeek's anthropic-compatible endpoint
accepts `Authorization: Bearer` — any future deviation surfaces
here rather than at runtime for users.
- Non-Anthropic baseURL strips the cache-scope beta AND
`scope: 'global'` from the wire shape, while keeping per-session
`cache_control: { type: 'ephemeral' }` on system / tools.
- Hot-flip test extended to assert tool `cache_control` flips alongside
system / user / beta header.
- Converter-level tests for per-call `enableCacheControl` and
`useGlobalCacheScope` overrides — both directions of the constructor
default (true→false, false→true) and the scope-independent-of-source
case (cache on, scope off → per-session shape).
- baseConfig in the per-request anthropic-beta block now targets
`api.anthropic.com` so cache-scope assertions remain meaningful; the
proxy-baseURL behavior is covered separately.
* docs(core): tighten useGlobalCacheScope JSDoc — baseUrl is NOT hot-mutated
#4020 review: the JSDoc claimed `Config.setModel()` mutates both
`enableCacheControl` AND `baseUrl` in place. Per the current Config
implementation, only the qwen-oauth hot-update path mutates
`enableCacheControl` in place; non-qwen-oauth providers go through
the refresh path which recreates the ContentGenerator (so `baseUrl`
is captured fresh at construct time, not mutated).
Tightened the wording to reflect actual behavior + kept the
read-both-each-request defense (cheap and avoids stale-cache
surprises if the hot-update list ever expands).
* fix(core)!: suppress env back-fill so proxy auth doesn't leak real Anthropic key
#4020 review (tanzhenxin, severity high): the IdeaLab-proxy branch
spread `{ authToken: <key> }` and omitted `apiKey` entirely. The
Anthropic SDK constructor destructures with defaults
(`apiKey = readEnv('ANTHROPIC_API_KEY') ?? null`), and destructuring
defaults only fire for `undefined` — so an omitted `apiKey` lets
`ANTHROPIC_API_KEY` back-fill it. The SDK's auth resolver then prefers
`apiKey` over `authToken`, shipping `X-Api-Key` (not
`Authorization: Bearer`) on the wire. Concrete impact: a user with
`ANTHROPIC_API_KEY=sk-ant-…` exported (normal for anyone also running
Claude Code in the same shell) configuring qwen-code with an IdeaLab
proxy plus an IdeaLab token would leak their real Anthropic key as
`X-Api-Key` to the third-party proxy endpoint.
- Pass `apiKey: null` explicitly on the proxy branch and `authToken: null`
on the Anthropic-native branch. Explicit `null` suppresses the
destructuring default; the env back-fill no longer fires.
- New helper `resolveEffectiveBaseUrl` mirrors the SDK's own
destructuring order (config → `ANTHROPIC_BASE_URL` env → SDK default).
`isAnthropicNativeBaseUrl` now consults the env too, so a user
configuring the proxy purely through `ANTHROPIC_BASE_URL` (qwen-code
`baseUrl` unset) gets the proxy identity bundle instead of silently
shipping native auth + UA + cache-scope beta to the proxy.
Tests:
- ANTHROPIC_API_KEY env + proxy baseURL → ctor receives `apiKey: null`
and `authToken: our-key`. Locks in the credential-leak fix.
- ANTHROPIC_AUTH_TOKEN env + Anthropic-native baseURL → ctor receives
`authToken: null` and `apiKey: our-key`. Symmetric guard for the
inverse direction.
- ANTHROPIC_BASE_URL env points to proxy, config.baseUrl unset → proxy
identity bundle (claude-cli UA, x-app, Bearer auth) applies.
- ANTHROPIC_BASE_URL unset → SDK default api.anthropic.com path keeps
native identity (predicate doesn't misclassify the SDK default as a
proxy).
- config.baseUrl wins over ANTHROPIC_BASE_URL — mirrors the SDK's own
resolution order.
- Existing 7 identity tests updated from `toBeUndefined()` to
`toBeNull()` to match the new explicit-suppression contract.
* refactor(core): gate cache-scope beta on body presence, not just predicate
#4020 review (Copilot): the comment promised "beta and body-side
scope: 'global' field always ship together" but the gate was just
`useGlobalCacheScope()`. In the degenerate case where the predicate is
true but the request body has no system text AND no tools, the beta
would still ship without any matching `cache_control.scope: 'global'`
on the wire — overstating the contract and shipping dead weight.
- New `hasGlobalCacheScopeOnWire(req)` scans the assembled request body
(system block when shaped as `TextBlockParam[]`, tools array) for any
`cache_control: { …, scope: 'global' }` entry. `buildPerRequestHeaders`
gates the `prompt-caching-scope-2026-01-05` beta on this scan, so the
beta and the body field share a single source of truth. No window
between sampling the predicate and emitting the body where the two
could diverge.
- `useGlobalCacheScope()` is still sampled once per `buildRequest` and
threaded into the converter to decide whether to ATTACH
`scope: 'global'` to the body. The body-scan downstream then derives
the beta from what actually landed.
Tests:
- New: empty systemInstruction + no tools + Anthropic-native + cache on
→ beta NOT shipped (degenerate body-scan case).
- New: empty systemInstruction + non-empty tools → beta shipped
(tool scope:'global' triggers the scan).
- Existing per-request beta tests now include a `systemInstruction` so
the body has the scope field; degenerate case is covered by the new
dedicated test.
Also tightened two stale comments (#3217834451, #3217834505) that
claimed `Config.setModel()` mutates both `enableCacheControl` and
`baseUrl` in place — only `enableCacheControl` is hot-mutated (qwen-oauth
path); non-qwen-oauth providers recreate the generator on refresh, so
`baseUrl` is captured fresh at construct time. Comments now describe the
real in-place mutation and note the qwen-oauth boundary.
* fix(core): trim config.baseUrl and document x-app in buildHeaders
#4020 review (Copilot): two low-stakes follow-ups on
|
||
|
|
8beb97eecf
|
feat(tools): keep ask_user_question always-visible to surface clarification UX (#4041)
Previously ask_user_question was deferred behind ToolSearch (shouldDefer: true), so the model only saw it as a name in the deferred-tools section and had to discover it via tool_search before invoking. In practice the model rarely went through that discovery step and instead asked clarifying questions in plain prose, losing the structured multiple-choice UX the tool exists to provide. Flip shouldDefer to false so the schema lives in the initial tool list. Add a unit-test guard so a future refactor doesn't silently put it back behind ToolSearch. |
||
|
|
d7a25682e6
|
refactor(core): route side-query LLM calls through runSideQuery chokepoint (#3775)
* refactor(core): route side-query LLM calls through runSideQuery chokepoint
Folds every one-shot side-query call site through a single `runSideQuery`
entry point with `thinkingConfig.includeThoughts: false` and `fastModel`
(falling back to main) as the default policy. Adds a text-mode sibling
to the existing JSON-mode helper, plus a `BaseLlmClient.generateText`
primitive that calls `ContentGenerator.generateContent` directly so
side queries get neither user-memory wrapping nor the main-prompt
fallback that `geminiClient.generateContent` applies.
Migrated call sites: session title, recap, tool-use summary, /rename,
follow-up suggestion (direct path), ACP rewrite, project /summary,
arena approach summary, chat compression, web-fetch, insight analysis,
subagent spec generation. Six call sites override the helper defaults
explicitly (subagent gen, suggestion, ACP rewrite, /summary, compression,
insight) where main-model quality or caller-supplied model matters.
The /summary path additionally fixes a latent bug: text extraction
previously did not strip thought parts, so on thinking models the
saved `.qwen/PROJECT_SUMMARY.md` could leak `reasoning_content` into
the file. The chokepoint now strips thought parts and the request
itself goes out with thinking off.
Best-effort cosmetic callers (recap, tool-use summary, kebab rename,
suggestion) opt into `maxAttempts: 1` so transient outages don't burn
seven retries on output the user will likely never see. `isInternalPromptId`
recognises the `side-query:` prefix automatically so new call sites are
filtered without per-site allowlist updates.
Removes the `getAgentContentGenerator` workaround in `InProcessBackend`
and the `getAgentSummaryGenerator` indirection in `ArenaManager` —
arena approach summaries now run through the chokepoint against
`fastModel`, giving every agent a neutral arbiter rather than a
self-summary on its own model.
* fix(core): guard isInternalPromptId against undefined prompt_id
logToolCall calls isInternalPromptId(event.prompt_id), and tool-call
events from useToolScheduler can carry an undefined prompt_id. The
side-query refactor added promptId.startsWith(SIDE_QUERY_PROMPT_PREFIX)
without a falsy guard, so the missing id crashed the logger and broke
six useToolScheduler tests across all OS / Node matrix entries on CI.
* fix(cli,core): polish runSideQuery callers from review feedback
- Cap web-fetch, chat-compression, and ACP rewrite at maxAttempts: 1.
These paths degrade gracefully on failure (tool error, NOOP fallback,
null return), so 7 retries only delays the user-visible outcome.
- /summary now carries the main session's system instruction so the
summarizer keeps the coding-assistant role, project context, and
user memory instead of summarizing the chat in isolation.
- Add isInternalPromptId tests for the side-query: prefix so future
callers minted via runSideQuery stay filtered out of recordings.
* refactor(core): document runSideQuery defaults and surface promptId in errors
- Add JSDoc on the model and config fields of SideQueryJsonOptions and
SideQueryTextOptions so the fastModel-first defaulting and the
thinkingConfig.includeThoughts: false default are visible at the API
surface, not buried in resolveDefaultModel / applyThinkingDefault.
- BaseLlmClient.generate{Json,Text} error wraps now include promptId
in the message and pass { cause: error }, so a side-query failure
identifies which call site failed and preserves the original stack.
- Add tests covering maxAttempts forwarding (present + omitted) and
rejection propagation for both JSON and text modes — the conditional
spread is non-trivial and was previously unverified.
* fix(core): preserve per-model provider routing in side queries
BaseLlmClient was bound to the main session's ContentGenerator and only
swapped the request `model` field, so side queries targeting a fast or
alternate model inherited the main provider's baseUrl, credentials, and
sampling settings — breaking cross-provider configurations.
Move per-model generator/authType resolution out of GeminiClient and into
BaseLlmClient as `resolveForModel`. Both generateJson and generateText
now build a per-model ContentGenerator (with cache) when the request
targets a non-main model and pass the resolved retry authType through
to retryWithBackoff. GeminiClient.generateContent delegates to the same
resolver so there is a single source of truth.
Also pin the /forget destructive selector to the main model — the
runSideQuery default moved to fast model in this branch, but /forget
acts on the selection without confirmation, so a weaker fast model
could silently delete the wrong managed-memory entries.
* test(core): assert thinkingConfig/maxAttempts/model forwarding in compression
The compression caller of runSideQuery sets thinkingConfig.includeThoughts=true
and maxAttempts=1. A future refactor that silently drops either would degrade
compression quality without test failure; this assertion locks the contract.
* fix(cli): route dynamic localization through side query
* refactor(core): remove unused memory governance review
|
||
|
|
28a3439307
|
feat(skills): Add codegraph skill for PR review risk analysis and conflict detection (#3910)
* feat(skills): add codegraph skill for PR review risk analysis and conflict detection Add .qwen/skills/codegraph/ with PR analysis, bug analysis, schema, patterns, and eval support. Enables per-PR risk scoring (blast radius, interface changes, test coverage), cross-PR conflict detection, and automated GitHub labeling via the codegraph-ai pip package. * chore: ignore .venv and .codegraph directories Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(skills): add HF mirror and ModelScope fallback for embedding model downloads Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(skills): address PR #3910 review feedback for codegraph skill - Rename skill from codegraph-qa to codegraph for consistency - Broaden description to cover index creation and inspection - Add HuggingFace/ModelScope model download tips - Document codegraph ingest command for adding git history - Fix stray '@' in stdout Tee pattern code snippet - Remove dead reference to PRReview.md - Add repo_dir parameter to CrossPRAnalyzer examples - Update CHANGES edge types (hunk/deleted/related/new) and resolve_pr_functions internals - Add Conflict Detection Dimensions table - Replace codegraph query with PRReview Python API examples - Remove OPTIONAL MATCH limitation from schema (now supported) - Update codegraph-ai link to PyPI - Add HF model download entry to Troubleshooting table --------- Co-authored-by: pomelo-nwu <czynwu@outlook.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
cadda23782
|
chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860)
* chore(deps): upgrade ink 6.2.3 -> 7.0.2 + bump Node engine to 22
ink 7 requires Node >=22 and react-reconciler 0.33 with React >=19.2,
so this PR also bumps:
- Node engines (root + cli + core) 20 -> 22
- React/react-dom 19.1 -> 19.2.4 (pinned exact via overrides to keep
the transitive React graph deduped to a single instance)
- @types/node pinned to 20.19.1 via overrides to avoid an unrelated
Dirent NonSharedBuffer regression in sessionService tests
- @vitest/eslint-plugin pinned to 1.3.4 to avoid an unrelated lint
regression introduced by the 1.6.x rule additions
- react-devtools-core 4.28 -> 6.1 (ink 7 peerOptional requires >=6.1.2)
- ink hoisted to root devDeps so workspace-private peer-dep contention
doesn't push ink-link/spinner/gradient into nested workspace
installs (which would skip transitive resolution for terminal-link)
Workflow + image + installer alignment:
- .nvmrc 20 -> 22
- Dockerfile node:20-slim -> node:22-slim
- CI test matrix drops 20.x (keeps 22.x + 24.x)
- terminal-bench workflow Node 20 -> 22
- Linux/Windows install scripts upgrade their Node version targets
Documentation alignment:
- README.md badge + prerequisites
- AGENTS.md, CONTRIBUTING.md, docs/users/quickstart.md,
docs/users/configuration/settings.md, docs/developers/contributing.md,
docs/developers/sdk-typescript.md, docs/users/extension/extension-releasing.md,
packages/sdk-typescript/README.md, packages/zed-extension/README.md,
scripts/installation/INSTALLATION_GUIDE.md
Test gating:
- Two AuthDialog/AskUserQuestionDialog tests that drive <SelectInput>
through ink-testing-library now race ink 7's frame-throttled input
delivery and land on the wrong option. The maintainers had already
marked one of them unreliable (skip on Win32 + CI+Node20). Extend
that gate to cover all environments until upstream
ink-testing-library ships an ink-7-compatible release that flushes
input deterministically. The other test now uses it.skip with the
same comment. No business code changes.
Verified locally:
- npm run typecheck across all workspaces: clean
- npm run lint (root): clean
- npm run test --workspaces:
cli 312/312 files, 4918 passed, 9 skipped
core 266/266 files, 6836 passed, 3 skipped
webui 6/6, 201 passed
sdk 40/40, 283 passed, 1 skipped
- npm ls ink: single ink@7.0.2 instance across all peer deps
- single react@19.2.4 instance
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* chore: align Node 22 floor across all shipping artifacts
Reviewer (tanzhenxin) flagged five surfaces where the >=22 engine bump
leaked: SDK package metadata, web-templates engines, /doctor runtime
check, main bundler target, and SDK bundler target. Each was a separate
escape hatch letting Node 18/20 consumers install or run the artifact
on an unsupported runtime.
- packages/sdk-typescript/package.json: engines.node >=18.0.0 -> >=22.0.0
- packages/web-templates/package.json: engines.node >=20 -> >=22
- packages/cli/src/utils/doctorChecks.ts: MIN_NODE_MAJOR 20 -> 22
- esbuild.config.js: target node20 -> node22 (main CLI bundle)
- packages/sdk-typescript/scripts/build.js: target node18 -> node22 (esm + cjs)
- packages/cli/src/utils/doctorChecks.test.ts: rename test label to v22+
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* ci(e2e): bump E2E workflow Node matrix to 22.x
Reviewer (tanzhenxin) flagged that e2e.yml still pinned node-version
20.x while root engines is now >=22, so every E2E run on push would
either fail at npm ci with engine error or silently exercise the bundle
on a runtime that's no longer in ci.yml's test matrix.
The macOS job in the same workflow already reads .nvmrc (which is 22)
so this only updates the Linux matrix.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(deps): drop root wrap-ansi override so ink 7 gets its declared dep
Reviewer (tanzhenxin) flagged that the root overrides.wrap-ansi: 9.0.2
predates this upgrade and forces every consumer (including ink) to v9,
while ink 7 declares wrap-ansi: ^10.0.0. The lockfile had no nested
install under node_modules/ink/, so ink 7 was running with a transitive
dep one major below its declared minimum.
Dropping the global override lets ink resolve its own wrap-ansi 10
nested install (now visible in the lockfile under
node_modules/ink/node_modules/wrap-ansi), while the cli package's own
direct `wrap-ansi: 9.0.2` dependency keeps the cli code path
(TableRenderer.tsx) on the version it has been tested against. The
nested cliui override is preserved for yargs which still needs v7.
Verified via `npm ls wrap-ansi`:
- ink@7.0.2 -> wrap-ansi@10.0.0 (newly nested)
- @qwen-code/qwen-code -> wrap-ansi@9.0.2 (unchanged)
- yargs/cliui -> wrap-ansi@7.0.0 (unchanged)
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(InputPrompt): un-skip placeholder ID reuse after deletion
Reviewer (tanzhenxin) flagged that the new it.skip on the
'should reuse placeholder ID after deletion' test was undisclosed in
the PR description and removed coverage of real product behavior
(freePlaceholderId / bracketed-paste backspace path) without a
TODO(#NNNN) link.
Their argument was sound: the skip rationale pointed at ink 7's input
throttle, but this same file just bumped the wait helper from 50ms to
150ms specifically to give ink 7 frame time. Re-running the test under
the bumped wait shows it passes reliably (5/5 runs in the full-file
context, 9/10 alone), so the skip was masking the throttle-flake that
the wait bump already addresses, not a real product bug.
Drop the it.skip and the now-stale comment so coverage of the
freePlaceholderId reuse logic is restored.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(InputPrompt): bump first prompt-suggestion test wait to 350ms
The "accepts and submits the prompt suggestion on Enter when the buffer
is empty" test is the first in its describe block, so it pays the
renderer cold-start cost. On macOS-22.x CI runners that pushes the
Enter → onSubmit microtask past the default 150ms post-Enter wait. Match
the 350ms initial render wait used immediately above to absorb the cold
start.
* Revert "test(InputPrompt): bump first prompt-suggestion test wait to 350ms"
This reverts commit
|
||
|
|
7e11428545
|
refactor(cli): remove legacy qwen auth CLI subcommand, redirect to /auth TUI dialog (#3959)
The `qwen auth` CLI subcommand (with subcommands like qwen-oauth, coding-plan, api-key, openrouter, status) has been superseded by the richer /auth TUI dialog introduced in the provider-first auth registry (#3864). Running `qwen auth` now prints a deprecation notice pointing users to the /auth TUI dialog (interactive), env vars (CI/headless), or /doctor (status check). Changes: - Replace auth.ts with a stub that prints a removal notice and exits - Delete handler.ts (734 lines), interactiveSelector.ts, and their tests (interactiveSelector.test.ts, openrouter.test.ts, status.test.ts) - Update /auth slash command to handle non-interactive/ACP modes gracefully - Enrich /doctor auth check with provider-aware diagnostics using findProviderByCredentials - Mark `auth` as a subcommand that handles its own exit in config.ts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
84ecb5b8a3
|
feat(cli): add --json-schema for structured output in headless mode (#3598)
* feat(cli): add --json-schema for structured output in headless mode
Registers a synthetic `structured_output` tool whose parameter schema IS the
user-supplied JSON Schema. In headless mode (`qwen -p`), the first successful
call terminates the session and exposes the validated payload via the result
message's `structured_result` field. Invalid schemas are rejected at CLI parse
time via a new strict Ajv compile helper so they can't silently no-op at
runtime.
* fix(cli): honour "first structured_output call ends session" + reject non-object root schemas
Two review fixes for the `--json-schema` feature:
1. `runNonInteractive` now breaks out of the tool-call loop as soon as the
first successful `structured_output` invocation is captured, rather than
continuing to execute any trailing tool calls the model emitted in the
same turn. This restores the documented single-shot contract and prevents
side-effecting tools from running after the final answer has already
been accepted.
2. `resolveJsonSchemaArg` rejects schemas whose root `type` is anything
other than "object" (or a type array including "object"). Function-
calling APIs require tool arguments to be JSON objects, so a schema
like `{"type": "array"}` would have registered an unusable synthetic
tool the model could never satisfy. Absent `type` and `type: "object"`
remain accepted.
Adds tests for both paths and updates the existing Ajv-compile test to
exercise that path without tripping the new root-type guard first.
* fix(cli): also reject root anyOf/oneOf schemas whose branches can't accept objects
Addresses a review follow-up: the previous root-object check only inspected
the top-level `type` keyword, so a schema like
`{"anyOf":[{"type":"array"},{"type":"string"}]}` slipped through even though
none of its branches can ever validate the object-shaped arguments that
function-calling APIs send.
Replace the single `type` check with `schemaRootAcceptsObject`, which
recursively walks root-level anyOf/oneOf branches and requires at least one
to accept objects. Absent `type`, `type: "object"`, `type: ["object", ...]`,
and mixed anyOf branches where one accepts object all still pass. `allOf`
is left to Ajv's runtime behaviour — guessing intent across contradictory
allOf branches at parse time is fragile.
* fix(cli): propagate exitCode from --json-schema failure path + tests
Address two PR-3598 review findings:
1. gemini.tsx unconditionally called process.exit(0) after
runNonInteractive/runNonInteractiveStreamJson, clobbering the
process.exitCode = 1 set by nonInteractiveCli.ts when the model
emits plain text instead of the structured_output tool. Switch
both call sites to process.exit(process.exitCode ?? 0) so CI can
detect the failure via the exit code.
2. nonInteractiveCli.test.ts: strengthen the structured-output
success path to assert registry.abortAll() is called and that the
stdout result envelope carries the JSON-stringified args under
`result` plus the raw object under `structured_result`. Add a
retry-path test that mocks executeToolCall to return an error on
the first structured_output call, then verifies sendMessageStream
is called a second time so the model can retry rather than the
session terminating early.
* fix(cli): suppress non-structured tool calls when structured_output is in the same turn
When --json-schema is active and the model emits a batch like
[write_file(...), structured_output(...)], the previous implementation
ran the leading side-effecting tool before accepting the structured
result, violating the "structured_output is the terminal contract"
guarantee. The trailing-only break also let an invalid first
structured_output fall through to subsequent tools before the retry
turn.
Pre-scan the batch: if a structured_output request is present, execute
ONLY the first one and skip everything else (leading and trailing).
This is consistent with the existing terminal-path semantics — the
suppressed tool_use blocks lack a matching tool_result, the same way
max-turns / cancellation leave the stream.
Adds a test covering the reverse-order [side_effect, structured_output]
case alongside the existing trailing-suppression and retry tests.
* fix(cli): tighten --json-schema root validation per review feedback
Three small holes flagged in the latest pass:
1. `schemaRootAcceptsObject` returned early when a root `type` keyword
was present, ignoring sibling `anyOf`/`oneOf`. JSON Schema applies
keywords at the same level conjunctively, so e.g.
`{type:"object", anyOf:[{type:"string"}]}` is unsatisfiable for any
value but used to pass. Now both `type` AND any sibling
`anyOf`/`oneOf` must independently admit object.
2. The FatalConfigError text said "Every branch of a root anyOf/oneOf
must be satisfiable by an object", but the actual logic only
requires *at least one* branch (and tests still accept
`anyOf:[object, string]`). Reworded to "at least one branch" so the
message matches the behaviour.
3. `compileStrict` used `typeof schema !== 'object'` to gate input,
which lets arrays through (`typeof [] === 'object'`). The contract
says "schema must be a JSON object", so add an `Array.isArray`
check so array input gets the intended error rather than a less
helpful Ajv compile message.
Tests cover the new rejection paths and the array case.
* fix(cli): handle root $ref and allOf in --json-schema accept-object check
`schemaRootAcceptsObject` previously only inspected `type`, `anyOf`,
and `oneOf` at the root, so a couple of unsatisfiable shapes still
slipped through:
1. `{"$ref":"#/$defs/Foo","$defs":{"Foo":{"type":"array"}}}` would be
accepted because we don't follow $refs, but registers a synthetic
tool whose params resolve to "array" — the model can never produce
a valid object. Now reject any root $ref unless the user adds a
sibling `type:"object"` as an explicit anchor.
2. `allOf` was deferred to Ajv runtime, but allOf is conjunctive at
the same level as `type` / `anyOf` / `oneOf`, so an entry like
`{"allOf":[{"type":"object"},{"type":"string"}]}` is unsatisfiable
for any value. Walk it like the others, requiring every branch to
admit object.
Tests cover the new $ref-rejected / $ref+anchor-accepted paths and the
allOf reject/accept paths.
* fix(cli): explicit exit code from runNonInteractive + pair suppressed tool calls
Three review threads on the structured-output flow:
1. The break that ends the for-loop on a successful structured_output
call sat *before* the responseParts.push and modelOverride capture.
SyntheticOutputTool currently returns neither, so it was safe today
— but anyone wiring extra signals into the synthetic tool later
would see them silently dropped. Move the break after both captures
so the contract is explicit, not implicit.
2. The failure path used to set process.exitCode = 1 and return void,
relying on global mutable state across an async boundary. Any
cleanup task between runNonInteractive and process.exit could
silently turn the structured-output failure into exit 0. Switch
runNonInteractive to Promise<number>, return 0 / 1 directly from
each function-level exit, and have gemini.tsx use the captured
return value.
3. The pre-scan from the prior commit suppresses sibling tool calls
when structured_output is in the same turn. On the retry path —
when structured_output fails validation — the next-turn payload
has tool_result for structured_output but no entry for the
suppressed siblings, leaving the prior assistant turn's tool_use
blocks unpaired. Anthropic and OpenAI both reject that batch
shape, so the retry would surface as an opaque provider error.
Synthesize a "skipped" functionResponse for every suppressed call
so every tool_use in the prior assistant message has a matching
tool_result.
Tests cover the new retry-pairing contract and update the existing
plain-text-failure test to assert on the return value rather than
process.exitCode.
* fix: address Copilot follow-up review on --json-schema scaffolding
Five small but real findings flagged on the latest pass:
1. core/src/index.ts re-exported `SyntheticOutputTool` via `export type`,
but it's a runtime class — that erased it from the emitted JS and
would break value imports. Split into a value `export { ... }` and a
`export type { StructuredOutputParams }`.
2. The structured-output success path returned without flushing
`localQueue` notifications or finalising one-shot monitors. If a
background task had already emitted `task_started`, exiting here
could drop its paired `task_notification` and leave SDK consumers
with unpaired lifecycle events. Mirror the regular terminal path's
`flushQueuedNotificationsToSdk` + `finalizeOneShotMonitors` calls
before `emitResult`.
3. `schemaRootAcceptsObject` ignored the `not` keyword, so
`{not:{type:"object"}}` (which forbids every object value) slipped
through. Add a best-effort `not` check that rejects when
`not.type` directly excludes object. Deeper negated patterns still
fall through to Ajv at runtime.
4. `compileStrict`'s JSDoc claimed it errored on "Ajv versions we can't
support", but the function doesn't actually check Ajv versions. Reword
to "malformed or uses unsupported draft/features for our Ajv
configuration" so the contract matches the implementation.
5. The pre-scan suppressed sibling tool calls but only synthesised
tool_result events for them on the retry path — the success path
left those tool_use blocks unpaired in the emitted JSONL/stream-json
event log. Move the synthesis after the for loop so it runs for both
the success break and the validation-failure fall-through; the event
log is now consistent regardless of which path the run takes.
Tests cover the new \`not\`-rejection paths, the success-path tool_result
synthesis, and the existing retry-pairing test still passes against the
restructured emit ordering.
* fix(cli): tighten --json-schema parse-time gate per Copilot review
Two more shapes that used to slip through:
1. `schemaRootAcceptsObject` defaulted to true when no narrowing
keyword was present, so root-value constraints like `{const: 1}`
or `{enum: [1, 2]}` registered an unsatisfiable structured_output
contract — the model could never produce a value matching the
tool's parameter schema, and the run would loop on validation
failures until max-turns. Reject `const` whose literal isn't an
object, and `enum` whose members include no object.
2. The yargs check rejected `--json-schema` with `-i` and with no
prompt, but not with `--input-format stream-json`. Stream-json
keeps the process open waiting for protocol messages, so
"terminate on the first valid structured_output call" silently
drops everything queued after that point. Refuse the combination
at parse time so the contradiction surfaces immediately.
Tests cover the new const/enum reject and accept paths.
* fix(cli): handle empty/boolean subschemas + allow stdin-only prompt
Three more shapes flagged on the latest review pass:
1. `schemaRootAcceptsObject` treated an empty root `anyOf`/`oneOf`
as "no constraint" (skipped when length === 0), but per JSON
Schema an empty union is unsatisfiable — no value can match a
member of the empty set. Reject those at parse time so users
get a clear parse error instead of an opaque runtime
never-validates loop.
2. JSON Schema (draft-06+) allows boolean subschemas anywhere a
schema is accepted: `true` matches every value, `false` matches
nothing. The `anyOf`/`oneOf`/`allOf` walks were rejecting
booleans via the typeof-object guard, which incorrectly
rejected `{anyOf:[true]}` and `{allOf:[true,{type:"object"}]}`
while letting `{anyOf:[false]}` slip through. Replace the
per-branch object guard with a `variantAcceptsObject` helper
that treats `true` as accepting and `false` as rejecting, then
recurses on object subschemas.
3. The yargs `.check` rejected `--json-schema` when no `-p` /
positional prompt was given, but the headless CLI also reads
the prompt from stdin (`cat prompt.txt | qwen --json-schema
'...'`) — a legit usage pattern that was being blocked. Drop
the parse-time no-prompt rejection; the existing runtime "No
input provided via stdin..." error in gemini.tsx still catches
genuinely empty input.
Tests cover the empty-union, all-`false`, mixed-boolean accept,
and `false`-in-allOf reject paths. Live-verified against the
bundled CLI: `echo "..." | qwen --json-schema '...'` now reaches
the model call, and the four schema edge cases all surface the
expected error text or proceed past parse time.
* docs(core): note SyntheticOutputTool as the value-export exception
The block comment above the lazy-load type re-exports said tool classes
"are now lazy-loaded and are not exported as values from the package
root", but `SyntheticOutputTool` was just promoted to a runtime export
in
|
||
|
|
e2f7661ef1
|
feat(cli): Ctrl+B promote keybind (#3831 PR-3 of 3) (#3969)
* feat(cli): Ctrl+B promote keybind — wire UI to PR-2's promoteAbortController (#3831 PR-3 of 3) Final piece of the foreground → background promote feature. PR-1 (#3842) landed the `signal.reason` foundation; PR-2 (#3894) wired `shell.ts` to detect a `{ kind: 'background' }` abort, snapshot output, register a `BackgroundShellEntry`, and stash the promote `AbortController` on `TrackedExecutingToolCall`. This PR exposes the user-visible surface: pressing Ctrl+B during an in-flight foreground shell command transfers ownership to a background task the user can inspect via `/tasks` or stop via `task_stop`. ## Changes - `keyBindings.ts`: new `Command.PROMOTE_SHELL_TO_BACKGROUND` bound to `Ctrl+B`. JSDoc explains the no-shell-running no-op semantics. - `useReactToolScheduler.ts`: project `promoteAbortController` from the core's `ExecutingToolCall` through `TrackedExecutingToolCall` so the React layer (AppContainer keypress handler) can find it by callId without re-plumbing through the scheduler. - `AppContainer.tsx`: `handleGlobalKeypress` gains a `PROMOTE_SHELL_TO_BACKGROUND` branch that walks `pendingToolCallsRef.current` (the ref, not the destructured array — keeps the deps list stable so the handler isn't re-bound on every tool-call status update), finds the executing tool call with a defined `promoteAbortController`, calls `.abort({ kind: 'background' })`, and returns early. No-op when no foreground shell is executing — Ctrl+B then falls through to the input layer's existing cursor-left binding. - `keyboard-shortcuts.md`: documents Ctrl+B with explicit fall-through behavior so the conflict with the prompt-area cursor-left binding is intentional + understandable. ## Tests - `keyMatchers.test.ts` (+1): Ctrl+B positive / bare-b + meta+b + Ctrl+other negatives. - `AppContainer.test.tsx` (+2): - **Ctrl+B promotes** — pendingToolCalls includes an executing shell with a stubbed `AbortController` + spy; firing Ctrl+B asserts `abort({ kind: 'background' })` is called once. - **Ctrl+B no-op** — empty `pendingToolCalls` + Ctrl+B must NOT throw (pins the safety contract for the typing-mid-prompt case where the input layer's own Ctrl+B should still fire). - 37/37 keyMatchers + 58/58 AppContainer pass; tsc + ESLint clean. ## E2E (manual, PR description guidance) The unit / integration tests cover the keybind → abort wiring and the promote handler's downstream behavior (PR-2's tests). Real-PTY E2E is intentionally manual since headless test infrastructure doesn't drive a real shell child + Ctrl+B keystroke; documented in the PR description checklist. Closes the 3-PR sequence for #3831 (Phase D part b of #3634). * fix(cli): #3969 review wave — broadcast comment + debug log + redundancy 5 #3969 review threads addressed: - **AppContainer.tsx Ctrl+B handler**: documented the KeypressContext.broadcast caveat (after `return`, the same Ctrl+B is still dispatched to text-buffer cursor-left + DebugProfiler; visible cursor-left side effect is cosmetic) so future readers understand why the prompt cursor moves on a successful promote. Added `debugLogger.debug` calls on both branches (matched callId on success; streamingState + pendingToolCalls.length on no-op fall-through) so "Ctrl+B doesn't work" reports are debuggable. - **useReactToolScheduler.ts TrackedExecutingToolCall**: dropped the redundant `pid?` and `promoteAbortController?` declarations — both come through the `& ExecutingToolCall` intersection unchanged. Fixed the JSDoc that wrote `{ kind: 'background', shellId }`: callers don't generate `shellId` (it's optional on the abort-reason union and `handlePromotedForeground` produces it downstream). The corresponding executing branch in `toolCallsUpdateHandler` no longer projects pid / promoteAbortController explicitly — `...coreTc` already spreads them; the explicit-undefined clearing in the non-executing branch is also dropped (those fields aren't on coreTc when status !== 'executing', so `...coreTc` doesn't carry them). - **AppContainer.test.tsx**: replaced two `as unknown as Key` double-casts with direct `: Key` annotations on the literal — the object already conforms to the Key interface, double-cast was bypassing type safety needlessly. Tests: 37/37 keyMatchers + 58/58 AppContainer pass; tsc + ESLint clean. No behavior change beyond the new debug log lines. * fix(cli): #3969 wave — tool-name guard + non-shell test + defensive clear 3 #3969 review threads addressed; 1 deferred: - AppContainer.tsx: Ctrl+B `find()` predicate now also checks `tc.request.name === ToolNames.SHELL` before matching the executing tool call. Defense-in-depth — today only the shell tool wires `promoteAbortController`, but a future copy-paste / type confusion that adds the property to a non-shell tool would otherwise let Ctrl+B mistakenly fire `abort({kind:'background'})` on a tool whose service has no promote-handoff handler. - useReactToolScheduler.ts: re-added explicit `pid: undefined` and `promoteAbortController: undefined` to the non-executing return. Previously dropped on the assumption that `...coreTc` doesn't carry these fields when the status isn't `executing` — true today, but the explicit clearing is defense-in-depth against a future core change that adds either field to a non-executing status type (would surface as a stuck PID display or a Ctrl+B handler that matches a no-longer-executing tool call). - AppContainer.test.tsx: replaced the placeholder "no-op when no pending tool calls" framing on the empty-array case (it does exercise the `executing-status` predicate but NOT the tool-name guard) with TWO tests: 1. existing empty-array no-throw test (renamed for clarity) 2. NEW: executing non-shell tool with a hostile-shape `promoteAbortController` — asserts `abortSpy` is NOT called. This is the regression test for the new tool-name guard above. Tests: 61/61 AppContainer.test.tsx pass; tsc + ESLint clean. Deferred to follow-up (replied + tracked): - `debugLogger.debug` is file-only; success-path "agent unblocks + next message says 'promoted to bg_xxx'" is the user-visible signal. Adding a synthetic history item or stderr line for the gap between keypress and agent message conflicts with Ink rendering and is better as a focused UX PR. * test(cli): pin inheritance of pid + promoteAbortController via type assertions #3969 review: the earlier "redundant declaration" review removed the explicit `pid?: number` and `promoteAbortController?: AbortController` from `TrackedExecutingToolCall`, relying on the `& ExecutingToolCall` intersection to inherit them. Current review flags the type-safety regression: if core renames or removes either field, the React-side build won't catch it locally — Ctrl+B handler silently breaks at runtime. Compromise: keep the type minimal (no re-declaration noise the prior review flagged) but add compile-time `extends keyof ExecutingToolCall` assertions that fail loudly + locally if either field disappears. The assertions are evaluated at compile time and zero-cost at runtime; the dummy `const` pins them so they aren't dead code. 61/61 AppContainer tests pass; tsc clean. |
||
|
|
cb7059f54d
|
feat(installer): add standalone archive installation (#3776)
* feat(installer): add standalone archive installation * fix(installer): harden standalone archive installs * fix(installer): address standalone review findings * chore(installer): clarify review followups * fix(installer): stabilize standalone script checks * chore(installer): remove internal planning docs * chore(installer): simplify standalone release review fixes * test(installer): add Windows batch install smoke * test(installer): fix Windows batch smoke quoting * test(installer): preserve Windows cmd quotes * fix(installer): use robust Windows checksum hashing * ci: narrow installer debug matrix * fix(installer): address standalone review hardening * fix(installer): avoid Windows validation parse errors * fix(installer): simplify Windows option validation * fix(installer): harden standalone review fixes |
||
|
|
576bd8e0a7
|
ci: skip unnecessary release and SDK checks (#3984)
* ci: skip unnecessary release and SDK checks * ci: guard release skip classification for non-pr events * ci: harden release sync skip gate * ci: refine release sync skip fallback |
||
|
|
b949ae070c
|
feat(tools): defer low-frequency built-in tools to reduce initial prompt size (#4022)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Waiting to run
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Waiting to run
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(tools): defer low-frequency built-in tools to reduce initial prompt size
Mark Monitor, SendMessage, Skill, TaskStop, TodoWrite, and WebFetch as
shouldDefer=true so their full schemas are excluded from the initial
function-declaration list. The model discovers them on demand via
ToolSearch, aligning with Claude Code's deferral strategy for
infrequently used tools.
* fix(tools): don't defer TodoWrite/Skill — they're required by the system prompt
TodoWrite is mandated as high-frequency by the core system prompt
("Use VERY frequently", "IMPORTANT: Always use the TODO_WRITE tool to
plan and track tasks throughout the conversation"). Deferring it forces
a ToolSearch round-trip before every required call, adding latency and
hurting prompt compliance.
SkillTool's full description carries the dynamically generated
<available_skills> listing and the BLOCKING invocation rules
("invoke the relevant Skill tool BEFORE generating any other response").
The deferred-tool summary truncates this, so the model loses both the
list of skills and the activation contract from the initial prompt.
Keep Monitor / SendMessage / TaskStop / WebFetch deferred — those are
genuinely infrequent and have no system-prompt requirement.
|
||
|
|
0a05ea8004
|
feat(telemetry): inject traceId/spanId into debug log files for OTel correlation (#3847) | ||
|
|
464e4cf343
|
feat(core): write runtime.json sidecar for active sessions (#3714)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Waiting to run
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Waiting to run
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(core): write runtime.json sidecar for active sessions Port kimi-cli PR #2082 part 1 to qwen-code. On interactive session start, atomically write a small JSON sidecar at <projectDir>/chats/<sessionId>.runtime.json recording the (pid, session_id, work_dir, hostname, started_at, qwen_version) tuple. External tools (terminal multiplexers, IDE integrations, status daemons) can map a running PID to its session id and work dir without parsing argv. Write is best-effort: a read-only filesystem must not block UI startup. OS process title (was #3713) and dynamic OSC tab title (kimi #2083) remain out of scope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(core): refresh runtime.json on same-PID session swap Config.startNewSession() reassigns this.sessionId in the same process, which is reached by /clear, /reset, /new and /resume. Previously the old <oldId>.runtime.json was left behind, falsely claiming the still- live PID for a session no longer being served, and no new sidecar was written for the incoming session. Centralize the swap by clearing the old sidecar and writing a fresh one for the new session id from inside startNewSession itself, so all same-PID transitions are covered. The refresh runs as a fire-and- forget best-effort; failures must not block the session switch. Mirrors the post-merge Codex P1 fix on kimi-cli PR #2082 (the source of the runtime.json sidecar pattern this PR ports). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(core): only refresh runtime.json when this process owns it Mirrors kimi-cli PR #2082 commit e237951f (Codex P1 r3158754463): a short-lived non-interactive invocation (qwen --prompt, ACP, etc.) that runs `/clear` would otherwise call `Config.startNewSession()`, delete a concurrent shell's runtime.json sidecar (same outgoing session id), and never write a replacement — leaving the shell discoverable to nobody. Add a `runtimeStatusEnabled` flag on Config, flipped on by the interactive UI bootstrap immediately after the first successful sidecar write, and gate the swap-time refresh in `startNewSession()` on it. Non-interactive entry points never reach the bootstrap, so they won't touch sibling sidecars. Kimi later reverted the equivalent `write only from shell mode` guard (commit 7083975a) in favor of writing from every long-lived mode, but qwen's wire point is already interactive-only, so the narrower guard is the right shape here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
9bd5a0180b
|
feat(cli): core built-in i18n coverage (#3871)
* feat(i18n): expand built-in locale coverage * feat(cli): add dynamic slash command translation * test(cli): stabilize session picker assertions * fix(core): close jsonl readers before cleanup * fix: address i18n review regressions * fix(cli): address dynamic i18n review findings * fix(cli): address i18n review follow-ups * fix(cli): address i18n review feedback * test(cli): align i18n parity coverage with strict locales * fix(cli): address i18n review findings |
||
|
|
04729d646c
|
test: stabilize main e2e flakes (#3992)
* test: stabilize main e2e flakes * test: stabilize macos e2e assertions |
||
|
|
1777b20e93
|
perf(core): bound session-list metadata reads to head/tail 64KB; pool buffer; lazy message count (#3897)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Waiting to run
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Waiting to run
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
SDK Python / SDK Python (3.10) (push) Waiting to run
SDK Python / SDK Python (3.11) (push) Waiting to run
SDK Python / SDK Python (3.12) (push) Waiting to run
* perf(core): drop full-file readline + pool tail buffer in listSessions
`listSessions` previously called `countSessionMessages` per file, which
streamed the entire JSONL through `readline` to count unique
user/assistant UUIDs. For a project with N sessions averaging M bytes
each, every /resume open paid O(N · M) wall time before showing the
picker — by far the dominant cost once a project accumulated many
multi-MB sessions.
This change:
- Drops the per-file count from listSessions / findSessionsByTitle.
`SessionListItem.messageCount` is now optional. Callers that need
a count call the new public `SessionService.countSessionMessages
(sessionId)` lazily — typically only when a SessionPreview panel is
about to display the badge.
- Pools a single 64KB tail-read buffer across the per-file metadata
reads in listSessions / findSessionsByTitle, mirroring the pattern
in claude-code's `enrichLogs`. The two helpers in
`sessionStorageUtils` (`readLastJsonStringFieldSync` and
`readLastJsonStringFieldsSync`) accept an optional caller-owned
scratch buffer; one-off callers (rename, single-session lookup) pass
nothing and keep the original alloc behaviour.
- Updates SessionPicker's row metadata to omit the "N messages"
segment when `messageCount` isn't available, keeping the visual
layout intact.
Tests:
- Pin that listSessions does not populate messageCount (regression
guard against silently re-introducing the per-file scan).
- Smoke test the buffer-pool plumbing — same caller-owned buffer
handed to two reads of different file sizes returns both correct
values without state bleed.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* perf(core): re-anchor custom_title; drop Phase-2 full-file scan
Tightens the title-write / title-read invariant so the picker's
metadata read is bounded to a fixed 2 × 64KB per file, regardless
of session length, with no fallback that scales with file size.
Writer (`ChatRecordingService`):
- New `bytesSinceTitleAnchor` counter and `TITLE_REANCHOR_BYTES`
threshold (32 KB, half of LITE_READ_BUF_SIZE).
- `appendRecord` now updates the counter and, once it crosses the
threshold while a title is set, re-appends a fresh `custom_title`
record to EOF (`reanchorTitle`). The recursive append routes
back through the same tracking path, which sees the title
record and resets the counter to zero.
- Sessions that never set a title pay zero overhead — the early
return in `updateTitleAnchorTracking` short-circuits.
Reader (`sessionStorageUtils`):
- `readLastJsonStringFieldSync` / `readLastJsonStringFieldsSync`
now read the file's last 64 KB, fall back to the first 64 KB
on miss, and return `undefined` if neither contains the field.
The previous Phase-2 streaming full-file scan (capped at 64 MB)
is removed entirely.
- The pooled scratch buffer (already optional) now backs both the
tail and head reads — only one allocation per `listSessions`
page even with the head fallback firing.
- `MAX_FULL_SCAN_BYTES` constant deleted (unused).
The two changes are coupled: the reader's tighter bound only works
because the writer guarantees the title stays in tail. A title
buried mid-file is now intentionally `undefined` (the picker
falls back to `firstPrompt` for display) rather than triggering a
full-file scan that would freeze the UI on long agent transcripts.
Tests:
- `chatRecordingService.customTitle.test.ts` — three scenarios
pinning the re-anchor invariant: threshold trigger, no-spurious
on no-title sessions, no-trigger on small write bursts.
- `sessionStorageUtils.test.ts` — replaces Phase-2 tests with
head-window fallback tests; pins the new "buried beyond both
windows returns undefined" contract; updates buffer-pool reuse
test to cover both tail and head reads with the same scratch.
E2E coverage (35/35 scenarios on a separate harness against real
fs): R1–R10 reader contract, M1–M4 multi-field, W1–W7 writer
re-anchor, L1–L6 listing latency + regression pins, S1 stress
(200 × 3 MB), C1 concurrency, I1–I5 writer/reader integration.
Measured 2.6× speedup on listSessions(50) over 50 × 4 MB
sessions vs the legacy `countSessionMessages` baseline; speedup
scales linearly with average file size.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(core): cover CR review gaps for session-list perf path
- countSessionMessages: valid id counts unique user/assistant uuids and
ignores other types/malformed lines; invalid id short-circuits without
filesystem access; ENOENT degrades to 0 instead of bubbling.
- readLastJsonStringFieldsSync: mirror the single-field variant's
scratch-buffer reuse test across a tail-hit then head-fallback to
catch any decode that ignores bytesRead.
- ChatRecordingService re-anchor: legacy resumed session (source
undefined) must omit titleSource on threshold-triggered re-anchor,
not silently reclassify as 'manual'.
Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>
* fix(core): count title-anchor bytes as utf-8 + reset on reanchor failure
`String.length` undercounts the on-disk size of multi-byte payloads
(CJK, emoji are 1 UTF-16 unit but 3 UTF-8 bytes), so a session full
of CJK content could push >96KB of writes past the last anchor before
the 32KB threshold thinks it has — silently drifting the title past
the 64KB tail window the picker scans. Switch the byte counter to
`Buffer.byteLength(..., 'utf8')` for parity with `jsonl.writeLine`.
Also reset `bytesSinceTitleAnchor` to 0 in the reanchor catch:
without it, a failing reanchor pins the counter at the threshold
and turns a single transient I/O fault into a per-record retry
storm. One missed anchor is the right tradeoff — finalize() will
re-emit on the next lifecycle event.
Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>
* fix(core): trim head-window to whole lines in lite metadata reads
The 64KB head-window fallback in `readLastJsonStringFieldSync` /
`readLastJsonStringFieldsSync` reads a fixed slice and hands it
straight to the extractor — its trailing bytes can fall mid-record.
A partial line whose `customTitle` value happens to close inside
the buffer but whose body extends past 64KB would otherwise win
the latest-match race and surface as the picker's title.
Drop everything past the last `\n` before extracting (only when the
buffer is shorter than the file — a small file is necessarily whole-
line). Honors the original Phase-2 contract that only complete lines
get a vote, without paying for the deleted full-file scan.
Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>
* fix(core,cli): preview lazy count, project-scope countSessionMessages, pin perf contract
Address #3897 follow-up review findings:
- SessionPreview footer no longer drops the message count when listSessions
omits it. The prop is the override path; default falls back to a unique
user/assistant uuid count derived from the already-loaded conversation,
matching countSessionMessages semantics with zero extra disk I/O.
- countSessionMessages now scopes to the current project, mirroring the
first-record cwd check in deleteSession/renameSession/loadSession. A
valid sessionId from another project sharing the chats dir no longer
bypasses project boundaries on lazy count.
- New regression tests:
- SessionPicker row renders cleanly with messageCount === undefined
(no "messages"/"undefined"/dangling separator)
- findSessionsByTitle perf contract: matches have messageCount
undefined and fs.createReadStream is never called
- countSessionMessages: cross-project sessions return 0 without
streaming, empty file returns 0
- Update corruption-recovery tests to call private countSessionMessagesFromPath
(the streaming entry point) instead of the public sessionId-shaped API the
merge from main pointed them at.
Co-Authored-By: Qwen-Coder <noreply@qwen.com>
* docs(core): correct stale 'full-file fallback' comments to head-window
The session metadata reader uses two bounded 64KB windows (tail + head)
since the perf rework on this branch — never a full-file scan. Two
comments still described the prior behavior. Reported in MR review.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): route ACP renameSession through live ChatRecordingService
ACP renameSession constructed a fresh SessionService and wrote the
custom_title record straight to disk. When the same session was live
in this process, ChatRecordingService.currentCustomTitle stayed at
the old value, so the next title re-anchor (every 32KB) or finalize()
re-emitted the stale title at EOF and silently reverted the rename.
Now we look up the live ChatRecordingService first and route through
recordCustomTitle, which keeps the in-memory cache and the on-disk
record in sync. The SessionService path remains for the non-live case
(e.g., another client renaming a backgrounded session). Reported in
MR review.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): harden session title re-anchor invariants
* docs(core): clarify bounded metadata race recovery cost
* test(core): cover bounded multi-field title reads
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>
Co-authored-by: Qwen-Coder <noreply@qwen.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
|