* fix(core): configurable vision bridge timeout + retry with fresh budget
The vision bridge capped image transcription at a hardcoded 30s. On a
slow or proxied vision endpoint one latency spike permanently lost the
image: the retry inside the side query shared the same abort signal, so
a second attempt inherited whatever seconds were left of the first
attempt's budget.
Add a visionBridgeTimeoutMs setting (per attempt; unset keeps 30s,
non-positive values are ignored) and retry a timed-out attempt once at
the bridge level with a freshly created timeout signal. Non-timeout
failures still fail immediately, and user cancellation is still
reported as skipped.
Fixes#6524
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): harden visionBridgeTimeoutMs against invalid timer values
Maintainer E2E review found that fractional or out-of-range values such as 30000.5 and 4294967296 could pass the old number-typed config path and Config's Number.isFinite && > 0 guard. Node rejects fractional AbortSignal.timeout values with RangeError and can degrade oversized timer values to a 1ms timeout, which made image turns fail before any model request.
Tighten the Config guard to positive integers within the supported 32-bit timer ceiling, make visionBridgeTimeoutMs a bounded integer setting so /config and the generated JSON schema reject bad values up front, and move AbortSignal.timeout/any creation inside the bridge try block so any future bad value becomes a safe failure result instead of an escaped rejection. Also mark the setting requiresRestart because it is read once in the Config constructor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Audit findings against the current codebase:
- model-providers.md, auth.md: the documented modelProviders shape used
the reverted `{ protocol, models }` wrapper. The canonical shape is a
bare `ModelConfig[]` array per provider id (a wrapped entry in a
migrated settings file is silently skipped). Update all examples and
prose, document the separate top-level `providerProtocol` map for
custom provider ids, and correct the unknown-key behavior.
- settings.md: correct the default for
`model.chatCompression.screenshotTriggerThreshold` (20, not 50).
- commands.md: add the missing `/reload-plugins` command and note that
`/dream` and `/forget` are registered only when managed auto-memory
is available.
- Add a Computer Use feature page (on-by-default desktop automation via
the cua-driver native driver) and wire it into the features nav and
the qc-helper doc index.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(memory): make background memory agent timeouts configurable
Adds a memory.agentTimeoutMinutes setting that overrides the hardcoded
max runtime of the four background memory agents (extraction, dream,
remember, skill review). Unset keeps each agent's built-in default
(2-5 minutes); 0 disables the time limit entirely.
Local LLM setups load large extraction prompts far slower than hosted
models, so the fixed 2-minute extractor budget times out before the
context even finishes loading — and each retry carries a longer
conversation, making the next timeout more likely.
Fixes#6308
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(memory): address review — wire agentTimeoutMinutes to skill review, clamp negatives, add tests
The auto-skill scheduling path always passed an explicit timeoutMs, so
the new setting never reached the skill review agent; drop the redundant
pass-through so the planner's config fallback applies. Clamp negative
settings values at the Config constructor (schema validation only runs
on interactive edit paths). Add positive override tests for the dream,
remember, and skill review planners, and reduce the settings.md diff to
the single new table row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(memory): cover negative-clamp and remember default-timeout paths
Review follow-up: assert the Config constructor treats a negative
memory.agentTimeoutMinutes as unset, and that the remember planner keeps
its built-in 5-minute default when nothing is configured.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
In compareRankedCommandMatches, recentScore was evaluated before nameVsAlias in the sort chain, causing recently-used alias matches to shadow name matches at the same matchStrength level.
The fix swaps the order so nameVsAlias is checked before recentScore, ensuring that a command matched by its primary name always ranks above an alias match, with recency acting as a tie-breaker only within the same match-type bucket.
Adds a regression test that gives an alias match a recentScore and verifies the name match still ranks first.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(session): bridge broken parentUuid chains instead of truncating history
reconstructHistory walked parentUuid from the newest leaf and stopped at the first missing ancestor, silently dropping every earlier record. A session file with a broken chain (a partial write, or a lost middle segment) therefore lost all history before the break on resume — in both the terminal /resume and the web-shell/ACP replay, which both go through sessionService.loadSession.
Add a shared hardened chain walk (buildOrderedUuidChain) that, on a missing parent, bridges onto the newest still-present earlier connected component (union-find; position-based). It treats /rewind gap children as a barrier and matches the tail's sidechain-ness, so it never resurrects abandoned rewind branches or crosses the main/subagent boundary. sessionService and background-agent-resume now share it.
loadSession returns historyGaps metadata; the terminal /resume and ACP replay render a localized (i18n) visible divider so the recovered halves are not read as contiguous. The bridged child's parentUuid is rewritten (on the aggregated copy) to the bridged record so rebuildTurnBoundaries/rewind re-root correctly.
Read-side only; write-side durability is a separate follow-up.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* qwen: fix CI failure on PR #6502
* fix(session): use non-recovered copy for history gaps with no bridged island
When a missing-parent gap has no earlier island to bridge onto (bridgedToUuid is null), the divider is the first visible item — the previous copy still said "recovered earlier history is shown above" with nothing above it. Emit a distinct notice for the null-bridge case (both terminal /resume and ACP replay go through formatHistoryGapNotice).
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* fix(i18n): add zh-TW translation for the non-recovered history-gap notice
zh-TW is a strict-parity locale, so the new null-bridge notice key must be translated there too (pre-empts a CI strict-parity failure).
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* fix(session): address history-gap review nits (accuracy, lazy maps, docs, tests)
- conversation-chain: gap duration now uses the target island's last-occurrence timestamp; a uuid can span several streamed records, and the first occurrence overstated the gap.
- conversation-chain: build posByUuid/lastByUuid lazily on the first gap (healthy sessions skip them); early-return for a caller-supplied leafUuid not backed by any record.
- resumeHistoryUtils: reorder createHistoryGapItem so the convertToHistoryItems JSDoc documents its own function again.
- HistoryReplayer: add tests covering the gap-notice replay path (notice emitted before the gap child; none when there are no gaps).
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* fix(acp): thread historyGaps through the qwen/session/loadUpdates replay path
collectHistoryReplayUpdates now accepts gaps from both callers; the loadUpdates ACP surface passed only records, so a bridged (recovered) history was rendered contiguous there with no gap divider. Adds a loadUpdates test asserting the gaps reach the replayer.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* fix(session): detect-and-mark broken history chains instead of stitching
Read-side, a record whose parentUuid is physically missing is indistinguishable from a lost /rewind marker — where the "earlier" turns are ones the user deliberately discarded. Speculatively stitching the nearest earlier island back on (the previous approach) could therefore resurrect deleted content (wenshao's [Critical]). This mirrors claude-code, which never guesses: it only reconnects across a gap when durable metadata (snip removedUuids, compact re-root) proves it safe, and otherwise truncates.
Replace the connected-component bridging with detect-only: on a missing parent the walk stops (as it always did) and records a HistoryGap, so the terminal /resume and ACP replay surfaces show a visible "earlier history was lost and could not be recovered" marker instead of silently truncating. No earlier records are reconstructed. Renames the option bridgeGaps -> detectGaps, drops the bridgedToUuid/approxLostMs fields and the now-unused "recovered above" i18n copy, and adds a regression test where the rewind marker is missing and the discarded branch must not be restored.
True recovery (keeping the earlier island) requires durable write-side metadata and is left as a follow-up.
* refactor(session): correct stale gap comments and dedup gap indexing
The detect-only rewrite (13c613c9) left doc comments that still described
the removed stitching path — claiming the earlier history was "bridged" or
"stitched back on". Reword them to match the actual behavior: the break is
detected and marked, the lost segment is not recovered.
Also extract the duplicated gap-by-child map construction (identical in both
HistoryReplayer.replay and resumeHistoryUtils.convertToHistoryItems) into a
shared indexGapsByChild helper alongside formatHistoryGapNotice — the one
still-applicable item from the review suggestion summary.
Comments + one small refactor only; no behavior change.
* fix(session): reset pending @-command state at a history-gap divider
Belt-and-braces for the resume renderer: when convertToHistoryItems emits a
history-gap divider it already flushes the pending tool group; also clear
pendingAtCommands so an unconsumed pre-gap at_command can never be shift()-
paired with the post-gap user turn (which would attach @file reads to a turn
the user never wrote them on).
In the current detect-only design reconstructHistory truncates to the tail
island — the gap child is always the first replayed record, so the buffer is
already empty at the divider and this cannot trigger. The reset keeps the
invariant if that ever changes. Adds a regression test at the
convertToHistoryItems boundary.
* fix(session): don't detect history gaps on the background-agent resume path
Addresses wenshao's review: this non-interactive transcript recovery has no
surface to render a gap marker on (unlike interactive /resume via sessionService
and the ACP replay via HistoryReplayer), so passing detectGaps: true only to
emit a debugLogger.warn and then drop the gaps was an inconsistent half-measure
("half-detection is worse than no detection"). Turn detection off here — the
walk truncates at a broken parent link either way, matching this path's
historical behavior. Gap surfacing stays exactly on the two paths that have a
UI for it.
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Split-view panes showed English descriptions for slash commands while
the main view showed Chinese. Two root causes:
1. ChatPane never merged getLocalCommands(t) into the command list,
so ~33 built-in commands (help, model, clear, etc.) lacked i18n
descriptions.
2. localizeBuiltinDescriptions required source === 'builtin-command',
but the daemon omits _meta.source in some SSE event paths
(available_commands_update), causing built-in commands to skip
translation unpredictably across sessions.
3. Skill localization depended on connection.skills, which can be
empty when SSE events deliver commands without availableSkills.
Fix: make the entire localization pipeline name-based and
session-independent — merge local commands, relax the source guard
to also translate when source is missing, and use skillDescriptionKey
directly instead of connection.skills for skill tagging.
Also adds missing autofix skill translation (EN + ZH).
/new (alias of /clear) silently did nothing when typed right after
cancelling a request, for two stacked reasons:
- hasBlockingBackgroundWork() gated on the registry's
hasUnfinalizedTasks(), which counts cancelled-but-not-yet-finalized
entries. That clause exists for the headless holdback loop (every
task_started must pair with a task_notification), but /clear and
session resume abort-and-reset the registry right after the gate —
suppressing that very notification — so blocking on it only made the
switch fail in the window between cancel and finalizeCancelled().
The gate now keys off a new BackgroundTaskRegistry.hasRunningTasks(),
which counts only entries still actually executing; the headless
holdback keeps using hasUnfinalizedTasks() unchanged.
- When genuinely blocked, interactive mode showed only a transient
debug line while non-interactive returned a proper error. Interactive
now returns the same visible error message, so a blocked /clear no
longer looks like a no-op.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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>
* fix(cli): fixed-width elapsed time below one minute to stop status-line jitter
The loading indicator ticks at 0.5s resolution, so the time string
alternated between forms like "1s" and "1.5s" every tick. The changing
width shifted everything after it on the status line twice a second,
making it distracting and hard to read. Render one fixed decimal below
the minute mark ("1.0s", "1.5s"); the >=1m path is unchanged.
Fixes#6402
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(cli): cover the 0s timer-start frame in the fixed-width time test
Review follow-up: useTimer initializes and resets at exactly 0, so
assert the "(0.0s · esc to cancel)" frame too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(core): add working_dir to the Agent tool for pinning subagents to an existing worktree
Add an opt-in working_dir parameter to the Agent (sub-agent) tool that pins a sub-agent's entire working-directory context to an existing, caller-owned git worktree of the current repository. Unlike isolation: 'worktree', the harness neither provisions nor removes the directory — the caller owns its lifecycle — so a sub-agent can be aimed at a worktree that some earlier step already prepared.
Every "where am I?" surface on the sub-agent's config (target dir, cwd, project root, file discovery, workspace context) is rebound to the worktree, reusing the existing worktree-isolation rebind, so the sub-agent's file and shell tools operate inside that directory and cannot leak into the parent project tree. The path is validated as a worktree registered against the current repository before use, and working_dir is mutually exclusive with isolation.
Wire the /review skill to pass working_dir to every review agent for same-repo PR reviews, so the PR worktree isolation is enforced deterministically at the agent boundary instead of by prompt convention. This makes reviewing multiple PRs concurrently safe.
* fix(core): harden working_dir validation from review feedback
- Reject a working_dir that resolves to the repository's own main working tree. getRegisteredWorktreeBranch accepts the primary checkout too, so `working_dir: "."` or the repo root would otherwise pin a sub-agent to the user's main tree and silently defeat the isolation. A linked worktree has a `.git` file; the main tree has a `.git` directory — require the former.
- Reject working_dir combined with run_in_background. The caller owns the worktree lifecycle and could remove it while a detached agent is still running in it (ENOENT on its own cwd); the isolation: 'worktree' path does not have this problem because the tool owns and reaps the worktree.
- Add an isGitRepository() preflight so a non-repo parent reports the real cause instead of a misleading "not a registered git worktree" error.
- Add tests: repo-relative working_dir resolution (the /review production form), main-working-tree rejection, and the run_in_background guard.
* fix(core): make main-worktree detection robust and tighten working_dir validation
Address a second review round on the Agent tool working_dir parameter.
- Replace the ".git is a file ⟹ linked worktree" heuristic with a reliable check: a linked worktree's per-worktree git dir (--absolute-git-dir) differs from the common git dir (--git-common-dir), while a main working tree's are identical. The heuristic was wrong for a main tree whose .git is a file (git clone --separate-git-dir, submodules), which would have let the main checkout pass the isolation guard. Extracted as GitWorktreeService.isLinkedWorktree(); the fs.stat probe (and its all-errors catch) is gone.
- Reject whitespace-only working_dir (trim before the empty check), matching the worktree-name validation.
- Tests: assert the sub-agent's getCwd()/getWorkingDir() are rebound (not just project root / target dir); cover the monorepo-subdirectory re-anchor branch (getTargetDir() below the repo root); and add real-git coverage for isLinkedWorktree, including the separate-git-dir main tree.
* fix(core): reject working_dir for named teammates
A named teammate spawns via TeamManager with cwd = getCwd() and returns before the working_dir rebind is reached, so the pin was silently ignored and the teammate ran in the parent working tree — a false sense of isolation. Reject working_dir at the point a teammate would actually spawn (name + active TeamManager + top-level session), leaving the name-without-active-team fallback (which spawns a normal sub-agent, where working_dir does apply) untouched. Add a test.
* docs(core): describe working_dir as a cwd pin, not a sandbox
The working_dir parameter description, the interface doc, and the /review skill said the sub-agent "cannot touch"/"cannot leak into" the parent tree. That overstates it: file and shell tools still accept absolute paths, so the isolation is a working-directory pin (cwd-relative operations and search tools stay inside the worktree), not a filesystem sandbox — the same limitation as isolation: 'worktree'. Reword all three to state this accurately.
* test(core): cover working_dir non-git and fail-closed paths; clarify main-tree error
- Add an execute-level test for the working_dir isGitRepository() preflight (non-git parent directory -> "not a git repository").
- Add a test for isLinkedWorktree's fail-closed catch (a non-git path returns false).
- Reword the main-tree rejection error to name both possible causes ("is the main working tree, or its git metadata could not be read"), since isLinkedWorktree also fails closed on a git error rather than only on the main tree.
* fix(core): canonicalize path in isLinkedWorktree; test relative working_dir in a monorepo
- isLinkedWorktree now realpaths the worktree path before comparing --absolute-git-dir (which git returns canonicalized) against --git-common-dir. Without this, a symlinked input (macOS /var → /private/var, os.tmpdir()) made the two diverge for the main working tree and misreported it as linked, letting the main checkout pass the isolation gate.
- Add a test proving a repo-relative working_dir resolves against the subdirectory cwd where fetch-pr actually creates the worktree (git resolves relative worktree paths against cwd), not the repo root — the production monorepo form.
* fix(core): close working_dir gaps from review — classifier, background config, review-workflow coverage
- Include working_dir in toAutoClassifierInput so AUTO-mode permission classification can see the child is rebound to another worktree; a launch could otherwise look benign from subagent_type + prompt alone.
- Reject working_dir when the effective background decision is true — not only for the explicit run_in_background param (already rejected in validateToolParams) but also for a subagent config with background: true, which validateToolParams cannot see. Guard on the resolved shouldRunInBackground so a nested call that downgrades to the foreground is not over-rejected.
- /review skill: extend the working_dir mandate to the Step 4 verification agent and Step 5 reverse-audit agents, which also read files and re-check the diff and would otherwise run against the user's main checkout.
- Add tests for all three.
* test(core): symlink + preserved-suffix coverage; soften working_dir grep/glob wording
- isLinkedWorktree: add a test that passes a symlinked path (the macOS /var → /private/var case, reproduced with an explicit symlink) so the realpath canonicalization is actually exercised.
- Assert a caller-owned worktree run does not emit a spurious "[worktree preserved]" suffix, guarding the externallyManaged cleanup skip against regression.
- Soften the working_dir description: grep/glob default to the worktree as their root but can still be pointed outside via an explicit path, so drop the "enforce it as the workspace boundary" claim.
* fix(core): validate working_dir against the authoritative git worktree registry
- Replace the --git-dir vs --git-common-dir "is it a linked worktree" heuristic with an authoritative check against `git worktree list`. The heuristic could be fooled by a hand-crafted directory carrying a .git file copied from a real linked worktree (structurally a linked worktree, yet not registered); `git worktree list` reads .git/worktrees/<name>/gitdir and excludes such a fake. Renamed isLinkedWorktree -> isRegisteredLinkedWorktree.
- Tests: reject a fake worktree (copied .git file); match a registered worktree through a symlinked input path; and a nested-context test confirming a background: true subagent that downgrades to foreground is NOT rejected by the working_dir background guard (guards the shouldRunInBackground keying against regression).
* fix(core): fix flaky nested working_dir test; harden worktree-registry parsing
- The nested background:true test relied on the default '/test/project' cwd, which does not exist on CI, so simple-git threw at construction instead of the test reaching the isGitRepository() path (this is the red CI on the prior commit). Point it at a real, non-git temp directory instead.
- isRegisteredLinkedWorktree: parse `git worktree list --porcelain -z` (NUL-terminated) so a worktree path that itself contains a newline cannot inject a fake `worktree <path>` entry into a newline-split parse; and reject the primary working tree by canonical path so a linked record whose on-disk path is a symlink onto the main checkout cannot smuggle it through.
* fix(core): fall back when `worktree list -z` is unsupported; add newline-injection regression test
- `git worktree list -z` requires Git >= 2.36. On older git the call threw and the surrounding catch rejected every valid caller-owned worktree with a generic "not a registered linked worktree" error. Fall back to the newline-separated porcelain form instead of hard-failing: that parse is only vulnerable to a worktree path that itself contains a newline, which is a far narrower exposure than rejecting every worktree.
- Add a real-git test that creates a worktree whose path embeds "\nworktree <other>" and asserts the injected path is not accepted while the real (awkwardly named) worktree still validates. Confirmed the test fails when the parser reverts to newline splitting, so it genuinely guards the -z parse.
* fix(core): accept detached-HEAD worktrees for working_dir; cover the failure-path cleanup guard
- getRegisteredWorktreeBranch returns null for a detached-HEAD worktree (its branch reads as 'HEAD'), so a legitimate `git worktree add --detach` target was rejected with a factually wrong "not a registered git worktree" error and the authoritative registry check never ran. Make isRegisteredLinkedWorktree the sole gate and read the branch best-effort: branch is unused for caller-owned worktrees anyway, since cleanup short-circuits on externallyManaged.
- Add a test asserting a detached worktree is accepted and the sub-agent is rebound onto it.
- Add a failure-path test: when the sub-agent throws, the finally / outer-catch path runs cleanupWorktreeIsolation() and the externallyManaged guard must leave the caller's worktree intact. Confirmed both new tests fail without their respective fixes, so they genuinely guard the behaviour.
* fix(core): verify a worktree's registry pointer instead of parsing `git worktree list`
Parsing the registry list forced a bad trade: `--porcelain` alone is newline-delimited, so a worktree path containing a newline can inject a bogus entry, while `-z` needs Git >= 2.36 and would reject every worktree on older git. Verify the single path in question instead:
1. `--git-dir` equal to `--git-common-dir` means the primary working tree, which is rejected.
2. The common dir must be this repository's, or the path belongs to another repo.
3. `<gitDir>/gitdir` records the worktree that registry entry belongs to; it must resolve back to the probed path.
Step 3 is what makes it authoritative: a directory holding a `.git` file copied from a real linked worktree does report a per-worktree git dir, but that entry's pointer names the real worktree, not the copy. No list is parsed, so the newline-injection class of bug is gone on every git version and no `-z` (Git >= 2.36) is required. Detached worktrees, symlinked inputs, separate-git-dir main trees, and other repositories' worktrees are all still handled correctly.
Also bump the real-git integration suite's vitest timeouts to 30s, matching the sibling hooks/symlinks suites, to reduce CI flakiness.
* fix(core): contain working_dir to the repo, tailor the pinned-worktree notice, keep the newline test off Windows
- Containment: a registered worktree can live anywhere on disk, and pinning rebinds the child's WorkspaceContext wholesale, so a model-supplied path could silently move the file tools' boundary outside the repository. Require the resolved path to sit inside the repo (canonical comparison, so a symlink cannot straddle the boundary) and say so in the parameter description. isolation: 'worktree' already had this implicitly.
- Prompt: a caller-owned worktree is the code the agent was asked to work on, not a provisioned copy of the parent's tree. Give it a narrow notice instead of the isolation notice, whose "translate the parent's paths" and "re-read what the parent changed" guidance contradicts the caller's own instructions (/review tells its agents not to cd or prefix absolute paths).
- Skip the newline-injection test on Windows: a path component containing a newline is not representable there (the embedded drive letter makes it invalid), so `git worktree add` errors and the windows test job would fail. The injection cannot occur on Win32 either.
- Add a regression test for a stale registry record whose directory was recreated as a plain directory: `git worktree list` keeps such records for months, and `git rev-parse --show-toplevel` inside one resolves to the MAIN checkout. Probing the path itself rejects it.
* docs(core): correct the working_dir helper's doc comment
The comment still credited getRegisteredWorktreeBranch with enforcing "must be a registered worktree". That gate is now isRegisteredLinkedWorktree (plus the in-repository containment check); getRegisteredWorktreeBranch is consulted only for a best-effort branch label and is deliberately not a gate, since it returns null for a legitimate detached-HEAD worktree.
* fix(core): read the worktree registry from the repo, not the candidate directory
isRegisteredLinkedWorktree derived everything from files inside the directory being validated: `<target>/.git` names a git dir, whose `commondir` and `gitdir` were then trusted. All three are candidate-controlled, so a *fabricated* chain (not merely a `.git` copied from a real worktree) could name itself and be accepted even though git's registry holds no entry for it. The docstring's claim that it consulted "git's own registry entry" was therefore wrong.
Read the registry from the repository instead: some `<commonDir>/worktrees/<name>/gitdir` must name the path. Keep a liveness probe inside the path as well: its own `--git-dir` must be that same entry. The two are complementary. The registry answers "is this path registered?" and so rejects a fabricated chain, a copied `.git` file (the entry names the original), another repository's worktree, and the primary working tree, which has no entry of its own. Only the probe answers "is it a worktree right now?" and so rejects a stale `prunable` record whose directory was deleted and recreated as an ordinary directory.
Add a fabricated-chain regression test, and confirm each of the two tests fails when its corresponding half of the check is removed.
* Initial plan
* fix(core): detect non-SSE HTTP 200 responses in OpenAI streaming pipeline
When a gateway/proxy intercepts a streaming request and returns HTTP 200
with a non-SSE content-type (e.g. text/html), the OpenAI SDK silently
produces zero chunks, resulting in a confusing "Model stream ended without
a finish reason" error and an empty interaction log (response: null,
error: null).
Add NonSSEResponseError that is thrown early when the response content-type
is incompatible with SSE (not text/event-stream, application/json, etc.).
The error carries diagnostic metadata: HTTP status, content-type, bounded
body prefix, and request-id header — allowing users and maintainers to
distinguish "empty model stream" from "gateway returned non-SSE page".
Uses the OpenAI SDK's withResponse() API to access the raw HTTP response
headers before consuming the stream iterator. Falls back gracefully when
withResponse() is unavailable (e.g. in mocked environments).
ClosesQwenLM/qwen-code#6465
* fix(core): replace non-null assertion with nullish coalescing in content-type check
* fix(core): address non-sse review feedback
* fix(core): expose non-sse request id alias
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yiliang114 <effortyiliang@gmail.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
* feat(cli): auto-retry next port when serve port is in use
When `qwen serve` or `npm run dev:daemon` encounters EADDRINUSE on the
default port (4170), automatically try the next available port (up to 10
attempts) instead of failing immediately. This allows running multiple
daemon instances side-by-side without manual port management.
- run-qwen-serve.ts: replace single listen() with recursive tryListen()
that retries on EADDRINUSE; --port 0 (ephemeral) skips retry
- daemon-dev.js: pre-scan for an available port via net probe before
spawning the daemon child, ensuring health-poll and web-shell target
the correct URL
- Tests: retry-then-succeed, non-EADDRINUSE immediate-fail, and existing
all-ports-exhausted test all pass (138 tests)
* test(cli): fix EADDRINUSE mock to create fresh server per listen attempt
The existing test reused a single fakeServer across all retry attempts,
accumulating 10+ once('listening') listeners and triggering a
MaxListenersExceededWarning. Create a new server per call to match
production behavior where each tryListen creates a fresh server.
* fix: address review feedback on port retry
- daemon-dev.js: strip IPv6 brackets before probe (ENOTFOUND fix),
add port range/NaN validation
- run-qwen-serve.ts: remove duplicate runtime error listener
(onListening already installs one via removeAllListeners + on)
- tests: add exhaustion (all 10 ports), port 0 EADDRINUSE no-retry,
and stderr retry message assertion (140 tests pass)
* fix: update stale comment referencing removed server.once('error', reject)
* fix: address R2 review — port cap, TLS reuse, exhaustion summary
- daemon-dev.js: cap probe at port 65535, skip probe when user
specifies --port, add --compacted-replay-max-bytes to whitelist
- run-qwen-serve.ts: move https.createServer before tryListen (avoid
recreating TLS context per retry), cap retry at 65535, log summary
"all ports X–Y are in use" on exhaustion
- tests: verify exhaustion summary stderr message
* fix: clear stale listening listeners on httpsServer before retry
* fix(core): omit deprecated temperature param for Claude 4.8+
Claude Opus 4.8 deprecated the `temperature` sampling parameter —
the server returns 400 with "temperature is deprecated for this model"
when it is sent.
Add `modelRejectsTemperature()` version gate (mirroring the existing
`modelRejectsManualThinking()` pattern) and conditionally omit
`temperature` from the request body for models with major > 4 or
4.minor >= 8. Older models and unknown/unversioned ids retain the
previous behavior (default temperature of 1).
Fixes#6519
* fix(core): address review — ternary form, debug warn, 5.x + unknown tests
- Switch from boolean-spread to ternary form for idiom consistency
with the thinking/output_config spreads at L697-698.
- Add a once-per-generator debugLogger.warn when a user-configured
temperature is silently dropped on 4.8+ (latched like effortClampWarned).
- Add test coverage for claude-sonnet-5 (5.x family omits temperature)
and unknown/unversioned model id (keeps temperature) to pin the
major > 4 branch and prevent future narrowing.
---------
Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
POST /session/:id/approval-mode was gated with `mutate({ strict: true })`,
requiring a bearer token even though similar session-scoped routes like
POST /session/:id/model and POST /session/:id/language use `mutate()`
(non-strict). This prevented the Web Shell from setting YOLO approval
mode on daemons without a configured token, showing a confusing toast
error on every new YOLO session.
Relax the approval-mode route to `mutate()` to match the model route,
consistent with the session create/load/resume lifecycle paths that are
also non-strict.
Extend StopInput and SubagentStopInput with background_tasks and crons fields,
and wire existing BackgroundTaskRegistry and CronScheduler snapshots into
fireStopEvent/fireSubagentStopEvent. This enables hook scripts to observe
async work state at agent stop time for status reports, cleanup logic, and
monitoring.
Fields are always present (empty arrays when no active tasks/crons). Data
collection is non-blocking via try/catch to not delay stop hook response.
Closes#6529
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): keep status line on session model
Resolve status line model display drift after fast-model background subagents.
Refs #6512
* test(cli): cover preset status model fallback
* fix(web-shell): i18n for ~43 hardcoded English strings across 15 files
Multi-round audit replaced hardcoded UI strings with t() calls and
added en/zh-CN i18n keys for:
- Session timeline labels, aria-labels, and kind labels (MessageList)
- SubAgent tab labels, tools count, pending/running status
- Auth protocol option labels, placeholders, API key masking
- Voice dictation UI states (VoiceButton)
- Copy tooltip, Runtime badge, N/A, Server/Agent fallbacks
- Mermaid code block labels, image alt text, model switch summary
* fix(web-shell): i18n for missed models placeholder in AuthMessage
* fix(web-shell): invalidate timeline cache on locale switch
Store the t function reference in sessionTimelineCache alongside the
message signature. When the locale changes, t gets a new reference,
triggering cache invalidation and re-generating entries with the new
language labels.
Deferred IDE startup could report a timeout while the underlying connection
promise later completed, leaving internal IDE state inconsistent with the
visible startup failure state.
- Disconnect the IDE client when deferred startup connection times out
- Repeat cleanup if the original connection later succeeds after timeout
- Keep late rejection handling to avoid unhandled promise rejections
- Cover timeout cleanup, late success, and quick rejection cases
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
* fix(cli): charge a streaming table's wrapped height so it doesn't jump
The rendered-height estimator charged a table `2 * dataRows + 5`, assuming
one line per data row. When cells wrap (a wide table — many columns, long
content — on a bounded content width), each row renders taller, so the live
frame briefly exceeds the viewport and ink repaints from the top (a jump to
the top; #6170's clip/commit recover it, so it does not lock, but the jump
is visible).
Charge each row (header + data) by its wrapped height instead: approximate
the column width as an equal share of the content area (TableRenderer
shrinks columns proportionally to fit; an equal share never gives a wide
cell more room than TableRenderer would, so it is a safe upper bound) and
sum the tallest wrapped cell per row plus the inter-row separators and
chrome. For a table that fits, every row is one line and the formula
reduces exactly to the previous `2 * dataRows + 5`.
Only the height estimate changes (shared by the render-side clip and the
incremental scrollback commit); no rendering behaviour changes.
28 estimator tests pass; MarkdownDisplay clip tests unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): charge a wide-terminal table's vertical fallback height
The pending-height estimator mirrored only TableRenderer's WIDTH-based
vertical-fallback trigger, not the maxRowLines one. On a wide terminal a
multi-column table whose cells wrap past MAX_ROW_LINES is laid out
vertically (label: value — much taller), but the estimator charged the
shorter horizontal height, so the live frame overflowed and Ink fell into
its from-top full-redraw path (the scroll-to-top lock).
Model both triggers: compute the tallest wrapped cell (maxRowLines) in the
same pass as the horizontal height, and when it exceeds MAX_ROW_LINES
charge the vertical height instead. The vertical estimate now also wraps
each label:value at contentWidth so a long value that wraps is not
under-counted. maxRowLines uses an equal column share (never wider than
TableRenderer's column), so it is an upper bound and a real vertical
table is never missed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): cap the live pending region at the viewport height (non-VP)
The estimator's source-line slice is the primary bound on the live
(non-<Static>) pending frame, but it is disabled whenever
availableTerminalHeight is undefined — which is exactly what happens when
constrainHeight is off (ctrl-s "show more lines"). A tall pending item, e.g.
a long vertical-fallback table, then renders past the viewport; Ink cannot
update incrementally, clears the terminal and redraws from the top on every
repaint — the scroll-to-top lock.
Wrap the non-VP pending region in an Ink maxHeight={availableTerminalHeight}
overflow="hidden" box as a hard backstop. availableTerminalHeight already
excludes the footer/controls, so the live frame can never exceed the
viewport and Ink never trips clearTerminal. While constrained the estimator
keeps content well under this, so the clamp is inert and only engages on
residual overflow. ShowMoreLines stays outside the clamp (it renders only
while constrained, so the clamp is inert then, and must not be clipped).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): anchor the estimator's vertical trigger to the first row
Mirror TableRenderer's change to decide the horizontal-vs-vertical format
from the header + first data row only (not every row), so the estimator and
the renderer still agree on which format a streaming table uses. Row heights
above continue to sum every row; only the maxRowLines trigger is anchored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): commit a completed table that alone fills the pending budget
The rendered-height-aware incremental commit stalled when a single block
(a long-text table modelled tall / vertical) charged more than the commit
budget on its own. fitPendingSlice returns kept = the block's trailing
blank line, so the safe split boundary sits exactly at keptLines, but the
boundary search started at keptLines - 1 and missed it. The table has no
internal blank line and the blank before it was already committed, so the
search found nothing and broke the loop. Every later block then appended
past keptLines, so the search window never again contained a blank line —
nothing committed until the stream finalized and dumped all remaining
tables at once (the "stream a few tables, pause, then dump the rest" bug).
Start the boundary search at keptLines so a completed over-tall block's
trailing blank is found and the block commits to <Static>. Committing an
over-tall completed block is fine — only the live pending frame must stay
within the viewport.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): don't flash the header while its separator streams in
The forming-table hold-back released the header the moment its separator
line ended with `|` at a column count different from the header — meant to
let a genuinely mismatched separator render as plain text. But a separator
is typed one group at a time and momentarily ends with `|` at every
intermediate count (`| --- | --- |` on the way to seven columns), so the
header flashed as raw `| … |` text on every closed-group frame while the
separator streamed in — a visible strobe for wide (7-column) tables.
A streaming separator only ever gains columns, so treat a mismatch as final
only when it can no longer become valid: it overshot the header's column
count, or a further line has already committed it (it is not the trailing
line). Also hold the header while the separator is still a bare-pipe prefix
(`|`, `| `) before its first dash. The only remaining flash is the
unavoidable one-cell header window (`| Foo |`), indistinguishable from a
single-pipe line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: add a terminal-capture ratchet for the streaming-table scroll-to-top lock
Drives ten wide 7-column tables with ~200-char wrapping cells through the
real TUI via a chunked fake OpenAI server and counts the full-screen clears
(`\x1b[2J\x1b[3J\x1b[H`) the app emits while they stream. Each such clear
resets the terminal scroll position, so it is exactly the "jump to top" a
user hits when scrolling up mid-stream. With the pending-height estimator
fix the count is 0; without it the under-charged frame overflows and the
count is ~300. Ratchet fails if it exceeds 20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): tighten the streaming-table vertical height estimate (review)
Two accuracy fixes to fitPendingSlice from PR review, both reducing residual
under-charge of the vertical fallback:
- Charge each vertical data cell as its rendered `label: value` line (parsing
the header labels once), not the value alone — TableRenderer's
renderVerticalFormat prefixes the header label, so a long label pushed the
wrapped line count higher than the estimator accounted for.
- Only model the vertical layout once a data row exists (`dataRows > 0`). With
just a header + separator, TableRenderer keeps the horizontal header box, so
the estimator no longer charges the shorter 2-row vertical stub for that
transient state on a narrow terminal.
Adds a test where the `label: value` line itself wraps (covering the wrapped,
label-inclusive formula) and a zero-data-row narrow-terminal test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): correct the clamp margin and share MAX_ROW_LINES (review round 2)
- Charge tableClampRows + 2 when the clamp engages: TableRenderer wraps the
height-clamped <Text> in <Box marginY={1}>, so a clamped table renders two
margin rows beyond maxHeight that the estimator was dropping.
- Make TABLE_MAX_ROW_LINES the single source of truth (pending-rendered-height)
and import it into TableRenderer as MAX_ROW_LINES, so the renderer and the
estimator can never disagree on the wrap-to-vertical threshold. Direction is
util→renderer so the pure height module stays free of the React/ink graph.
- Correct the perColWidth comment: an equal column share is exact for uniform
columns but can under-count a heterogeneous table (the renderer shrinks a
narrow column below the share); the MainContent maxHeight backstop is the
hard cap for that residual case.
- Add a horizontal-layout test whose cells wrap within MAX_ROW_LINES, covering
the per-row wrapped contentRows sum (not just the old flat 2*dataRows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): estimator cleanups and stronger tests (review round 3)
- Charge the clamped-table margin: covered by a new test asserting a clamped
table costs tableClampRows + 2 (the marginY the earlier fix added).
- Strengthen the "no stall-then-dump" regression: require all three tables to
have committed (>= 3 items, each table marker present), so a partial stall
no longer passes.
- Reuse headerCells instead of re-parsing the header row inside the loop, drop
the redundant .trim() (splitMarkdownTableRow already trims), and start the
data-row loop at i + 2.
- Refresh the stale TABLE_CHROME_ROWS JSDoc (no longer 2*dataRows+5) and
document the estimator's known under-charge gaps (proportional column widths,
word-aware wrapping, the renderer's post-layout width fallback) that the
MainContent maxHeight backstop is the hard cap for.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(web-shell): restore the full composer in split-view panes
The in-window split view rendered each pane through a deliberately minimal composer: typing "/" opened nothing, and the approval-mode, model, and voice controls were all hidden. Wire each pane's composer to its own session so all four work per pane.
The slash menu lists the pane session's own daemon commands and is submitted through that session, where the daemon executes it server-side — so e.g. /clear clears that pane's session, not the outer chat. The approval-mode and model pickers drive the pane session's own setApprovalMode / setModel actions, and the SDK reflects the change back on the connection. The shared model-visibility filter moves to utils/composerModels so the main chat and split panes hide the same models.
* feat(web-shell): auto-approve pending tool call when a pane switches to yolo
Address review feedback on #6510: switching a split pane to yolo (or auto-edit for an edit tool) now auto-approves a tool call already awaiting approval in that pane — mirroring App's handleSetMode so the shortcut behaves the same as in the single-session chat. The pending approval is read through a ref so the async setApprovalMode resolution targets the approval current at that time, not a stale one.
Also cover the two untested branches of handleSelectMode: the isDaemonApprovalMode guard (invalid mode is rejected without hitting the daemon, reported via onError) and the setApprovalMode async-rejection path.
* fix(cua-driver): migrate Windows install/uninstall scripts to qwen-cua-driver
install.ps1 and uninstall.ps1 were essentially un-migrated from upstream
trycua/cua — binary name was `cua-driver.exe` instead of
`qwen-cua-driver.exe`, repo pointed to trycua/cua, install paths used
upstream vendor names.
Changes:
- Binary: cua-driver.exe → qwen-cua-driver.exe
- Repo: trycua/cua → QwenLM/qwen-code
- Install dir: Programs\Cua\cua-driver\ → Programs\Qwen\qwen-cua-driver\
- Home dir: .cua-driver → .qwen-cua-driver
- Autostart task: cua-driver-serve → qwen-cua-driver-serve
- All URLs and user-facing text updated
- BAKED_VERSION: 0.6.8 → 0.7.0
- Added PATH restart hint after install
- Docs URL in post-install-hints.txt and _install-rust.sh fallback
* fix(cua-driver): correct cua-driver-uia.exe process name in install.ps1
The UIA helper binary ships as cua-driver-uia.exe in release tarballs
(not qwen-prefixed), so taskkill and Get-Process must use the actual
binary name.
* docs(cua-driver): rewrite README with full install/verify/MCP guide
Covers macOS, Windows, Linux installation; verification steps; MCP
configuration for Qwen Code, Claude Code, and Codex; relative-coordinate
mode; uninstall instructions; and platform support matrix.
* fix(cli): show file path in compact tool summary for single collapsible tools
buildToolSummary() previously discarded the description field from
collapsible tools (ReadFile, Grep, Glob, ListFiles), showing only
generic counts like 'Read 1 file'. Now shows the actual file path or
search pattern for single tools, while preserving count format for
batches of multiple tools of the same type. Falls back to count format
when description is unavailable.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): sanitize description in buildToolSummary for error args and ANSI
When a tool call errors, useReactToolScheduler sets description to
JSON.stringify(args) which produces '{...}' blobs. Strip ANSI escape
sequences and reject JSON-looking descriptions so the summary falls
back to the count format instead of rendering raw JSON.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): broaden ANSI stripping and replace newlines with spaces in summary
Strip all common ANSI escape sequences (OSC, charset, CSI, single-byte
ESC) instead of just CSI. Replace all C0 control characters including
newlines with spaces so embedded \n in shell descriptions does not
break the single-line compact summary layout.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* test(cli): update ToolGroupMessage expectations for description-based summary
The buildToolSummary change from 'Read 1 file' to 'Read a.ts' broke
4 assertions in ToolGroupMessage.test.tsx. Update all 5 occurrences
to use the new description-based format.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
---------
Signed-off-by: Alex <alex.tech.lab@outlook.com>
Reject Windows-style absolute and traversal paths in record_artifact workspacePath validation by checking portable slash-normalized path semantics. This keeps artifact metadata aligned with the workspace-relative locator contract while preserving valid relative paths.
Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
Always display the current permission/approval mode in the footer,
including when in the default (Ask permissions) mode. Previously,
non-default modes showed indicators but the default mode showed nothing,
creating ambiguity for users switching between modes.
- Add grey ⏸ badge with 'Ask permissions' text for DEFAULT mode
- Update both main Footer and AgentFooter to render DEFAULT indicator
- Use theme.text.secondary for subtle, unobtrusive styling
- Badge is i18n-aware using existing t('Ask permissions') key
- Other mode indicators remain unchanged
Closes#6496
* fix(web-shell): count daemon sessions in Daemon Status usage dashboard
The Web Shell usage dashboard read usage_record.jsonl exclusively, but only
the TUI /clear path ever writes that file — so daemon / Web Shell sessions and
any un-cleared session (whose usage lives only in the per-session transcripts)
were never counted. Real-world "today" totals could undercount ~20x.
Add core loadUsageHistoryWithLive(): the durable persisted history unioned with
a bounded replay of recent transcripts, deduped by sessionId (persisted wins,
as the authoritative final snapshot). rebuildFromSessionJsonl gains an mtime
window and a skip-set (read from each transcript's first line) so the merge is
incremental and cheap. The daemon /usage/dashboard route and loadUsageDashboard
now use it.
The trailing window (35d) keeps the summary + daily charts exact while bounding
load latency (~1.7s vs ~13s for a full-year replay on a heavy history); older
heatmap cells fall back to persisted data. With no persisted base (fresh
machine / pure Web Shell user) it replays the full history, so the heatmap is
never silently truncated. Read-only: serving the dashboard never writes ~/.qwen.
* fix(web-shell): address review — skip transcripts by filename, add coverage
Skip already-persisted transcripts by their filename (`{sessionId}.jsonl`,
guaranteed by chatRecordingService) instead of opening each file to read the
sessionId from its first line — zero I/O per skipped session on a cache miss.
Add tests: the loadUsageDashboard wiring counts a transcript-only (daemon)
session, the rebuilt-empty (all-persisted) common case, and a corrupt
usage_record.jsonl falling back to a full transcript replay.
Prevent the 'Settings' label from wrapping to a new line when the
sidebar is narrow. Instead, the text is clipped via overflow:hidden
and only the gear icon remains visible.
* feat(cli): skill review dialog — inline preview, open-in-editor, turn-off option
The auto-skill review dialog now shows the staged SKILL.md inline
(sanitized, bounded reads, wrap-aware height cap), opens it in the
configured editor without advancing (with watcher-based preview refresh
so non-blocking GUI editors work), and offers a visible last option to
turn the feature off — effective immediately in-session, persisted at
workspace scope, non-destructive to the pending batch. Bulk options
render only while at least two skills remain. Re-enabling auto-skill
from /memory can resurface a batch put aside by turn-off.
* test(integration): render harness + capture scenarios for the skill review dialog
Browser-free harness that renders the production dialog from source via
an ESM loader hook; its before mode renders the globally installed qwen
(no local fixture — the baseline is what actually shipped, or a loud
failure). Terminal-capture scenarios produce the PR's before/after
screenshots.
* fix(cli): address review findings on the skill review dialog
- Sanitize the model-generated name and description in the dialog header,
same as the preview body — an escape sequence in the frontmatter must not
reach the terminal through the header fields.
- Clamp the preview width to the dialog container cap (min(columns-4, 100),
the same clamp DiffDialog uses) instead of the raw terminal width, which
broke the wrapped-row accounting on terminals wider than ~106 columns.
- Catch settings persistence failures in the turn-off option: surface the
error in the dialog and leave the feature untouched instead of letting the
throw escape the keypress handler.
- Extract the auto-open gate into shouldAutoOpenSkillReview and cover it
with a truth table (turn-off, /memory overlap, re-enable, Esc-dismiss).
- Cover the MemoryDialog auto-skill ON->OFF toggle direction.
- Release the capture harness temp dir with try/finally.
* fix(cli): guard the preview watcher against async errors and event bursts
An FSWatcher 'error' event after attach had no listener, so Node raised
it as an uncaught exception and the global handler exited the CLI.
Consume it and drop the watcher; the blocking-editor reload still works.
Also debounce the watch callback (300ms, same as SettingsWatcher): a
single editor save fires several raw events, and each one re-read the
file and re-attached the watcher.
* test(cli): drop white-box watcher tests, keep the end-to-end refresh test
The prototype-spy scaffolding tested implementation details (listener
registration, synthetic event bursts) and leaned on vite-node interop
quirks. The existing on-disk refresh test already exercises the watcher
path, debounce included.
* fix(cli): sanitize action errors, log preview read failures, cover key guards
- Render actionError through sanitizeMultilineForDisplay: error messages
can embed the staged path, whose basename derives from the
model-generated skill name.
- Log the underlying cause when the preview read fails; all failures
render the same 'Preview unavailable' otherwise.
- Cover Ctrl+O/Cmd+O inertness and Esc dismissal with tests.
- Document that getAutoSkillEnabled() also gates on bare/safe mode.
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(web-shell): rename scheduled tasks "查看历史" to "查看对话"
* feat(serve): bind cron_create durable tasks to dedicated sessions via keepalive
The cron_create tool (core layer) writes durable tasks to disk without a
sessionId because it has no access to the session bridge. The keepalive
loop runs in the daemon process where the bridge IS available, so it
retroactively binds unbound tasks to dedicated sessions — the same flow
POST /scheduled-tasks uses for UI-created tasks. Each unbound task gets:
spawnOrAttach(sessionScope:'thread'), named ⏰ prompt, sessionId written
back to disk. This makes chat-created tasks show "查看对话" with a clock
icon in the session list, matching the UI's "新建定时任务".
* feat(serve): watch tasks file for immediate binding of new cron_create tasks
The keepalive interval is 2-5 minutes, so a chat-created task could wait
that long before being bound to a dedicated session — showing no "查看对话"
link until the next tick. Adding a file watcher (same directory-watch +
debounce pattern the scheduler uses) triggers an immediate tick when
cron_create writes to disk, so the task is bound within ~500ms.
* feat(serve): bind cron_create tasks to current session + ⏰ rename via keepalive
Switch from creating a separate dedicated session to binding the task to
the current chat session (so the first message is already in the
transcript). The keepalive then renames that session to ⏰ prompt — the
core layer can't rename sessions (no bridge access), but the daemon
process can. A Set tracks renamed sessions to avoid repeated
updateMetadata calls. Unbound tasks (legacy/CLI) still get new sessions
via the existing bind path.
* fix(core): keep createDurable() tasks unbound by default
Reverts the auto-binding of durable tasks to the current session in
createDurable(). Binding to a specific session means only that session
can fire the task (#shouldFireDurable), but non-daemon paths (TUI, ACP,
headless) have no keepalive to rehydrate the session after exit — making
tool-created durable tasks go dormant.
The daemon keepalive (bindAndNameSessions) already handles binding
unbound tasks to dedicated sessions with ⏰ naming, so daemon-mode
tasks get the same UX without the regression.
* fix(serve): roll back orphan sessions in keepalive binding + add tests
When bindAndNameSessions spawns a dedicated session for an unbound task
but the subsequent updateCronTasks write fails (or the task was deleted
between read and write), the spawned session was left behind with no
owning task — the next tick would see the task still unbound (or spawn
more orphans). Add rollback: closeSession + removeSession on failure,
matching the POST /scheduled-tasks rollback pattern.
Also add positive test coverage for the new binding paths:
- unbound task → spawn + name + write sessionId to disk
- bound task without ⏰ prefix → named exactly once (renamed Set dedup)
- task vanishes before write → spawned session is rolled back
* fix(serve): add timeout to spawnOrAttach in keepalive binding + test hardening
BZ-D: spawnOrAttach in bindAndNameSessions had no timeout boundary — a
hung spawn would keep running=true and stall all subsequent ticks,
stopping heartbeats/revives for every scheduled-task session. Wrap with
withTimeout (configurable via spawnTimeoutMs, default 30s) and attach a
background handler to clean up late-resolved orphans.
Also generalized withTimeout error messages to include the operation
name, and made spawn timeout configurable for tests.
Test improvements (GPT-5 review suggestions):
- Assert spawnOrAttach payload (workspaceCwd + sessionScope: thread)
- Verify SessionService.removeSession called during rollback
- Regression test: createDurable stays unbound after enableDurable
- Hung-spawn test: tick completes despite non-abortable spawn hang
* fix(serve): keepalive hardening + i18n sync (review suggestions)
- i18n: sync English 'View history' → 'View conversation' to match
Chinese '查看对话'
- Prune renamed Set alongside reviveState when tasks are removed
- fs.watch: clarify null filename handling for Linux (treat as match)
- updateCronTasks: skip .map() when task not found (no-op optimization)
- Add tests: disabled unbound exclusion, naming failure resilience
* 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