* feat(web-shell): show more slash commands with category headers
The slash-command menu capped its visible height at exactly four rows, so
with 40+ merged commands users had to scroll a thin list to find anything,
and the built-in custom/skill/system grouping was only a faint 1px divider
with no label.
Raise the cap to min(12 rows, 40vh) and render the category name as a visible
header at each group boundary (custom / skill / system), keeping the divider
between groups. Sub-command menus are ungrouped and unchanged.
* feat(web-shell): fuzzy-match slash commands and show per-group counts
Typing in the slash menu now fuzzy-ranks commands with the same fzf engine the
TUI uses, so abbreviated input like "mdl" finds "model" and "arf" finds
"agent-reproduce-feature" — substring matching alone could not. An empty query
still browses the category-ordered list; a non-empty query switches to a flat
relevance-ranked list (headers are dropped since results interleave categories).
Each category header also shows how many commands the group holds (e.g. "Skill
commands 28"), so the volume hidden below the fold is visible at a glance.
The fzf index is built once per command set (keyed on the array identity) and
falls back to substring filtering if construction fails.
* refactor(web-shell): address slash menu review feedback
- Extract the section header/divider boundary logic into a pure
`planSlashSectionRows` helper and unit-test it (headers at group
boundaries, first row header without a divider, no repeated headers for
adjacent duplicate sections, per-group counts). This also moves the
section-count computation past the `!anchorRect` early return so it no
longer runs on first render.
- Simplify `--slash-panel-max-height` to a round `min(460px, 45vh)` instead
of a `12 * rowHeight` formula that ignored header/divider overhead and so
showed only ~9-10 rows; the panel now shows ~12-13 rows.
- Log a warning when fzf fuzzy search throws before falling back to
substring matching, so a silent failure is diagnosable.
- Add a completion test for the zero-match case returning null.
* fix(web-shell): encode vision model selection & polish picker
Address Wenshao's review comments on #6209:
[Critical] Encode vision model selection before persisting
- handleVisionModelSelect now strips ACP (authType) suffix and stores
as authType:modelId format expected by core's resolveVisionModelSelection()
- Without this, picker selections silently fail to resolve when the same
model ID appears on multiple providers
[Suggestion] Add currentVisionModel derivation
- Mirror currentVoiceModel pattern so the picker highlights the active
vision model instead of falling back to the main model
[Suggestion] Extract MODE_TITLE_KEY record for exhaustive dispatch
- Replace duplicated 4-way ternary in App.tsx dialog title with a single
Record<ModelDialogMode, string> lookup
- Replace if/else if/else onSelect chain with a handlers record that
would fail at compile time if a new mode is added without a handler
[Suggestion] Add settings.label/description.visionModel i18n keys
- Add to both EN and ZH locales so Chinese users see proper labels
in the Settings dialog
Files changed:
- App.tsx: encoding fix, currentVisionModel, MODE_TITLE_KEY, handlers record
- i18n.tsx: visionModel label + description (EN + ZH)
* fix(web-shell): encode vision model selection & polish picker
Address Wenshao's review comments on #6209:
[Critical] Encode vision model selection before persisting
- handleVisionModelSelect now strips ACP (authType) suffix and stores
as authType:modelId format expected by core's resolveVisionModelSelection()
- Without this, picker selections silently fail to resolve when the same
model ID appears on multiple providers
[Suggestion] Add currentVisionModel derivation
- Mirror currentVoiceModel pattern so the picker highlights the active
vision model instead of falling back to the main model
[Suggestion] Extract MODE_TITLE_KEY record for exhaustive dispatch
- Replace duplicated 4-way ternary in App.tsx dialog title with a single
Record<ModelDialogMode, string> lookup
- Replace if/else if/else onSelect chain with a handlers record that
would fail at compile time if a new mode is added without a handler
[Suggestion] Add settings.label/description.visionModel i18n keys
- Add to both EN and ZH locales so Chinese users see proper labels
in the Settings dialog
Files changed:
- App.tsx: encoding fix, currentVisionModel, MODE_TITLE_KEY, handlers record
- i18n.tsx: visionModel label + description (EN + ZH)
* fix(web-shell): address review comments for vision model picker encoding
- Extract encodeVisionModelForSetting / decodeVisionModelForPicker into
shared utils/modelEncoding.ts so they can be tested in isolation
- Add 17 unit tests covering ACP encoding, colon-bearing IDs, empty
parens passthrough, and round-trip identity
- Memoize modelHandlers record with useMemo to avoid re-allocation on
every model picker click
- Replace dead fallback (?? 'main') — the outer modelDialogMode guard
already ensures non-null, so use an explicit if-guard instead
* test(web-shell): add edge-case tests for model encoding functions
- Add passthrough tests for already-encoded colon format
- Add malformed input tests (bare authType, unclosed paren, double-parens)
- Add leadin-colon malformed input test for decode
- Add empty string passthrough test
- 23 encoding tests passing (up from 17), full suite: 735 passing
* fix: PR #6236 follow-up — vision model encoding + fast model highlight
- decodeVisionModelForPicker: strip \0baseUrl suffix before decoding to ACP
- Remove dead encodeFastModelForSetting (fast picker strips ACP suffix before handler)
- Add currentFastModel derivation + 'fast' branch to currentModelId ternary
- Fix misleading voice handler comment (bare IDs, not ACP)
- Replace unnecessary useMemo on modelHandlers with plain object
Co-authored-by: atlarix-agent <agent@atlarix.dev>
---------
Co-authored-by: Qwen3.6 Plus agent <agent@atlarix.dev>
* fix: avoid vsce secret scanner false positive on regex patterns
Use character class `[b]` instead of literal `b` in Slack token regex
patterns to prevent vsce's secret scanner from detecting `xoxb-` as a
real Slack bot token during VSIX packaging.
The regex semantics are unchanged — `[b]` is equivalent to `b` in a
character class, but the built string no longer contains the continuous
`xoxb-` substring that triggers the scanner.
Fixes#6199
* fix: document vsce scanner regex workaround
* docs: clarify Slack regex workaround
* test(acp-bridge): cover Slack user token redaction
These jobs transitively depend on precheck-pr via authorize. precheck-pr
is intentionally skipped for same-repo PRs (only runs for forks). Without
always(), GitHub Actions' default behavior propagates the skipped upstream
job, causing delay-automatic-review to be skipped even when authorize
succeeds. This breaks the entire review chain for same-repo PRs on
opened/synchronize events.
Fixes PR #5629 review not triggering.
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(web-shell): show the qwen-code version in the sidebar footer
The Web Shell had no visible version. Show the running qwen-code version (from the daemon capabilities) in the sidebar footer, inline with the Settings button so it stays visible without taking its own row.
Render the version consistently wherever it appears:
- Prefix "v" only for a real semver release; a non-semver fallback such as "unknown" is shown as-is, so we never render a bogus "vunknown". Applied to the Web Shell badge and the TUI header.
- Dev builds (scripts/dev.js) now report the real package version instead of the "dev" sentinel, matching scripts/start.js, so the UI shows the actual version (e.g. v0.19.4). DEV=true / NODE_ENV=development remain the signals that mark a dev build.
* test: add readFileSync to node:fs mock in dev.test.js
scripts/dev.js now reads package.json via readFileSync at module load to
report the real CLI_VERSION, but the node:fs mock in dev.test.js did not
export readFileSync, causing vitest to throw "No readFileSync export is
defined on the node:fs mock" and failing the suite.
* fix(qqbot): validate gateway URL protocol to prevent SSRF
- Add validateGatewayUrl(): enforce wss:// protocol, warn on unexpected hostnames
- Integrate into fetchGatewayUrl() return path
- Truncate error body in fetchAccessToken() to 80 chars
- Add 6 tests covering protocol rejection, wss acceptance, and edge cases
* fix(qqbot): atomic state persistence and error log sanitization
State persistence hardening:
- Atomic saveQQState() via tmp+renameSync with disposed guard and unref()
- Atomic flushQQState() with {mode: 0o600} permissions
- Entry type validation in restoreQQState() for chatTypeMap and msgSeqMap
Error log sanitization:
- Wrap all user-controlled data in process.stderr.write() with sanitizeLogText()
- Covering: connect retry, sendMessage errors, state persistence failures,
token refresh, malformed gateway, WebSocket errors, reconnect, and
C2C/group handler error paths
* fix(qqbot): add replyMsgId validation in restoreQQState()
Add type/length validation for replyMsgId entries when restoring from
persisted state, consistent with the existing chatTypeMap and msgSeqMap
input validation filters. Entries must be strings ≤ 128 chars.
* fix(qqbot): add test coverage for restoreQQState filters, atomic writes, and gateway URL validation
* docs(qqbot): update restoreQQState doc and add disposed guard comment
- Update restoreQQState JSDoc: document validation instead of "trusts persisted JSON"
- Add inline comment explaining why saveQQState has disposed guard but flushQQState doesn't
* docs(qqbot): update validateGatewayUrl docstring to reflect TLS enforcement, not SSRF
* fix(qqbot): address wenshao review — hostname rejection, error sanitization, msgSeqMap validation
* fix(qqbot): address PR review — drain body, narrow gateway hostname
- Drain resp.body?.cancel() in fetchAccessToken error to prevent
Undici connection leaks on repeated token failures
- Narrow validateGatewayUrl hostname check from broad Tencent
wildcards (.tencent.com, .tencentcs.com) to only *.qq.com
to prevent attacker-controlled Tencent Cloud API Gateway
domains from passing validation
* test(qqbot): tighten token-error assertion to exact match
* test(qqbot): add connect retry sanitization + msgSeqMap edge-case tests
Add test verifying the final connect() retry sanitizes newline/control
characters in the thrown error message. Add tests for fractional,
overflow, and Infinity values in msgSeqMap restore validation to
prevent regression of the Number.isSafeInteger fix.
* fix(qqbot): address wenshao review round 3 — error preservation, validation logging, URL normalization
* test(qqbot): fix connect retry sanitization assertion — sanitizeLogText preserves readable content, not censor words
* fix(qqbot): address wenshao review — error preservation, validation logging, URL userinfo stripping, test coverage
* fix(qqbot): add Array.isArray guards and replyMsgId drop logging in restoreQQState
Prevent TypeError from .filter() on non-array state values (e.g. object
from partial write). Missing Array.isArray() guard caused all three maps
to be lost on a single corrupted section — now each map independently
validates with both truthiness + Array.isArray() before filtering.
Also add replyMsgId drop-count logging (was missing while chatTypeMap
and msgSeqMap already had it).
* fix(qqbot): drain response body in token error path, sanitize URL TypeError, clean tmp on atomic write failure
Three fixes from wenshao review:
1. fetchAccessToken: cancel unconsumed response body to prevent Undici TCP
socket leak (could exhaust connection pool over hours in long-running daemon)
2. validateGatewayUrl: strip raw URL from TypeError message to prevent
log injection via malformed URL strings
3. saveQQState/flushQQState: unlinkSync(tmpPath) in catch blocks to
prevent orphaned .tmp files when renameSync fails (cross-device, Docker)
* fix(qqbot): test Infinity rejection via 1e999 raw JSON, not JSON.stringify nullification
* fix(qqbot): address PR #6200 review — beforeExit hook, key validation, error clarity
- Add beforeExit hook to flush debounced state on abnormal process exit
(SIGKILL, OOM, crash) when unref'd 500ms timer has pending writes
- Validate map keys (typeof string, ≤256 chars) in restoreQQState filters
alongside existing value validation to prevent non-string key bloat
- Improve gateway hostname error message to include expected domain
* fix(qqbot): address PR #6200 review round — non-object JSON restore, disposed guard, beforeExit dedup, comment fix
* fix(qqbot): hoist tmpPath declaration before try block in saveQQState
* fix(qqbot): update beforeExit JSDoc to accurately describe behavior
* fix(qqbot): drain response body in fetchGatewayUrl error path
The bare `curly: 'warn'` defaults to `'all'`, flagging single-line
guard statements (e.g. `if (!x) return;`). The root eslint.config.js
uses `['error', 'multi-line']` which only requires braces on
multi-line bodies. Align the package config to match, eliminating
10 false-positive warnings in AuthMessageHandler.ts and
settingsWriter.ts.
* 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(cua-driver): sync vendored cua-driver from 0.6.8 to 0.7.0
Upstream 0.7.0 brings action-time modality (get_window_state always
returns AX tree + screenshot, capture_mode retired), honest verification
(every action returns verified/path/effect/escalation), browser page
(CDP) tools, desktop-scope (capture_scope=window|desktop,
get_desktop_state, screen-absolute click/scroll), and Rust-only
consolidation (Swift backend retired).
Cherry-pick patch changes:
- #2025 (X11 synthetic click): retired — superseded by 0.7.0
honest-verification
- #2035 (session revive): retired — superseded by 0.7.0
revive_session/loud-reject/is_session_lifecycle_tool
- #2036 (EAGAIN socket write retry): re-injected — merged upstream
post-0.7.0 tag, EAGAIN os error 35 is not acceptable
- #2021 (null/empty-title windows + resolve_uwp_host_window): retained,
adapted to 0.7.0's new is_listable_top_level and window_title helpers
Shim extensions:
- coord_norm: desktop-scope support — denormalize_args falls back to
screen basis when no window cache (desktop-scope click),
normalize_result handles get_desktop_state, ingest_screen_size reads
get_desktop_state's screen_width/height
- serve.rs: rewrite_coord_desc re-injected at both tools/list sites
Install scripts simplified to Rust-only (Swift backend retired upstream).
Fix platform-linux cross-compilation: cfg-gate wayland::PORTAL_LIBEI_ENABLED
reference in health_report.rs for non-linux hosts.
* fix(cua-driver): distinguish desktop-scope from window cache miss in denormalize_args
When screenshot_w == 0 (no cached window size), check whether the call
has a pid/window_id target before falling back to screen-size basis.
Window-scoped clicks with a cache miss now correctly leave coordinates
as-is instead of mapping them against the screen dimensions.
* fix(cua-driver): use screenshot pixels not logical points for desktop-scope coord basis
ingest_screen_size was reading screen_width/height (logical display
points from CGDisplayBounds) for get_desktop_state, but the model
operates on the screenshot PNG whose dimensions are physical pixels.
On Retina displays (2x) this caused every desktop-scope click to land
at half the intended position.
Read screenshot_width/height instead — the same basis normalize_result
rewrites to 1000. Updated test to use realistic Retina values
(screen 1920, screenshot 3840) to catch 1:1 masking.
* fix(cua-driver): split screen cache into logical (move_cursor) and physical (desktop-scope)
The single screen_cache was being written by both get_screen_size
(logical points, e.g. 1920x1080) and get_desktop_state (physical
pixels, e.g. 3840x2160). Since move_cursor operates in CGEvent screen
points (logical) while desktop-scope clicks operate in screenshot
pixels (physical), whichever tool ran last would corrupt the other's
denormalization basis — causing 2x overshoot or 1/2 offset on Retina.
Split into:
- SCREEN_SIZE: logical points, written by get_screen_size, consumed by
move_cursor (screen_basis=true)
- DESKTOP_SCREENSHOT_SIZE: physical pixels, written by get_desktop_state,
consumed by desktop-scope click fallback (screenshot_w==0, no pid)
Added cross-contamination isolation tests.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
The streaming inactivity watchdog aborted any stream silent for 120000ms,
which breaks large-prompt ingest, /compress reprocessing, and long thinking
phases on local models and reasoning proxies that legitimately emit no SSE
deltas for over two minutes. Raise DEFAULT_STREAM_IDLE_TIMEOUT_MS to 300000
(5 minutes) and append an actionable hint to the timeout error pointing at
QWEN_STREAM_IDLE_TIMEOUT_MS (or 0 to disable).
Refs #5975
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* feat(web-shell): add daemon UI support for vision model selection
Add /model --vision support to the web-shell daemon UI, mirroring the
existing --fast and --voice patterns.
- Add 'vision' to ModelDialogMode type and aria-label
- Add --vision branch to /model handler (dialog + direct set)
- Add handleVisionModelSelect callback
- Wire vision into ModelDialog rendering (title, onSelect)
- Add visionModel to SettingsMessage SUB_DIALOG_KEYS
- Add visionModel to onSubDialog callback
- Update localCommands argument hint
- Add model.setVision i18n keys (en/zh-CN)
* feat(web-shell): add daemon UI support for vision model selection
Add /model --vision support to the web-shell daemon UI, mirroring the
existing --fast and --voice patterns.
- Add 'vision' to ModelDialogMode type and aria-label
- Add --vision branch to /model handler (dialog + direct set)
- Add handleVisionModelSelect callback
- Wire vision into ModelDialog rendering (title, onSelect)
- Add visionModel to SettingsMessage SUB_DIALOG_KEYS
- Add visionModel to onSubDialog callback
- Update localCommands argument hint
- Add model.setVision i18n keys (en/zh-CN)
* Updated .gitignore
Co-authored-by: atlarix-agent <agent@atlarix.dev>
* adding atlarix to gitignore
* chore: remove .atlarix/.atlarixignore from tracking
* chore: revert unrelated .gitignore, package-lock.json, and NOTICES.txt changes
These were local environment artifacts accidentally staged alongside the
/model --vision feature. Reverting to keep the PR focused.
- Restore .gitignore to base state
- Restore package-lock.json (fsevents peer flag, test-utils entry)
- Restore NOTICES.txt (hasown version)
* Updated .gitignore
Co-authored-by: atlarix-agent <agent@atlarix.dev>
* chore: add .atlarix/ to .gitignore
Prevents Atlarix workspace files from being tracked in the repo.
* Updated .gitignore
Co-authored-by: atlarix-agent <agent@atlarix.dev>
---------
Co-authored-by: Qwen3.6 Plus agent <agent@atlarix.dev>
* 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>
* Initial plan
* fix(scheduler): add opt-in per-tool-call execution timeout
Wrap each tool call in CoreToolScheduler with an optional execution timeout, disabled by default (experimental). Enable by setting QWEN_CODE_TOOL_EXECUTION_TIMEOUT_MS to a positive number of milliseconds.
On timeout the tool's derived AbortSignal is fired (cooperative tools stop; the shell kills its subprocess) and the call returns an EXECUTION_TIMEOUT ToolResult error, so a hung tool can no longer block the session indefinitely.
* fix(scheduler): add opt-in per-tool-call execution timeout
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Karataev Pavel <minmax777@gmail.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.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>
When a subagent systemPrompt contains ${hook_context} but no
SubagentStart hook is configured (or the hook returns no
additionalContext), the hook_context key was never set in ContextState,
causing templateString() to throw and the subagent to crash on startup.
Fix by always setting hook_context to an empty string at the hook
injection boundary before the optional hook override, in:
- agent.ts: runSubagentWithHooks and background execution path
- background-agent-resume.ts: applySubagentStartHook and SubagentStop
continue path
- forkedAgent.ts: forked agent execution path
templateString() retains its strict missing-key throw behavior, which
remains useful for catching genuine template typos.
* fix(web-shell): cut mobile session-switch jank (P0)
- MessageList: wrap in memo and gate the O(transcript) session-timeline
signature/entries computation behind rail visibility (container >=
1160px, never true on mobile), so scroll frames and unrelated App
renders no longer rebuild a transcript-sized string
- DaemonSessionProvider: dispatch the replay snapshot before the
providers/commands/context fetches so the transcript paints one
metadata round-trip earlier on session switch; keep catchingUp
cleared once the replay is injected
Refs #6181
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(webui): cover replay resume catchingUp state
* test(webui): assert catchingUp replay state sequence
* fix(web-shell): restore timeline observer bootstrap
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Bare /effort now reliably opens the picker dialog instead of letting Enter auto-select the first completed tier. The tiers are surfaced as a placeholder via argumentHint ([low|medium|high|xhigh|max]) rather than as submenu-like completion entries; typing /effort <tier> still sets one directly.
* feat(cli): add credential redaction for worker stderr forwarding
Add a `redactLogCredentials` function that strips credentials from
worker log lines before they reach the daemon's stderr and log file.
Covers Bearer/QQBot tokens, Authorization headers, common API key
prefixes, env-var secret assignments, URL-embedded credentials, JSON
secret fields, and platform-specific headers (DingTalk).
Integrate the redaction into both stderr forwarding paths:
- ACP children: `createStderrForwarder` now applies redaction in both
the normal flush and 64 KiB forced-truncation code paths.
- Daemon channel worker: change supervisor stdio from `'inherit'` to
`'pipe'` for stderr, add a line-buffered forwarder with redaction,
64 KiB buffer cap, and try-catch to prevent daemon crashes. Wire
`onDiagnosticLine` so worker stderr also reaches the daemon log file
(previously it only went to the daemon's terminal).
Issue: #5976 (V1.5 follow-up)
* fix(cli): align redaction marker and bound URL scheme length
- Change redaction marker from ***REDACTED*** to <redacted> to match
the supervisor's existing convention and avoid double-redaction output
mismatches in tests.
- Bound URL credential regex scheme to {0,31} chars (matching the
supervisor's pattern) to prevent O(n²) backtracking on long strings
of scheme-like characters.
* fix(cli): use daemon-local heartbeat timestamp and split multiline env secrets
Two fixes for unresolved review threads from #6098:
- Heartbeat: use daemon's own `new Date().toISOString()` instead of
reflecting the worker-supplied `message.at` value. Prevents a
compromised adapter from injecting arbitrary data into `/daemon/status`.
- PEM multiline: split multi-line sensitive env values (e.g. PEM keys)
into per-line redaction patterns so individual logged lines match.
The full value is kept as a pattern too for single-line matches.
* fix(cli): address review feedback on credential redaction
- Fix sensitiveEnvValues: change `lines.length > 1` to `> 0` so single
matching lines from multi-line env values are also added as patterns.
- Add hyphens to sk- charset for compound prefixes (sk-proj-, sk-ant-).
- Add github_pat_ (fine-grained PATs) and ghu_ (app user tokens).
- Add ASIA prefix for AWS STS temporary credentials alongside AKIA.
- Update Authorization catch-all comment to accurately describe the
2-token limitation.
* fix(cli): add heartbeat rationale comment
Add comment explaining why heartbeat uses daemon clock instead of
worker-supplied message.at (security: compromised adapter injection).
* test(acp-bridge): add onEnd redaction test and fix lint
Add test verifying credential redaction on partial lines flushed via
forwarder.onEnd(). Suppress pre-existing vitest/no-conditional-expect
lint errors in getAcpMemoryArgs tests (system-dependent heapArg).
* test(acp-bridge): add onEnd redaction test and revert spawnChannel lint
Move the onEnd credential redaction test to logRedaction.test.ts to
avoid triggering pre-existing vitest/no-conditional-expect lint errors
in spawnChannel.test.ts. Revert the spawnChannel test file to its
upstream state.
* fix(test): remove unused eslint-disable directives
vitest/no-conditional-expect is not enabled in this repo's eslint
config. The directives cause CI failure via reportUnusedDisableDirectives
warn + --max-warnings 0.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
* feat(web-shell): add keyboard-nav and IME-safe filter hooks
Two reusable hooks for list-style dialogs:
- useListboxKeyboard: Arrow/Home/End/Enter navigation driven by an active
index, with a "keyboard mode" flag to suppress hover. Yields modified-key
combos (Cmd/Ctrl/Alt/Shift), Home/End in text inputs, and Enter on focused
buttons/links to native handling.
- useFilterInput: IME-composition-safe search state so a filtered list does
not refire on every intermediate pinyin character (commits on compositionend).
* feat(web-shell): DialogShell Escape/backdrop close and focus management
Give every dialog shared, accessible dismissal and focus behaviour:
- Escape closes (guarded during IME composition so it cancels the
composition, not the dialog)
- click on the backdrop closes
- Tab is trapped within the panel, wrapping at both ends
- focus moves into the dialog on open and is restored to the opener on close
* feat(web-shell): overhaul list-dialog interaction and accessibility
Unify interaction across the model, theme, approval, resume, tools, delete,
release and rewind dialogs:
- keyboard navigation via useListboxKeyboard, with a roving highlight that
opens on the current value and does not fight the mouse
- consistent selection visuals: a single roving highlight plus a persistent
"current" accent bar + checkmark; options are role=option divs (no stray
focus ring)
- IME-safe search via useFilterInput; fix Chinese-input jitter in the
resume/delete/release search boxes
- accessibility: role=listbox/option, aria-activedescendant, and aria-selected
bound to the current value rather than the roving highlight
- destructive dialogs keep Enter non-destructive where a confirm button is the
commit (delete/release); rewind confirms on Enter like a single-select picker
Refactors:
- extract shared SessionRow used by resume/delete/release
- rename resume-picker-* CSS primitives to picker-* (they are shared by all
list dialogs, not resume-specific)
Adds regression tests for the model duplicate-current fix, release hover
selection, rewind Enter, the listbox/aria wiring, and the shared hooks.
* fix(web-shell): address dialog interaction review feedback
Follow-up fixes from upstream review:
- DialogShell closes on completed backdrop clicks instead of mousedown, and its
Tab trap now also catches the panel-focused fallback case
- ToolsDialog now has full listbox semantics (ids, aria-activedescendant,
aria-expanded)
- Rewind keeps the roving cursor separate from the confirmed target; Enter only
confirms, and the danger button executes the rewind
- Model/Approval aria-selected now reflects the actual current value; model
highlights stay in bounds when the model list shrinks
- Home/End and modified arrow-key combos yield to native text navigation in
search inputs; Escape yields to IME composition in DialogShell
- Dead picker CSS and duplicate declarations removed; extra regression tests
added for reviewer-raised edge cases
* fix(web-shell): harden shared dialog keyboard and IME handling
* fix(web-shell): tighten dialog shell focus, stacking, and backdrop behavior
* fix(web-shell): align list dialog selection semantics and add coverage
* fix(web-shell): mobile UX — safe areas, overscroll, native-app feel
The Web Shell felt like a regular web page on iPhone:
- white bands at the top (status bar) and bottom (home indicator) when
scrolling on notched devices,
- iOS Safari's rubber-band overscroll briefly exposed the default white
browser canvas,
- 300ms tap delay and the blue tap-flash gave away that buttons were
HTML elements,
- long-pressing UI chrome (hamburger, composer buttons) popped the
browser's "Copy / Look Up" callout,
- tapping the composer auto-zoomed the viewport because the editor
font-size was 14px (below iOS's 16px threshold),
- the soft keyboard pushed the composer off-screen instead of resizing
the content area.
This commit fixes all of the above so the Web Shell reads as a native
chat app on mobile, while preserving accessibility (users can still
pinch-zoom message content and long-press to copy code).
Safe-area / overscroll fixes:
- index.html: add `viewport-fit=cover` so content extends behind the
status bar and home indicator; add a `theme-color` meta; add an
inline script that runs before first paint to read the stored theme
and apply `.theme-dark` / `.theme-light` to `<html>` so the canvas
background matches the app theme from the very first frame.
- main.tsx: add a `useEffect` that keeps `<html>` class and
`<meta theme-color>` in sync when the React theme changes (covers
the `?theme=` URL parameter and in-app toggling).
- standalone.css: set html/body background per theme class and add
`overscroll-behavior-y: none` to suppress the pull-to-refresh bounce
(the chat pane handles its own scroll internally).
- App.module.css: add `padding-top: env(safe-area-inset-top)` and
`padding-bottom: env(safe-area-inset-bottom)` on `.app`. On desktop
and non-notched devices every inset evaluates to 0 so nothing changes.
Native-app feel:
- index.html: add `interactive-widget=resizes-content` to the viewport
meta so the iOS soft keyboard resizes the content area instead of
panning / zooming the viewport.
- standalone.css: set `touch-action: manipulation` on html/body to
remove the 300ms tap delay and disable double-tap zoom (we
intentionally do NOT set `user-scalable=no` / `maximum-scale=1` —
those break WCAG 1.4.4 and iOS 10+ already ignores them); add
`-webkit-tap-highlight-color: transparent` to remove the iOS blue
flash; add `text-size-adjust: 100%` to stop iOS bumping the font
size on orientation change; apply `user-select: none` +
`-webkit-touch-callout: none` to all descendants of html so long
presses on UI chrome no longer trigger the browser's callout menu;
re-enable selection on message content, the CodeMirror editor
surface, and native inputs via a `:where()` rule.
- standalone.css: add an `@supports` + `@media` rule that bumps the
composer / input font-size to 16px on iPhone only, preventing iOS
Safari's auto-zoom on focus while preserving the 14px desktop look.
- MessageItem.tsx: wrap each rendered message in a
`data-user-selectable="true"` div so users can still long-press /
drag-select reply text (the blanket `user-select: none` on
`html *` would otherwise disable selection on message bodies too).
Tested manually on iPhone over `qwen serve --hostname 0.0.0.0`.
Authored-on: Qwen Code Web Shell (mobile) ~(¯▽¯~)~
Signed-off-by: pomelo-nwu <czynwu@outlook.com>
* fix(web-shell): address review feedback — specificity, safe-area, minor fixes
- Fix :where() specificity bug: plain [data-user-selectable] at (0,1,0)
beats html * at (0,0,1), restoring text selection on message bodies
- Add left/right safe-area insets for landscape notch phones
- Replace silent outer catch with console.warn for theme-init script
- Update MessageItem comment to match code (wrapper now always applied)
- Add Android-only comment for interactive-widget=resizes-content
- Use explicit theme class removal instead of stripping all theme-* prefixes
* fix(web-shell): increase mobile font-size specificity, add drawer safe-area padding
- Scope mobile 16px font override under .app (0,3,0) to beat
.editorArea .cm-content (0,2,0) from ChatEditor.module.css
- Add safe-area top/bottom padding to mobileDrawerOpen so sidebar
content stays clear of status bar notch and home indicator
* fix(web-shell): scope font-size override to composer, add dialog selection
- Replace dead `.app` selector (CSS module hashes the class name) with
`#root [data-composer]` at specificity (1,2,0), reliably beating the
EditorView.theme `.cm-content` at (0,2,0). Add `data-composer`
attribute to the editorShell div in ChatEditor.tsx.
- Add `[role='dialog']` and `[role='dialog'] *` to the selection
re-enable block so tool-approval overlays, MCP config, theme picker,
and other portal dialogs remain selectable on mobile.
* fix(web-shell): address review round 2 — drawer, inputs, color-scheme, theme URL
- Exclude mobile drawer from `[role='dialog']` selection re-enable
(drawer gets role=dialog when open, which would undo user-select:none
on sidebar controls)
- Add horizontal safe-area padding to the fixed drawer for landscape
notch iPhones
- Strip `?theme=` from URL via replaceState to prevent bookmarked /
shared URLs from permanently overriding stored theme preference
- Broaden font-size 16px override to cover all text inputs on iPhone
(dialog inputs, sidebar search, etc.), not just the composer
- Add `color-scheme: dark/light` to theme blocks so native form
controls (scrollbars, selection handles, autofill) match the theme
* fix(web-shell): review round 3 — touch-only user-select, iPad auto-zoom, URL params
- Scope `html * { user-select: none }` to `@media (hover: none) and
(pointer: coarse)` so desktop users retain normal text selection in
sidebar, error messages, and toasts
- Replace deprecated `max-device-width` media query with
`(hover: none) and (pointer: coarse)` for auto-zoom prevention,
covering iPad in addition to iPhone
- Strip `?language=` and `?lang=` from URL alongside `?theme=` to
prevent bookmarked URLs from permanently overriding stored preferences
- Fix stale `:where()` reference in comment
---------
Signed-off-by: pomelo-nwu <czynwu@outlook.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(serve): keep skill slash commands available after the ACP child is reaped
`GET /workspace/skills` is answered exclusively by the ACP child — the
daemon has no local SkillManager. `requestWorkspaceStatus` only checks for
an already-live channel (`liveChannelInfo()`, never `ensureChannel()`), so
when no child is running it returns the idle placeholder
(`initialized: false`, empty `skills`).
That is the norm before the first session, and — crucially — again after
the child is reaped on session close, which happens immediately by default
(`--channel-idle-timeout-ms` defaults to 0 = immediate kill). Unlike
`/workspace/providers`, skills have no daemon-local status provider to fall
back on. So once a user has created and closed a session, every subsequent
pre-first-prompt `/workspace/skills` query returns empty, the Web Shell's
slash-command list falls back to the hardcoded built-ins (which omit
skills), and `/rev` stops autocompleting `/review`.
Retain the last skills status a live child produced and replay it while no
channel is live, so skill-backed slash commands keep autocompleting; the
next live query refreshes the cache. `initialized` cleanly separates a real
child answer (always `true`) from the idle placeholder (always `false`).
Completes #6153, which wired the Web Shell to fetch `/workspace/skills` in
the deferred-connect path but could not surface skills the daemon was
unable to answer without a live child.
* fix(serve): enumerate workspace skills locally when the ACP child is unavailable
The cache from the previous commit keeps the last child answer alive across
a reap, but it never warms when the child never answers at all — most
visibly under `npm run dev`, where the on-demand-transpiled child's
`initialize` handshake routinely exceeds the 10s preheat budget, so preheat
times out and no channel ever comes up. `/workspace/skills` then stays empty
until the first prompt, dropping `/review` and every other skill from the
Web Shell's pre-first-prompt autocomplete even though the skills exist on
disk (typing `/review` in full still runs it, since submitting spawns a
session — hence "not in the list, but usable").
Add a daemon-local skills provider that enumerates skills straight from the
filesystem via SkillManager (a lightweight Config shim — no child, no MCP
init), mirroring the existing daemon-local providers-status provider. The
facade falls back to it only after both a live child answer and the cached
last answer are unavailable, so the live child stays authoritative (and
keeps extension-provided skills) while a never-preheated child still yields
the on-disk skills — `/review` included.
* fix(serve): fall back to cached/local skills when the child query throws mid-flight
Addresses review feedback on #6169: the channel can die after
`liveChannelInfo()` returns a valid channel but before the RPC completes, so
`queryWorkspaceStatus` rejects. Previously that exception propagated even
though the cache or the daemon-local provider could still answer. Wrap the
query in try/catch (logging via writeStderrLine, matching
getWorkspaceEnvStatus / getWorkspacePreflightStatus) and treat a mid-flight
failure as "no live child", so the request degrades to the cached last answer
or daemon-local enumeration instead of failing.
* refactor(serve): address review feedback on daemon-local skills provider
- Extract the SkillConfig → ServeWorkspaceSkillStatus mapping into a shared
workspace-skills-mapping module used by both the ACP child's
buildWorkspaceSkillsStatus and the daemon-local provider, so the two skill
listings can't drift; cover it (including the disable-model-invocation
branch) with a unit test.
- Memoize the SkillManager per workspace so repeat queries reuse its in-memory
cache instead of re-scanning every skill level on each call.
- Honor the safe-mode env (isSafeModeEnv, as Config does) instead of hardcoding
isSafeMode to false; keep bareMode off (the daemon never runs `--bare`).
- Log daemon-local enumeration failures via writeStderrLine, matching the rest
of the workspace-service error handling.
* test(serve): cover daemon-local skills error path; guard the facade provider call
- Test the previously-uncovered `buildWorkspaceSkillsStatus` catch branch:
when enumeration fails it returns `{ initialized: false, skills: [],
errors: [{ kind: 'skills', status: 'error', error }] }` and logs to stderr.
Also cover the per-workspace SkillManager memoization.
- Wrap the facade's `workspaceSkillsStatusProvider` call in try/catch so a
throwing injected provider degrades to the idle placeholder instead of
failing the request (matching getWorkspaceEnvStatus / getWorkspacePreflightStatus),
with a facade test for the throw path.
* 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
* docs: add channel lifecycle status adapter design
* docs: add channel lifecycle status adapter plan
* feat(channels): map telegram lifecycle to typing
* feat(channels): map weixin lifecycle to typing
* fix(channels): reset weixin typing state after failed start
* feat(channels): map dingtalk lifecycle to reactions
* feat(channels): add feishu card status labels
* fix(channels): preserve feishu collapsible status labels
* feat(channels): show feishu lifecycle card status
* fix(channels): cover loop lifecycle metadata
* fix(channels): preserve feishu terminal card status
* chore: remove internal task reports from branch
* fix(channels): clear late lifecycle status starts
* fix(channels): harden lifecycle event edges
* fix(channels): clean adapter lifecycle state
* fix(channels): finalize cancelled lifecycle before cleanup
* fix(feishu): keep cancelled card status visible
* docs(channels): add lifecycle status no-op coverage
* fix(channels): address lifecycle review suggestions
* docs(channels): align lifecycle status documentation
* fix(channels): route adapter stop through lifecycle cancel
* fix(channels): close lifecycle cancellation races
* fix(channels): keep first feishu terminal status
* fix(telegram): guard lifecycle typing updates
* test(qqbot): cover lifecycle status no-ops
* fix(feishu): preserve completed card race status
* docs(lifecycle): align status review docs
* fix(channels): separate pending cancel state
* fix(channels): clean adapter lifecycle status edges
* fix(channels): order clear cancellation lifecycle
* fix(feishu): preserve user stop status label
* fix(channels): suppress loop chunks during pending cancel
* test(channels): use active session in cancel regression
* fix(channels): harden adapter cancellation tests
* fix(channels): sanitize lifecycle tool fields
* fix(channels): route shared tool call lifecycle
* fix(channels): preserve pending cancel intent
* fix(channels): preserve pending cancel intent
* fix(telegram): track typing by session
* fix(feishu): preserve stop status during finalization
* 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>
* test(qqbot): keep lifecycle mock in sync
* fix(channels): unify cancel state machine and adapter lifecycle edges
Address the remaining review findings on #6114:
- /cancel now delegates to requestActivePromptCancellation — one cancel
state machine for slash command and adapter stop buttons; the helper
refuses to claim success once delivery started and sanitizes its logs
- share isTerminalTaskLifecycleType from channel-base instead of four
hand-rolled terminal checks
- DingTalk: only attach reactions for message ids seen inbound (loop job
ids no longer trigger doomed emotion API calls), log reaction API
failures, and recall reactions when a session dies
- Feishu: track userStopped on the card state so every wind-down path
renders 已停止生成 after a Stop click, and pass terminal labels through
updateCard's statusLabel param at the remaining baked-text sites
- Weixin: guard the typing .then() against a racing disconnect
- document onTaskLifecycle as the canonical hook (onPromptStart/End are
back-compat), fix TS4111 bracket access in the DingTalk test
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): close self-review findings on adapter lifecycle edges
- DingTalk: track inbound message ids in a capped insertion-ordered set
instead of the TTL-swept dedup map, so a turn queued minutes behind a
long predecessor still attaches its reaction
- Feishu: reset userStopped when the cancel RPC fails (later wind-down
must render the real terminal status), and give handleStop's plain
message fallback the same ---/label shape the strip regex expects
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(feishu): strip every status-label layout from quoted-reply context
Replace the two $-anchored strip regexes in extractCardText with one
line-granular status-block filter. The anchored patterns missed four
layouts the cards actually render: the two-block truncation card
(notice block + label block), terminal labels joined mid-string before
a collapsible panel body, the 停止失败,请重试 label (never in the
alternation), and label-only stopped cards where the divider leads the
text. Tests now assert the real rendered shapes.
Also update the adapter-cancellation suppression test to the new
hold-and-replay chunk semantics from the base layer.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(feishu): ignore loop job ids in prompt hooks
* fix(channels): defer adapter chunks while cancel is pending
* fix(feishu): preserve bare status text in quotes
* fix(channels): release held chunks on failed cancel
* fix(feishu): preserve generated stop fallback status
* fix(telegram): clear typing on dead sessions
* fix(feishu): share status label definitions
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>