getAutoMemoryRoot() resolved linked worktrees back to the canonical
repository root, so every worktree of a repo shared one project memory.
Focused worktree sessions polluted the shared MEMORY.md index with
unrelated entries, and chats/, workflows/, and team memory were already
per-worktree — project memory was the only shared exception.
Anchor project memory at the nearest git root (same resolution team
memory already uses) so each worktree gets its own memory directory.
The main checkout resolves to the same path as before, so existing
memory is unaffected.
Fixes#6449
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(channels): add dmPolicy config to disable private/DM messages
Add DmGate class mirroring GroupGate to gate DM/private messages in
channel adapters. Operators can now set dmPolicy: 'disabled' in their
channel config to silently drop all DM messages while keeping group
messages active.
Closes#6392
* fix(channels): address review feedback for dmPolicy
- Add dmPolicy: 'open' to all test config factories (8 files) to
maintain type correctness with required ChannelConfig field
- Add integration tests in ChannelBase.test.ts:
- preflightInbound: DM dropped + group passes when dmPolicy=disabled
- isStoredLoopTargetAuthorized: DM loop job disabled + group passes
- Add dmPolicy assertions in config-utils.test.ts (default + explicit)
- Keep dmPolicy as required field (not optional) for strict parity
with groupPolicy
The WeCom intelligent robot channel (added in #6436) has its own guide and _meta.ts entry, but the channels overview was never updated. Add WeCom to the platform list, quick-start guide links, the `type` option and `token` exclusion note, new `botId`/`secret` option rows, the media-support note, and the slash-command channel enumeration.
Co-authored-by: Claude <noreply@anthropic.com>
* perf(cli): defer startup prefetch tasks
* fix(cli): await IDE for prompt-interactive startup
* perf(cli): defer interactive telemetry startup
* test(cli): add missing assertions and Zed/ACP path coverage for startup prefetch
Address three test coverage gaps identified during code review:
- Assert mockStartEarlyStartupPrefetches in both kitty protocol tests
(C1: API preconnect call was wired but never verified)
- Add Zed/ACP integration test verifying deferIdeConnection is false
when getExperimentalZedIntegration returns true (C2: Zed path was
entirely untested)
- Assert mockStartBackgroundHousekeeping in startup-prefetch test
(C3: unconditional housekeeping dispatch was never verified)
* docs: move startup prefetch design doc to performance subdirectory
* docs: translate startup prefetch design doc to English
* fix(cli): address startup prefetch review comments
Tighten the startup prefetch follow-up fixes from review while keeping
prompt-interactive telemetry on the fast interactive startup path.
- Preserve Error objects when deferred startup tasks fail
- Remove the unbalanced api_preconnect profiler lifecycle event
- Guard background housekeeping so it only runs for interactive configs
- Document and test prompt-interactive telemetry deferral semantics
* fix(cli): initialize telemetry for prompt-interactive prompts
Ensure sessions launched with an initial interactive prompt have
telemetry ready before the auto-submitted first request runs.
- Exclude prompt-interactive startup from telemetry deferral
- Pass a post-render telemetry option through interactive UI startup
- Skip duplicate post-render telemetry startup for initial prompts
- Update tests to cover the first-prompt telemetry guarantee
Note: Plain interactive TUI startup still defers telemetry post-render.
* fix(cli): preserve startup first-request guarantees
Keep deferred startup work from weakening first-request behavior in
interactive sessions that submit prompts automatically or remotely.
- Store telemetry deferral on Config and reuse that decision at render time
- Keep IDE startup awaited for prompt-interactive and input-file sessions
- Add a timeout for deferred IDE connection failures
- Cover ordinary interactive telemetry deferral and IDE startup edge cases
* fix(cli): make post-render IDE connection opt-in
Default startInteractiveUI to the already-connected IDE path so future
callers do not accidentally connect twice when initializeApp used its
eager default.
- Change the post-render IDE connection default to false
- Update startInteractiveUI tests to assert the safer default
* perf(cli): surface deferred IDE connection status
Make ordinary interactive IDE startup visible while preserving the
post-render prefetch path and first-paint performance tradeoff.
- Emit deferred IDE connection lifecycle events for connecting, success,
and failure states
- Surface IDE startup status in the TUI footer without blocking input
- Log late underlying IDE failures after timeout for better diagnostics
- Document telemetry deferral tradeoffs and add startup lifecycle tests
---------
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
* feat(channels): add natural channel memory intents
* fix(channels): add explicit guard and exhaustiveness check for clear_confirm intent
The clear_confirm path was handled as implicit fall-through at the bottom
of handleChannelMemoryIntent. If a new intent kind were added to the
ChannelMemoryIntent union, it would silently execute clearChannelMemory
without user confirmation — a data-loss risk.
Add explicit if (intent.kind === 'clear_confirm') guard and a
const _exhaustive: never assertion so TypeScript flags any unhandled
kinds at compile time.
* fix(channels): close session leak in classifier and fix regex separator
- BridgeChannelMemoryIntentClassifier now wraps prompt() in try/finally
to always call cancelSession(), preventing daemon session leaks on
every classifier invocation. Cleanup errors are caught so they cannot
mask a successful classification result.
- Add missing optional punctuation separator to the 以后记住 regex
pattern for consistency with other Chinese remember patterns.
* fix(channels): enforce pending clear state for channel memory confirmation
The clear_confirm intent executed clearChannelMemory directly without
verifying a prior clear_request was issued for the same chat. Any
authorized user could clear any chat's memory by sending the confirmation
phrase standalone, bypassing the two-step flow.
Add a per-target pending clear map (chatId + threadId, 60s TTL) that
is set during clear_request and verified+consumed during clear_confirm.
Standalone confirmation phrases now get rejected with a prompt to
issue the clear request first.
* fix(channels): include senderId in pendingClears key to prevent cross-user confirmation
User A could initiate clear_request in a group chat and User B could
confirm it, since the pending key only included chatId+threadId.
Add senderId to the key so only the user who initiated the clear
can confirm it.
* fix(channels): harden memory intent review fixes
* fix(channels): cover memory clear sender guard
* fix(channels): block group memory mutations
* fix(channels): avoid ambiguous memory saves
* test(channels): cover memory classifier cleanup
* test(channels): cover memory clear expiry
* fix(channels): restore channel memory slash aliases
* test(channels): cover memory intent edge cases
Design docs and implementation plans were scattered across .qwen/design,
.qwen/plans, and docs/superpowers. The .qwen/ locations are git-ignored, so
docs written there never got tracked, while docs/design already held the
richer, version-controlled set. Consolidate everything under docs/design and
docs/plans, relocate two stray root docs into docs/design, and repoint the
references left dangling by the move (moved-doc cross-links and a few source
comments).
Also update AGENTS.md and the feat-dev skill so the documented workflow writes
new design docs and plans to the tracked docs/ locations.
Co-authored-by: DragonnZhang <dragonzhang1024@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): add --project and --global flags to /model for per-project model persistence
Add scope control to the /model command so users can persist model
selections to either project-level or user-level settings independently.
- /model --project: persist to workspace .qwen/settings.json
- /model --global: persist to user ~/.qwen/settings.json
- /model (no flag): unchanged behavior (backward compatible)
- Model dialog title shows scope: 'Select Model (this project)' / 'Select Model (global)'
- Completion and argumentHint updated with new flags
- Full i18n support for zh/en
Closes#6052
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): add missing zh-TW translations for /model scope flags
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): address PR review — scope flags, subcommand persistScope, titles, tests
- parseScopeFlags: use (?:^|\s) instead of \b for --flag matching
(\b fails because - is not a word character)
- Completion: strip all flags to isolate model prefix, supports any order
- Subcommand dialogs (fast/voice/vision) now propagate persistScope
- slashCommandProcessor forwards persistScope for all subcommand cases
- ModelDialog title combines subcommand mode + scope label
e.g. 'Select Fast Model (this project)'
- Subcommand confirmations show scope suffix (project/global)
- Extract persistScopeSpread() helper to reduce duplication
- Add 9 tests covering scope flags, dialog returns, confirmations
- Add i18n keys for scope suffix labels in zh/en/zh-TW
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): use Partial<Config> & {[key:string]:unknown} to fix index signature TS error
Replace Record<string,unknown> with Partial<Config> & {[key:string]:unknown}
to satisfy TS4111 index signature access rule in the CI build.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): add scope suffix to ModelDialog history items
Address review comment: historyManager.addItem for voice/fast/vision/main
model selections now shows scope indicator like ' (this project)' or
' (global)', consistent with CLI direct-set confirmations.
Affected: handleModelSwitchSuccess (main), handleSelect (voice/fast/vision)
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): wrap scopeSuffix in t() and unify wording with ModelDialog
- scopeSuffix in modelCommand.ts now uses t(' (this project)') / t(' (global)')
instead of hardcoded English strings, matching ModelDialog.tsx wording
- Main model confirmation uses shared scopeSuffix instead of separate
i18n keys, eliminating 'Model: {{model}} (project)' duplication
- Remove unused i18n keys from en/zh/zh-TW locales
- Update tests to expect '(this project)' wording
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): address code review feedback — scope validation, i18n, tests
- Reject inline prompt + scope flag combination with clear error (#1)
- Add mutual exclusivity check for --project and --global (#5)
- Verify setValue scope parameter in tests + add --global test (#2)
- Extract scopeSuffix to shared variable, remove duplication (#3)
- Remove dead i18n keys 'Select Model (this project)' / '(global)' (#4)
- Fix scopeSuffix placement on model line not API key line (#8)
- Add fr.js / ja.js translations for scope keys (#10)
- Remove unused export ModelDialogPersistScope (#6)
- Wrap non-interactive help text in t() with new flags (#7)
- Fix argumentHint grouping to show mode vs scope flags (#11)
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): reject --project when workspace is untrusted
Reject --project scope flag before direct persistence or opening ModelDialog
when settings.isTrusted is false. Workspace settings are ignored on merge in
that state, so the save would silently not take effect.
Also mirrors the guard in ModelDialog.tsx resolvePersistScope() to fall back
to user scope when the dialog is opened with --project on an untrusted folder.
Default mock settings now includes isTrusted: true.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
---------
Signed-off-by: Alex <alex.tech.lab@outlook.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
The bundled /review skill is a general command that runs against arbitrary
repositories (and cross-repo PRs), but a previous change baked qwen-code's own
"core infrastructure is maintainer-only" governance into the shipped prompt:
hardcoded packages/core and packages/*/src/{auth,providers,models,config,tools,services}
paths, a 500+ line hard block, and an authorAssociation-based maintainer check.
Those path names are generic — src/auth, src/config, src/tools, src/services are
common across monorepos — so an external contributor's large PR to an unrelated
repo would be hard-blocked as "must be maintainer-initiated" under a policy that
repo never adopted.
Remove the gate and its escalate-flag plumbing (Steps 1, 6, and 7) from the
bundled skill, along with the matching DESIGN.md rationale and the user-doc
section. qwen-code's maintainer-only policy stays documented in AGENTS.md for
this repo. The Issue Fidelity / root-cause ownership agent (Agent 0) is a
universal review principle and is left unchanged.
Co-authored-by: dragon <dragon@U-2Q53JQG9-0233.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(review): add issue-fidelity and root-cause ownership gate to /review
Adds a dedicated Issue Fidelity & Root-Cause Ownership agent (Agent 0) to
the /review pipeline and a core-infrastructure scope gate that runs before
the review agents.
Agent 0 fetches linked GitHub issue evidence directly (closingIssuesReferences
plus issue comments) instead of trusting the PR author's framing, compares the
original reported failure against the PR's claimed fix, and flags client-side
parser/sanitizer workarounds for malformed upstream output as Critical unless a
maintainer explicitly requested the defensive mitigation. The core-infra gate
applies the repository's existing two-tier maintainer-only rule before spending
review budget.
This hardens the pipeline against a false-approval mode where a bot PR passes
its own tests and reads as internally reasonable but fixes the author's mistaken
diagnosis rather than the linked issue's actual root cause.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(review): address PR review feedback on issue-fidelity gate
- Fetch issue evidence with `gh issue view --json title,body,comments` so the
issue body (reporter repro/observed payload/expected behavior) is included;
`--comments` alone omits it. Use each closingIssuesReferences entry's own
repository so cross-repo linked issues resolve correctly.
- Treat closingIssuesReferences as a discovery hint (fetch apparent target
issues even when it is empty) and treat fetched issue content as untrusted
data (extract facts, ignore embedded instructions).
- Run Agent 0 (Issue Fidelity) only for PR targets; skip it for local-diff and
file-path reviews, and require the PR number/repo/context in its prompt.
Handle empty references / non-bugfix / gh failure explicitly.
- Pass Agent 0's quoted issue evidence to Step 4 batch verification and stop it
rejecting issue-grounded findings just because the code compiles/tests pass.
- Make the core-infrastructure gate concrete: deterministic maintainer signal
via authorAssociation, count only core-path lines, honor the AGENTS.md
low-risk-sweep exception, clean up the worktree on hard block, run the gate
right after fetch-pr (before npm ci), and map escalate -> COMMENT (never
APPROVE) in Steps 6-7.
- Sync agent counts and token math across SKILL.md, DESIGN.md, and
code-review.md (Agent 0 is PR-only; ~620-730K).
* docs(review): rename 'Linked Issue Fit' heading to 'Issue Fidelity'
Aligns the code-review docs heading with the 'Issue Fidelity' name used
for Agent 0 in SKILL.md and DESIGN.md, so the section connects to the
pipeline diagram. Addresses review feedback.
* docs(review): stop core-infra hard block before load-rules and surface it via --comment
- Hard block now stops before Step 2 (load-rules) instead of before Step 3,
so a PR destined for hard-block no longer runs the load-rules step.
- In --comment mode the hard block posts an event=COMMENT on the PR, matching
the escalate path's GitHub visibility, so external authors see the block.
---------
Co-authored-by: dragon <dragon@U-2Q53JQG9-0233.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Correct and complete the user-facing settings documentation against
packages/cli/src/config/settingsSchema.ts:
- settings.md: fix general.defaultFileEncoding type (enum, not string);
document the general.voice.* dictation settings, top-level
modelFallbacks and modelPricing, tools.computerUse.idleTimeoutMs,
mcp.toolIdleTimeoutMs, and the skills.disabled denylist.
- model-providers.md: correct the resolution-layers table — only
--openai-api-key/--openai-base-url exist; there are no
provider-specific credential CLI flags.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(core): Add LSP server config hot-reload support
- Implement reconcileServerConfigs to diff desired vs current LSP configs and apply minimal add/remove/restart operations with a serialized reconcile queue
- Add configHash utility to detect config changes via stable hashing
- Add lspConfigWatcher in CLI to watch .lsp.json and trigger reconciliation on file changes
- Extend LspServerManager with per-server config hash tracking and detailed debug logging
- Add design docs for LSP runtime reinitialization and hot-reload overview
- Include comprehensive unit tests for all new modules
* refactor(cli): Extract registerLspHotReload from main function
Move the LSP config file watcher setup and reconciliation logic into a dedicated module-private function registerLspHotReload, reducing the size and nesting depth of the main startup flow. Added a JSDoc summarizing responsibilities, early-return conditions, and the AppEvent.LspStatusChanged side effect.
* fix(lsp): release server resources during reload
* fix(lsp): address hot reload review feedback
* fix(lsp): harden hot reload reconciliation
* docs(lsp): update hot reload design notes
* fix(lsp): harden hot reload retry semantics
* fix(lsp): harden hot reload lifecycle
* fix(lsp): harden hot reload lifecycle
* fix(lsp): isolate hot reload recovery paths
* fix(lsp): align command probes and replay tracking
* fix(lsp): prevent crash restarts during shutdown
* fix(lsp): preserve reload state across failures
* fix(lsp): cancel reloads during shutdown
* fix(lsp): handle socket startup races
* fix(lsp): harden command probe env and socket startup
* fix(lsp): report skipped reload and restart states
* fix(lsp): harden hot reload lifecycle cleanup
* chore: add one comment for `Object.create(null)`
---------
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Make tool declaration ordering deterministic so prompt-cache prefixes do not depend on asynchronous registration history.
- Sort function declarations by canonical tool name after existing visibility filtering
- Preserve deferred, revealed, and alwaysLoad filtering semantics
- Add tests covering deferred tools, revealed tools, and MCP registration order
- Document the prompt-cache motivation and next-step cache break detection plan
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
* docs: fix skill invocation syntax and include Feishu in channel lists
* docs: add Feishu column to channel media-handling table
Address review feedback on PR #6320: after adding Feishu to the prose
channel lists, the 'Platform differences' media-handling table still
omitted it. Add a Feishu column (images/files via the authenticated Open
API resources endpoint, 50MB limit; rich-text 'post' captions), verified
against packages/channels/feishu/src/media.ts and FeishuAdapter.ts. Add a
note that QQ Bot ignores incoming media (per QQChannel.ts) so it has no
row.
* docs: clarify Feishu post messages drop embedded images
The Platform differences table claimed Feishu rich-text (post) messages
carry 'mixed text + images', but FeishuAdapter's post parser only
extracts text/a/at nodes and silently drops img nodes (both the live
handler and the history-backfill path). Correct the Captions cell to
say text is extracted and embedded images are ignored.
* docs: mark clientId/clientSecret as required for Feishu too
Feishu declares requiredConfigFields: ['clientId', 'clientSecret']
(packages/channels/feishu/src/index.ts), same as DingTalk, but the
channel options table listed both fields as DingTalk-only. Update the
Required column and descriptions to cover Feishu (App ID / App Secret).
* docs: note token is not needed for Feishu in channel config table
* docs: note 50MB limit on Feishu image downloads
Feishu images and files both flow through the same downloadMedia() in
packages/channels/feishu/src/media.ts, which enforces a single
MAX_DOWNLOAD_BYTES = 50MB cap. Add the (50MB limit) note to the Images
cell for consistency with the Files cell.
* docs: clarify /skills panel-vs-run behavior per review feedback
- skills.md: add a migration Note that /skills <name> now opens the
Skills panel and ignores trailing args; use /<skill-name> to run.
- commands.md: list the /skills Usage cell as /skills, /<skill-name>
for consistency with the rest of the table.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: 易良 <1204183885@qq.com>
* fix(core): treat request timeout of 0 as disabled instead of aborting immediately
A provider `generationConfig.timeout` of `0` (and `QWEN_CODE_API_TIMEOUT_MS=0`) now
disables the request timeout, matching the existing `QWEN_STREAM_IDLE_TIMEOUT_MS=0`
convention, instead of being coerced to the 120s default (Anthropic `||`) or passed
to the OpenAI SDK as `timeout: 0` (which the SDK treats as an immediate abort).
- add `resolveRequestTimeout()` + `DISABLED_REQUEST_TIMEOUT_MS`, mapping a disabled
timeout to the JS timer ceiling (2^31-1 ms), reusing the same ceiling already used
for `MAX_STREAM_IDLE_TIMEOUT_MS`
- use it in the OpenAI default/dashscope providers and the Anthropic provider
- accept `0` in the `QWEN_CODE_API_TIMEOUT_MS` env override without weakening the
shared `parsePositiveIntegerEnv` (relied on by ~15 other callers to reject 0)
- document the timeout unit and 0-disables semantics in settings.md
Fixes#6049
* Update packages/core/src/core/openaiContentGenerator/provider/dashscope.test.ts
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
* test(core): fix broken constants mock merge in dashscope.test
Commit 0f5aa0c interleaved two vi.mock('../constants.js') blocks, leaving
orphaned fragments that produced TS syntax errors and stopped the dashscope
suite from loading. Replace with a single importOriginal-based mock that
overrides only DASHSCOPE_PROXY_BASE_URL and delegates every other constant
(timeouts, DISABLED_REQUEST_TIMEOUT_MS, resolveRequestTimeout) to the real
module, so the mock cannot drift from the implementation.
---------
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
* fix(core): give Stop-hook continuations a fresh per-turn tool-call budget; make the cap configurable
A blocking Stop-hook continuation (e.g. a /goal iteration) feeds a fresh
user-role prompt to the model — a new logical turn — but the loop detector
never reset, so an entire goal chain billed one per-turn tool-call budget
and healthy long-running goals halted with turn_tool_call_cap. The ACP
daemon path already used per-continuation budgets; core now matches.
- Reset loop detection at each blocking Stop-hook continuation
- Add model.maxToolCallsPerTurn setting (default 100; <= 0 disables),
resolved once in Config (<= 0 maps to Infinity)
- Honor the in-session 'Disable loop detection for this session' choice
in the per-turn cap, as the dialog always claimed
- Point the headless halt message at the setting; update dialog/docs
* test(cli): add DEFAULT_MAX_TOOL_CALLS_PER_TURN to core module mocks
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* feat(serve): add runtime.activity fields to daemon status API
Add activePrompts, lastActivityAt, and idleSinceMs to the
GET /daemon/status runtime section. These fields already exist on the
bridge (and are exposed via GET /health?deep=1) but were missing from
the richer status endpoint that operators use for troubleshooting.
The idleSinceMs value is computed from a cached lastActivityAt read
(same pattern as the health handler) to ensure consistency within a
single response.
* feat(serve): add MCP server health summary to workspace status
Extract serversConnected, serversErrored, and serversDisabled counts
from the MCP servers array into the workspace.mcp.summary object.
Operators can see MCP fleet health at a glance without expanding the
full JSON.
* fix(serve): guard activity fields against undefined bridge getters
Add ?? null / ?? 0 fallbacks for lastActivityAt and activePromptCount
to prevent RangeError when a test fake bridge omits these properties.
Carry nested-agent lineage (parentAgentId, parentName, depth) through the
daemon tasks snapshot as optional fields and render the web-shell tasks
panel as a tree: children group under their parent with a ↳ marker and
clamped indentation, agents whose parent left the roster are promoted to
root with a "from <parent>" annotation, and the detail view gains a
nesting line. The [blocking] tag and the two-step stop confirmation now
apply only to provably user-blocking chains, mirroring the TUI's
agent-forest semantics from #6191.
* feat(scheduler): make recurring cron/loop job expiration configurable
Recurring cron/loop jobs previously auto-expired after a hardcoded 7 days with no way to extend or disable the limit, forcing long-running daemon deployments to recreate jobs weekly.
Add an experimental.cronRecurringMaxAgeDays setting (default 7) and a QWEN_CODE_CRON_MAX_AGE_DAYS environment-variable override (takes precedence, for cloud/container deployments). A value of 0 disables expiry so jobs run until deleted; negative or unparseable values fall back to the 7-day default. The configured limit also applies to durable tasks restored from disk, and the CronCreate tool description now reflects the effective limit instead of a hardcoded '7 days'.
Closes#6167
* refactor(scheduler): drop unused DEFAULT_RECURRING_MAX_AGE_MS export
Review feedback on #6173 — the ms constant is only used inside the module as the constructor default; keep only the days constant public.
* fix(scheduler): align zero max-age semantics and warn on cron expiry misconfiguration
Review feedback on #6173:
- CronScheduler constructor now maps 0 to Infinity (never expire), matching the config layer instead of silently substituting the 7-day default for direct callers.
- Invalid QWEN_CODE_CRON_MAX_AGE_DAYS / cronRecurringMaxAgeDays values now log a warning before falling back to the default, leaving a breadcrumb for misconfigured deployments.
- Durable tasks found past the recurring max age at load now log a warning before their final fire + delete, since a lowered max age retroactively expires long-lived tasks and deletion is unrecoverable.
- New durable-restore tests: a custom max age applies to tasks reloaded from disk, and a disabled max age (Infinity) restores a 30-day-old task as live instead of aging it out.
* fix(scheduler): surface cron expiry warnings on the console
Review feedback on #6173:
- The invalid-config and retroactive-expiry warnings moved from debugLogger (file-only, off unless QWEN_DEBUG_LOG_FILE is set) to console.warn so they reach container/daemon logs where this knob matters; the config warning is latched to fire once per Config instance.
- CronCreateTool's constructor now resolves the max age once instead of three times, so an invalid env var can't emit duplicate warnings during tool registration.
* fix(scheduler): reject non-finite timestamps in durable task validation
Review feedback on #6173: JSON like -1e999 parses to -Infinity, which passes the typeof-number check and then poisons date math — with a finite lastFiredAt the entry reads as an overdue aged task and the retroactive-expiry warning's toISOString() throws RangeError mid-load, so one malformed persisted entry blocks durable cron startup/takeover. Validation now requires finite createdAt (and lastFiredAt when non-null), routing such entries through the existing fix-or-delete contract for corrupt files. Verified the new end-to-end test reproduces the RangeError without the fix.
* refactor(scheduler): single-source the max-age contract and freeze it at Config construction
Review follow-ups on #6173:
- Extract normalizeRecurringMaxAge as the single owner of the
0/Infinity no-expiry contract, used by both the Config layer and the
CronScheduler constructor, so the constructor's 0 handling can no
longer be removed as apparent dead code.
- Resolve QWEN_CODE_CRON_MAX_AGE_DAYS once at Config construction into
a readonly field, honoring the setting's requiresRestart contract;
mid-session env changes can no longer make the tool description,
tool output, and scheduler report different expiries. The warn
once-latch is now unnecessary (construction warns at most once).
* feat(core): allow bounded nested sub-agent spawning via maxSubagentDepth
Sub-agents may now spawn sub-agents up to a configurable maximum nesting
depth (default 5; 1 reproduces the previous no-nesting behavior).
Enforced in two layers sharing one predicate: prepareTools() hides the
agent tool from leaf-depth sub-agents, and AgentTool.execute() rejects
over-depth spawns as an authoritative backstop. Teammates, forks, and
the workflow tool remain excluded from nesting. Launch depth is
persisted in the agent meta sidecar and restored on resume (including
deferred-approval continuations and in-process AgentInteractive frames)
so a resumed nested agent cannot regain spawn capacity.
See knowledge/qwen-code/design/nested-subagents.md.
* fix(core): address review findings on nested sub-agent spawning
- Deny the agent tool to workflow-spawned subagents: depth gating would
otherwise re-admit it, letting a workflow leaf spawn outside the
orchestrator's concurrency cap, agent accounting, and token budget.
- Reject non-finite maxSubagentDepth values (JSON 1e309 parses to
Infinity and would unbound the recursion cap; NaN would silently block
all nesting) and cap the knob at 100 to catch typos.
- Add a --max-subagent-depth CLI flag mirroring sibling budget flags,
with loud validation for flag typos, and document the setting.
- Log guard rejections (depth, fork containment) and silent
fork-to-subagent downgrades through the agent debug logger.
- Refresh stale comments (depthOverride resume pinning, depth-gated
AgentTool exclusion) and drop references to a design doc that lives
outside the repository.
- Fill review-noted test gaps: nesting predicate primitives, fork-context
prepareTools, persisted-depth restoration on background resume,
nested AgentInteractive depth pinning, nested fork fallback, and the
blocked-spawn returnDisplay shape.
* fix(cli): add maxSubagentDepth to the CliArgs test literal
The exhaustive CliArgs mock in gemini.test.tsx missed the new field,
failing CI's clean tsc build (the local incremental build skipped the
test file).
* fix(core): add a teammate backstop to the agent spawn guards
execute() backstopped depth and fork containment but not the teammate
exclusion, so its guards covered less than prepareTools() gates. A
teammate spawn call that slipped past schema-hiding would have nested.
Block it symmetrically with the fork guard, log the rejection, and pin
the behavior in a test.
* fix(core): normalize persisted maxSubagentDepth on resume
The resume path trusted the raw sidecar value, bypassing the Config
clamp — a tampered or malformed sidecar (1e309 parses to Infinity;
JSON.stringify turns Infinity into null) would remove the nesting cap
for resumed agents. Extract the clamp into a shared
normalizeMaxSubagentDepth used by both the Config constructor and the
flag-restore path, and refresh the stale settings schema description
(clamp range, non-finite fallback, workflow-agent wording).
* test(core): pin null-to-default normalization of resumed maxSubagentDepth
JSON.stringify(Infinity) === 'null', so a sidecar can legitimately carry
null; widen the persisted flag type to admit it and parameterize the
resume test over both the clamp (5000 -> 100) and the null fallback
(null -> 5).
* fix(core): harden nesting depth edges from final review pass
- Normalize persisted meta.depth on resume: the sidecar is untrusted
JSON, and a tampered negative depth (or -1e309 → -Infinity) would pin
the resumed frame below zero and pass canSpawnNestedAgent for every
cap. Invalid values fail closed to the depth ceiling — the agent
keeps running but cannot spawn.
- Register monitor notification routing for in-process interactive
agents: framing runLoop() made their monitors agent-owned, and owned
dispatch has no session fallback, so notifications were silently
dropped. InProcessBackend now routes them into the agent's message
queue and tears the routing down on release.
- Downgrade background spawn requests from nested launchers to awaited
foreground runs: a nested launcher cannot honor the background
completion contract (send_message/task_stop excluded, notifications
session-scoped), which orphaned the child's results.
- Extract spawnBlockReason() as the single spawn-exclusion policy
shared by prepareTools() and execute(), replacing two hand-kept
copies of the depth/teammate/fork rules.
- Share DEFAULT_MAX_SUBAGENT_DEPTH / MAX_SUBAGENT_DEPTH_LIMIT across
the core normalizer, the CLI flag validator, and the settings schema.
- Log dropped teammate names from nested spawns; revert the impossible
|null persisted-flag widening to an honest tampered-sidecar framing;
document the constructor-time depth capture invariant.
* test(cli): add DEFAULT_MAX_SUBAGENT_DEPTH to core package mocks
settingsSchema.ts now imports the shared constant, so CLI tests that
mock @qwen-code/qwen-code-core with an explicit export list need the
new export.
* feat(cli): display nested sub-agents as a tree in the TUI (#6191)
* feat(core): allow bounded nested sub-agent spawning via maxSubagentDepth
Sub-agents may now spawn sub-agents up to a configurable maximum nesting
depth (default 5; 1 reproduces the previous no-nesting behavior).
Enforced in two layers sharing one predicate: prepareTools() hides the
agent tool from leaf-depth sub-agents, and AgentTool.execute() rejects
over-depth spawns as an authoritative backstop. Teammates, forks, and
the workflow tool remain excluded from nesting. Launch depth is
persisted in the agent meta sidecar and restored on resume (including
deferred-approval continuations and in-process AgentInteractive frames)
so a resumed nested agent cannot regain spawn capacity.
See knowledge/qwen-code/design/nested-subagents.md.
* feat(cli): display nested sub-agents as a tree in the TUI
Render nested agents depth-first with indent + dim '↳' in the live agent
panel and background tasks view; promote orphaned children to root with a
'· from <parent>' annotation. Detail view gains a level badge, Parent
breadcrumb, and Sub-agents section. The [blocking] tag and two-step cancel
confirm now apply only to provably user-blocking foreground chains. Parent
completion summaries carry a '· N sub-agents' tail (guard-rejected spawns
now record as failed tool calls so the count stays honest). Also fixes the
live-panel Enter-for-detail order mismatch by sharing one display order
between the panel render and the composer keyboard mapping.
* fix(core): address round-1 review on nested sub-agent spawning
- Derive launch metadata (hooks, spans, task rows, meta sidecar) from the
resolved subagent config instead of the raw requested type, so a fork
request that falls back to the awaitable path no longer reports "fork".
- Pin the blocked-spawn failure contract in tests: error is set and
returnDisplay.status is 'failed' for both the depth and fork guards; also
document the failure-path routing at buildSpawnBlockedResult.
- Drop source-comment references to private knowledge/ design docs that do
not exist in this repository.
* test: address round-2 review on sub-agent counting and fork fallback
- Exercise the legacy 'task' alias in the scrollback sub-agent count so
the migration-aware name set is covered, not just the canonical name.
- Pin the nested-fork downgrade: a sub-agent requesting a fork falls back
to the awaitable general-purpose subagent even in interactive mode.
- Drop a duplicated 'nesting depth guard' describe block left behind by
the automated base-branch merge (kept the copy with the failure-shape
assertions).
* fix(core): keep actionable guidance in blocked-spawn error messages
The scheduler's failure path sends only error.message to the model and
the scrollback, discarding llmContent. With the terse terminateReason as
the message, a blocked spawn lost its "do the task yourself instead"
instruction, inviting retry loops. Carry the full guidance text in
error.message and keep terminateReason for the display card.
* test(cli): pin the tree indent clamp at depth beyond TREE_INDENT_MAX_LEVELS
Maintainer mutation-testing on the PR found that removing the clamp in
treeRowPrefix survived the suite. Assert a depth-4 row indents 3 levels
(12 spaces), plus the base marker/indent behavior.
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
- Remove nonexistent --all-files/-a and --show-memory-usage flags from the
CLI arguments and headless option tables (no longer defined in the yargs
parser in packages/cli/src/config/config.ts).
- Add the commonly-needed --model/-m flag to the headless options table and
fix the --approval-mode example to use the valid choice auto-edit (the
parser rejects the underscore form auto_edit).
- Drop the stale Meta+Enter alias from the external-editor shortcut; that
chord is bound to NEWLINE, while OPEN_EXTERNAL_EDITOR binds only Ctrl+X.
- Document the model.reasoningEffort setting (set via /effort), which is
exposed in the settings dialog but was missing from the settings reference.
Co-authored-by: Claude <noreply@anthropic.com>
* chore: ignore local worktrees
* docs(channels): design identity and task lifecycle p0
* docs(channels): plan identity and task lifecycle p0
* feat(channels): add identity and task lifecycle metadata
* fix(channels): suppress cancelled tool call lifecycle
* fix(channels): cover loop lifecycle metadata
* fix(channels): harden lifecycle event edges
* fix(channels): finalize cancelled lifecycle before cleanup
* fix(channels): address lifecycle review suggestions
* fix(channels): close lifecycle cancellation races
* fix(channels): separate pending cancel state
* fix(channels): order clear cancellation lifecycle
* fix(channels): suppress loop chunks during pending cancel
* test(channels): use active session in cancel regression
* fix(channels): sanitize lifecycle tool fields
* fix(channels): route shared tool call lifecycle
* fix(channels): preserve pending cancel intent
* fix(channels): preserve responses after failed cancel
* fix(channels): tighten lifecycle cancellation reasons
* fix(channels): close lifecycle cancel races and validate identity config
Address the outstanding review findings on #6105:
- carry a typed reason on ChannelLoopSkippedError and report disabled
loops as 'dropped' instead of 'timeout'
- treat a turn as committed once delivery starts: /cancel re-checks
deliveryStarted after the cancel RPC settles, and neither prompt path
lets a late-settling cancel rewrite a delivered turn into cancelled
(or follow a /clear cancellation with completed)
- tag failed lifecycle events with phase: agent vs delivery
- validate identity/memoryScope shape at config parse time instead of
throwing an opaque TypeError on the first prompt of every session
- guard onPromptStart hooks, pass job.id to loop onPromptStart/End,
append the boundary block after operator instructions, gate
/who + /status identity lines on configured identity/memoryScope,
cache the boundary prompt, and route error logs through
lifecycleError()
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels): keep failed terminal event when cancel settles post-delivery
Self-review follow-up: once delivery started, the catch paths no longer
reconcile a pending cancel — a late-resolving cancel RPC used to flip
cancelled=true there, suppressing the failed emit while the /cancel
handler (seeing deliveryStarted) also declined to emit, leaving a
started task with no terminal lifecycle event at all.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels): hold streamed chunks while a cancel is pending
Chunks arriving while a /cancel RPC is in flight were pushed straight
into the BlockStreamer, which can send a block on a size/paragraph
threshold before the cancel resolves — leaking output a successful
cancel can never recall. Hold the pending-window chunks instead: replay
them (block streaming + text_chunk transcript) when the cancel fails,
discard them when it succeeds. onResponseChunk stays live through the
window so adapter-accumulated display state has no permanent hole on a
failed cancel; adapters gate visible updates on their own stop flags.
Also stop passing the loop job id to onPromptStart/onPromptEnd (and the
/clear eviction path): the hook contract is inbound platform message
ids, and adapters act on them — cards and reactions keyed to a fake id.
Lifecycle events still carry job.id for correlation.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels): defer adapter chunks while cancel is pending
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(core,cli): unified reasoning effort with /effort command
Expose a provider-agnostic reasoning-effort ladder (low/medium/high/xhigh/max) through a new /effort slash command and a global model.reasoningEffort setting. Bare /effort opens an interactive tier picker (and lists tiers in non-interactive/ACP mode); /effort <tier> sets one directly. A single canonical reasoning.effort config is translated and clamped per provider so one control works everywhere:
- OpenAI: reasoning_effort (max clamps to xhigh)
- DeepSeek: flat reasoning_effort (low/medium→high, xhigh→max)
- GLM / z.ai: new adapter flattens nested reasoning.effort to a verbatim reasoning_effort
- Anthropic: output_config.effort, gated per model — Opus 4.7/4.8 and 5.x families pass xhigh/max through; Opus 4.6/Sonnet 4.6 take max only; older clamp to high
- Gemini: thinking_level (adds medium; xhigh/max cap at HIGH)
- Qwen / DashScope: a set effort turns on enable_thinking (no per-tier field yet — the single column to extend when qwen ships one)
Config.setReasoningEffort updates the live config so the change takes effect on the next turn without an auth refresh, and is re-applied across model switches and auth refreshes (including the initial one at startup) so the effort survives /model switches and CLI restarts — even for provider-backed models, whose preset sync would otherwise overwrite reasoning and drop the persisted effort. The model-with-reasoning status line now tracks the effort and updates immediately when /effort changes it.
A new core/reasoning-effort.ts holds the canonical tier list, rank-based clamp, and input normalizer shared across providers and the command.
* fix(core): reject date suffix as Anthropic minor version for effort gating
The version regex parsed the 8-digit date suffix of dated model ids
(e.g. claude-opus-4-20250514 = Opus 4.0) as the minor version, so
atLeast(4, 6)/atLeast(4, 7) returned true and Opus 4.0 was wrongly
granted the xhigh/max effort tiers it does not support, producing an
HTTP 400 on /effort max with no clamp warning.
Cap the minor-version group at one or two digits with a trailing (?!\d)
so a date suffix is not mis-parsed (a bare {1,2} cap is insufficient: it
would still greedily capture '20' from '20250514'). Dated ids that do
carry a minor, like claude-opus-4-7-20251101, still resolve to minor 7.
Adds a regression test asserting effort 'max' clamps to 'high' on
claude-opus-4-20250514.
* fix(i18n): translate /effort command description for all locales
The mustTranslateKeys strict-parity test failed because the new /effort
command description fell back to English in zh and zh-TW. Add the
description string to every locale file so the command is translated like
its siblings and strict-parity locales no longer fall back.
* fix(core): correct DeepSeek effort mapping and adaptive-thinking model parse
Address two review findings on the reasoning-effort path in
anthropicContentGenerator:
- DeepSeek anthropic-compatible output_config.effort accepts only high/max,
but resolveEffectiveEffort passed low/medium through verbatim (only
remapping xhigh -> max), which the endpoint would 400 on. Mirror the
DeepSeek OpenAI adapter (deepseek.ts): low/medium lift to high, xhigh/max
group to max.
- modelSupportsAdaptiveThinking used /claude-(?:opus|sonnet|haiku)-(\d+)-(\d+)/,
so a dated id like claude-opus-4-20250514 (Opus 4.0) parsed the 8-digit date
suffix as the minor version (>= 6), wrongly selecting adaptive thinking and
tripping a server 400. Apply the same date-suffix-guarded regex the sibling
anthropicSupportedEffortTiers already uses, also covering the fable/mythos
families and a bare major (claude-opus-5).
Add regression tests for both.
* fix(core,cli): address /effort review feedback
- anthropic: family-guard the `max` tier on the 4.x branch so haiku 4.x no
longer gains `max` (a server 400); 5.x families still get it via major>=5.
- effort command: when `setReasoningEffort` no-ops because thinking is disabled
(reasoning: false), report that the tier won't take effect yet instead of a
misleading success message (the tier is still persisted for future sessions).
- dashscope: drop the pipeline-injected nested `reasoning` object when
`enable_thinking` is set, so qwen never receives two competing thinking knobs
(mirrors deepseek.ts / zai.ts).
Adds regression tests for each.
* fix(core): collapse empty reasoning to undefined and drop redundant effort re-apply
setReasoningEffort(undefined) now resets reasoning to undefined instead of
leaving a truthy empty object, which the pipeline would emit as wire noise
and which would make 'if (cfg.reasoning)' a false positive.
handleModelChange no longer re-applies the reasoning effort after the
full-refresh path: refreshAuth already captures and restores it, so the
second call was redundant. The qwen-oauth hot-update path keeps its own
re-apply because it never routes through refreshAuth.
* fix(core): log once when Gemini clamps xhigh/max effort to HIGH
Gemini has no tier above HIGH, so /effort xhigh|max silently ran at HIGH
with no trace. Mirror the Anthropic generator's one-time clamp warning via
a per-generator latch so the downgrade is discoverable in debug logs.
* fix(core): warn when GLM model on non-Z.ai host skips reasoning_effort flatten
isZaiProvider routes any glm-* model through this provider, but the
reasoning_effort reshape stays hostname-gated. A self-hosted GLM on a
non-Z.ai hostname therefore kept its nested reasoning.effort unflattened
silently. Keep the reshape hostname-gated (so we never push GLM's flat
field at an arbitrary OpenAI-compatible backend) but log the skip once so
the gap is discoverable.
* docs(core,cli): correct false OpenAI 'max'->'xhigh' clamp claim
The reasoning-effort docs claimed OpenAI adapters clamp 'max' to 'xhigh',
but the default OpenAI-compatible pipeline forwards the tier verbatim and
no such clamp exists. Reword the doc comment and the reasoningEffort
setting description (and the regenerated VS Code schema) to match.
* fix(cli): clarify effort confirmation and improve the effort dialog UX
- The /effort confirmation now states the tier is the requested one and the
effective tier depends on the active provider/model, instead of implying
the shown tier is what reaches the wire (providers clamp per model).
- EffortDialog no longer pre-selects 'high' when no effort is configured;
the cursor starts at the top and a footer notes the provider default is
in effect, so the highlight is not mistaken for the current setting.
- The dialog path now mirrors the slash-command read-back: when thinking is
disabled (setReasoningEffort is a no-op), it surfaces an info message that
the tier is persisted but won't take effect until thinking is re-enabled.
* refactor(core): extract shared parseClaudeModelVersion for Claude effort gating
anthropicSupportedEffortTiers and modelSupportsAdaptiveThinking each parsed
Claude model ids with their own near-identical regex (family list +
date-suffix guard), kept in sync only by a comment. Extract a single
parseClaudeModelVersion helper both consume so adding a new family is a
one-line change with no drift risk between effort tiers and the thinking
shape.
* fix(anthropic): drop manual budget_tokens on models that reject it
Opus 4.7+ and every 5.x family (Sonnet 5, Fable 5, Mythos 5) dropped
manual extended thinking: a request with
thinking:{type:'enabled', budget_tokens:N} returns a 400 on those models,
which require adaptive thinking with output_config.effort instead
(https://platform.claude.com/docs/en/build-with-claude/effort).
buildThinkingConfig honored an explicit reasoning.budget_tokens on every
model, so a preset carrying budget_tokens (with or without /effort) shipped
the manual shape to an adaptive-only model and 400'd. Gate the escape hatch
behind a new modelRejectsManualThinking() predicate so the budget is only
emitted for models that still accept it (Opus 4.5/4.6, Sonnet 4.6, older
4.x, and unknown/unversioned ids); adaptive-only models fall through to
{type:'adaptive'} and let output_config.effort govern thinking depth.
* fix(cli,core): warn on invalid model.reasoningEffort; guard hot-update field set
- modelConfigUtils: surface a startup warning when settings.model.reasoningEffort
holds an unrecognized value (normalizes to undefined and was silently skipped),
so a typo in settings.json doesn't leave /effort with no visible effect.
- config: document that the qwen-oauth hot-update field set deliberately excludes
`reasoning`, which is captured and re-applied via setReasoningEffort() — adding
it to the copied set would mask the effort restore across model switches.
* fix(core,cli): address /effort review feedback
- config: re-apply reasoning effort on the full-refresh model-switch path.
switchModel() wipes modelsConfig.reasoning via applyResolvedModelDefaults
before handleModelChange fires, so refreshAuth's own capture reads undefined;
restore the tier from the live config. Add a regression test.
- gemini: make buildThinkingConfig's effort switch exhaustive (explicit
'undefined' case + never-guarded default) so a new tier is a compile error
rather than a silent map to UNSPECIFIED.
- anthropic: emit a one-time warning when budget_tokens is dropped on models
that reject manual thinking (Opus 4.7+/5.x); reword the version-regex comment
to correctly credit the (?!\\d) lookahead. Add reseller-prefixed and 5.x
effort-gating tests.
- cli/effort: confirm the requested tier in-chat on dialog success (mirror the
slash-command message); add hook tests.
- tests: add dashscope vision-branch effort test and determineProvider z.ai
routing tests.
* fix(core): harden reasoning-effort normalize and correct stale clamp comment
- normalizeReasoningEffort: reject non-string input (e.g. a hand-edited
settings.json with a boolean/number reasoningEffort) instead of calling
.trim() on it and crashing at startup; add regression tests.
- contentGenerator: update the reasoning-effort ladder comment to reflect
the per-model Anthropic tier gating (Opus 4.7+/5.x accept xhigh/max,
Opus/Sonnet 4.6 accept max, older models cap at high) instead of the
outdated blanket 'xhigh/max -> high' description.
Addresses PR review feedback.
* fix: remove stray OpenClaw-Query-Submit gitlink
This branch carries a submodule gitlink (mode 160000, commit be66572)
with no .gitmodules entry. It entered via a merge from an older main
state; current main no longer contains it, so this branch would
re-introduce it on merge, breaking clone --recursive and any submodule
init. Remove it from the index.
* test(cli): add EffortDialog render tests
Adds render/behavior tests for EffortDialog mirroring
ApprovalModeDialog.test.tsx: renders the title and all five tiers,
shows/hides the 'no effort configured' hint based on currentEffort,
and verifies the active Escape handler cancels with undefined while
non-Escape keys do not.
* refactor(anthropic): single-source Claude family list; warn on DeepSeek effort remap
Derive the ClaudeModelFamily union and the model-id regex from one
CLAUDE_MODEL_FAMILIES const so the type and the parser can't drift apart
behind the 'as ClaudeModelFamily' cast.
Emit a one-time debugLogger.warn when the DeepSeek anthropic-compatible
branch remaps a requested effort tier (low/medium -> high, xhigh -> max),
mirroring the real-Anthropic clamp path so a silently upgraded effort is
visible in debug logs.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DragonnZhang <dragonzhang1024@gmail.com>
* feat(serve): support HTTPS/TLS via --tls-cert and --tls-key
Serve `qwen serve` over HTTPS when both `--tls-cert` and `--tls-key`
point at PEM files, instead of plain HTTP. The motivation is mobile /
cross-device access: a LAN IP (`192.168.x.x`) is not a browser secure
context over `http://`, so `getUserMedia` (voice input), WebRTC, and
other secure-context-only APIs are blocked on phones/tablets. Bringing
your own cert (e.g. via mkcert) unlocks them.
Implementation wraps the existing Express app in `https.createServer`
when TLS is configured; `https.Server extends http.Server`, so the
connection cap, address lookup, ACP attach, and graceful-close paths are
unchanged. The plain-HTTP path stays bit-for-bit identical when no certs
are given. The startup banner and loopback same-origin set now reflect
the active scheme.
Scope is TLS termination only — no auto-generation, no ACME. TLS is
orthogonal to the bearer-token gate: non-loopback binds still require a
token. Boot fails loudly if only one of the two flags is given, or if a
cert/key file can't be read, rather than silently downgrading to HTTP.
Closes#6001
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(serve): add HTTPS loopback origins to self-origin and ACP checks
* fix(serve): accept port-less Origin header on default ports 80/443
Per RFC 7230 §5.4, browsers omit the port in the Origin header when it
matches the scheme default (http→80, https→443). The origin checks in
self-origin.ts and acp-http/index.ts always included the port suffix,
so a browser on port 443 sending 'Origin: https://localhost' (no :443)
would fail the loopback origin match.
Add port-less origin entries when the server listens on port 80 or 443,
mirroring the pattern already used in auth.ts hostAllowlist.
* fix(serve): add port-less origins to installSameOriginOriginStrip for RFC 7230 compliance
* fix(serve): validate TLS cert expiry and parse errors at boot
https.createServer starts cleanly with an expired certificate, then every
client handshake is rejected (NET::ERR_CERT_DATE_INVALID) while /health stays
green — a silent outage that's hard to diagnose. Parse the cert with
X509Certificate at boot and fail loud with an actionable message when it's
expired or unparseable. Also wrap createServer so a cert/key mismatch surfaces
a framed error instead of a raw OpenSSL string.
Add tests for the expired-cert guard and the previously-uncovered --tls-key
read error path.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(serve): close TLS validation gaps from review
- Reject not-yet-valid certs (notBefore > now) at boot, symmetric to the
existing expiry guard — same silent NET::ERR_CERT_DATE_INVALID outage.
- Add port-less host entries (default 80/443) to the WS upgrade Host
allowlist, mirroring the REST allowlist (auth.ts) and the Origin checks;
without it every WS upgrade is 403'd when TLS runs on port 443.
- Cover the previously-untested X509 parse-error and cert/key-mismatch
boot branches.
- Docs: include localhost/127.0.0.1 in the mkcert TLS example so the URL
--open rewrites to (127.0.0.1) isn't rejected with CN mismatch.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <noreply@qwen.ai>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
* docs: document model/auth settings, /model --vision, and --safe-mode
Refresh user docs to match the current codebase:
- commands.md: add the /model --vision override (vision-bridge model)
- settings.md: add model.baseUrl, model.sessionTokenLimit, visionModel,
and voiceModel; document the deprecated security.auth.apiKey and
security.auth.baseUrl keys with a pointer to modelProviders
- troubleshooting.md: document the --safe-mode flag for isolating
customization issues
* docs: address review feedback on sessionTokenLimit, safe-mode, deprecation notes
- model.sessionTokenLimit: correct default to -1 (runtime fallback in
core/config.ts) and clarify breach behavior (current send dropped, not
session abort) per client.ts SessionTokenLimitExceeded handling.
- --safe-mode: expand the disabled-customizations list to also cover
permission rules, approval mode overrides, memory features, and sandbox
settings, matching cli/config.ts.
- security.auth.apiKey/baseUrl: align deprecation wording with the existing
tools.* entries (**Deprecated.**) and drop the unsubstantiated
'(slated for removal)' qualifier.
* docs: note QWEN_CODE_SAFE_MODE env var as a safe-mode alternative
Document the QWEN_CODE_SAFE_MODE=true environment variable as an
alternative activation path for safe mode, for cases where the CLI
cannot accept flags (verified against isSafeModeEnv in
packages/core/src/utils/safe-mode.ts).
* docs: clarify model.baseUrl, sessionTokenLimit=0, and safe-mode subagents
- model.baseUrl: describe it as a picker-managed disambiguator, not a
hand-editable override (stale values can misroute to a same-id provider).
- model.sessionTokenLimit: note that 0 is treated as unlimited (same as -1),
unlike model.maxToolCalls where 0 disallows all calls.
- --safe-mode: include custom subagents in the list of disabled
customizations (only built-in subagents load in safe mode).
* docs: clarify sessionTokenLimit semantics and add --safe-mode to headless flags
- settings.md: reword model.sessionTokenLimit to reflect that the gate
compares the last recorded prompt token count before the next send
(not a per-send preflight cap), and that the next send is dropped.
- headless.md: add a --safe-mode row to the CLI flags table so the
diagnostic flag is discoverable there, cross-referencing Troubleshooting.
* docs: align safe-mode sandbox wording to 'sandbox settings'
Safe mode passes an empty Settings object to loadSandboxConfig
(packages/cli/src/config/config.ts:1793), so it strips settings-sourced
sandbox config while the --sandbox flag and QWEN_SANDBOX env still apply.
Match headless.md to troubleshooting.md's accurate 'sandbox settings'.
* docs: correct safe-mode approval-mode wording and align both lists
Safe mode only strips settings-sourced approval mode; the --yolo and
--approval-mode CLI flags are evaluated before the safeMode guard
(packages/cli/src/config/config.ts:1521-1528) and still take effect.
Reword to 'settings-sourced approval mode overrides' and note the CLI
flags in troubleshooting.md and headless.md, and make the enumerated
safe-mode disable list identical (same items and order) across both.
---------
Co-authored-by: Claude <noreply@anthropic.com>