Commit graph

454 commits

Author SHA1 Message Date
Shaojin Wen
02b8b0108c
feat(web-shell): show a Channel event timeline (#16106)
* feat(web-shell): show a Channel event timeline

Channels reported what they did only through stderr, the publication audit,
and the inbound task store, none of which the daemon could read. An operator
wondering why a GitHub comment got no reply had to read the daemon log.

Every Channel now appends one bounded journal per instance: the inbound
messages it accepted, the ones it skipped and the machine reason for each
skip, publication outcomes, inbound task transitions, and poll results. The
journal holds routing decisions and identifiers only, never message text; it
rotates at 1 MiB to a single backup, and a write failure is swallowed into one
stderr line so diagnostics cannot break message handling.

The daemon serves the journal through GET /workspace/channels/events and its
workspace-qualified form, with limit, type, channel, and since filters, a
32-file and 2 MiB read bound, and the existing trust and client checks. The
channel parameter repeats once per instance name because a Channel name may
legally contain a comma.

The Web Shell gains a Channel events page and sidebar entry that render the
cross-Channel timeline for the resolved workspace, filtered by Channel and
event type and refreshed every five seconds, gated by channel_management.

Docs: the design in English and Chinese, the serve protocol, and the Channel
user guide. Channel tests isolate QWEN_HOME so journals never land in a real
home directory.

* fix(web-shell): scope Channel events to the selected workspace

Review found four correctness gaps in the event timeline.

The page read the provider's daemon-wide primary cwd, so a Channel started in
a registered secondary workspace could never be inspected. It now offers the
same workspace selection as the management page and passes the selected cwd
to both hooks; changing the workspace resets the Channel filter.

The poll loop captured the adapter's detail before the poll ran, so the first
success had no counters and every later one carried the previous poll's
counters against the current timestamp. The field is now cleared before the
poll and consumed after it settles, on both the success and failure paths.

The 32-file read cap was applied before the Channel filter, which made an
explicitly requested, quieter Channel unreachable once busier peers had newer
files. Candidate files are now narrowed by the requested identities first.

Channel identities were truncated to 80 code points while the management API
accepts 255 UTF-8 bytes, so longer legal names never matched their own filter
and names sharing a prefix became indistinguishable. The identity now keeps
its full legal length; only the UI shortens it for display.

Each fix has a regression test that was mutation-checked against the previous
behavior. Design (both languages) and the Channel user guide now describe the
workspace selection, the poll-detail timing, the filter-before-cap ordering,
and the identity limit.

* fix(web-shell): mask Channel events cached for another query

The workspace selector changes the query without remounting the hook, and the
underlying resource keeps its previous data while a request is in flight or
after it fails. Selecting a second workspace therefore showed the first
workspace's rows under the new selector until the new request succeeded, and
kept them when it failed — including their Channel names in the filter list.

Each snapshot is now tagged with the query that produced it (workspace,
Channel, types, limit) and mismatched data is masked, the same way the Channel
management hook already does. Two hook-level regressions drive the real hook
and resource with a pending and a rejected second-workspace request; both fail
without the tag.

* fix(web-shell): keep Channel event reads off the daemon's critical path

A verification round measured the journal read at the documented cap: one
page poll took ~860 ms of synchronous I/O, an unrelated /health request waited
~790 ms behind it, and four concurrent reads serialized to 3.4 s. The route
runs on the daemon that also serves sessions, so a diagnostic page must not be
able to stall it.

The read is now asynchronous and chunked (64 KiB awaits) under tighter bounds:
256 KiB per journal file, 4 MiB per request, 32 files, freshest first. On the
same 32-file / 32 MiB fixture the read is ~175 ms, a bystander /health stays at
6 ms p50 against a 2 ms baseline, and four concurrent reads finish in ~630 ms.

Two reporting gaps go with it. Anything the bounds clip — dropped files or an
exhausted byte budget — now sets `truncated`, so the page cannot show a
timeline that silently omits whole Channels. And a scope directory that cannot
be read propagates instead of returning an empty list, so the route answers
5xx rather than claiming the workspace's Channels did nothing; a missing
directory still means "nothing written yet".

The page also stops polling while its tab is hidden and reloads when it
becomes visible, so a forgotten tab no longer reads journals every 5 s.

Tests pin the two behaviors the wire depended on but the suite did not:
reading events that survive only in the rotated file, and clamping a limit
above the maximum. Both new mutations are killed, as are the truncation,
unreadable-directory, and hidden-tab guards; a bystander-timer test pins that
the read yields to the event loop.

* fix(web-shell): report a clipped journal tail as truncated

The per-file read window introduced with the asynchronous read is 256 KiB,
but a journal grows to 1 MiB before it rotates. `readTail` skipped the head of
a taller file without telling anyone, so a single busy Channel lost its own
older events while the response still claimed `truncated: false` — the exact
completeness contract the previous commit set out to fix. A 1 MiB journal of
1020 records answered 255 events with `truncated: false` at `limit=500`.

`readTail` now reports whether it started mid-file and the reader folds that
into `truncated`. The request budget is also spent in whole tail windows
instead of a final partial one, so its flag keeps the precise meaning "whole
journals were left out".

Three tests close the gaps this exposed: a journal taller than the tail window
must report truncation even when the returned rows fit the limit; the budget
fixture now uses twenty full-window journals whose combined rows stay under
the record limit, so only the budget can explain the flag; and a journal file
that cannot be read must fail loudly, like the directory case already does.
All four previously surviving mutants — tail flag, budget flag, budget debit,
swallowed file-read errors — are now killed.

* fix(web-shell): bound Channel event read retention

The events read parsed and retained every matching record across all
journals before applying `limit`, so an all-Channels poll on a busy
workspace held far more than the rows it returns. Trim the accumulator to
the newest `limit` once it passes four times that. A dropped record is
older than every kept one and the sort is stable, so the answer does not
change: a 60-scenario differential run against the previous behaviour
produced byte-identical output. Trimming counts as hitting the record
limit, so it sets `truncated` too.

Closes the remaining non-blocking review items as well. The provider
workspace action declares the full query type it already forwards, so the
interface no longer suggests that path cannot filter by Channel. The new
UI strings use the established 频道 label rather than introducing a second
word for Channel beside the existing sidebar entry; the design docs keep
渠道, which is what every other zh-CN design uses. The docs now state the
page's own request size (200) apart from the route default (100), and the
`truncated` comments describe every reason it can be set instead of only
the record limit. The NUL query-key separator is documented as
collision-free: instance names reject control characters, journal names
are sanitized, and a path cannot contain NUL.

* fix(channels): read a journal that fits the budget and keep aligned records

Reserving a full 256 KiB window per journal left a small journal unread
when the remainder was under one window, even when it covered that
journal many times over — coverage lost in exchange for a flag that was
already truthful. Reserve what the journal actually needs instead, using
the size the enumeration already stats, so the budget flag still means
"whole journals were left out" and a 200-byte journal is no longer skipped
beside seventeen full ones.

A tail window that landed exactly on a line boundary also dropped one
complete record, because the first line was discarded whenever the window
started mid-file. Read one leading byte and drop that line only when the
byte is not a newline. Folding the byte into the first chunk keeps the read
to one awaited call per chunk, which is what lets the daemon serve other
requests between them — now pinned by a test that compares the event-loop
turns a tall read takes against a short one over the same six journals. A
synchronous chunk read fails it.

Also records why an unparseable instant cannot satisfy `since` while an
unfiltered read still returns it, and pins both halves.

* fix(channels): collapse repeated reason-filter skips into a counted row

A real end-to-end run — the actual GithubAdapter over HTTP against a local
GitHub-compatible API, a real agent turn, the page in a browser — exposed an
amplification the seeded-journal runs could not. The poll cursor does not
advance while an inbound task is recoverable, so a task that failed and is
still retryable makes the provider return the same notifications on every
poll, and filtered notifications are skipped before any dedupe. Each poll
therefore re-journaled the whole batch: with a 2 s poll and 300 filtered
notifications the journal rotated and pushed the accepted/posted rows out of
the page's 200-row window.

Journal at most 20 reason_filter rows per poll and collapse the remainder into
one row carrying detail.filtered, which the page renders as a count in both
languages. The budget is per poll, so a genuinely new batch is still described
in full. Measured against the real adapter: 15 polls served 4,500 notifications
and wrote 315 rows instead of 4,500, without rotating. Against a stub that
honours both `since` and GitHub's read-state semantics, the redelivery stops
once the batch is marked read and a new batch gets a fresh budget.

The design docs record the mechanism precisely, including that an in-flight
turn cannot cause it because the poll loop awaits pollOnce.

* fix(web-shell): keep one Channel event polling loop across visibility restores

Restoring tab visibility while a journal request was still pending started a
second run. Clearing the timer handle cannot cancel an in-flight run, so both
runs settled, both called schedule(), and only the last timer handle was kept —
the orphan kept polling forever. Ordinary tab switches on a slow response
therefore multiplied journal reads permanently: one overlapping hide/show cycle
left two requests per interval, three cycles left four.

Guard the cycle with an in-flight flag so a restore during a pending request
lets that request finish and reschedule instead of starting a second loop. Both
read paths are time-bounded (the SDK's 30 s fetch default and the action
timeout), so the guard cannot stall polling. The regression test holds every
request pending, overlaps three hide/show cycles, and asserts one request per
interval afterwards; removing the guard reproduces the reported four calls.
2026-11-03 13:42:18 +08:00
Su
ae7901552e
fix(core): settle the loop reminder delivery at the model-acceptance boundary (#15559)
* fix(core): settle the loop reminder delivery at the model-acceptance boundary

takeLoopReminder() cleared the pending map and logged the 'reminded'
telemetry while the ToolResult request was only being assembled. Between
the take and turn.run(), the send still awaits microcompaction and
memory recall and yields pending goal events, so a consumer that stops
the generator there loses the streak's warning (dropPendingReminders
only unmarks still-pending keys) while telemetry credits a delivery the
model never saw.

The delivery is now only prepared at the take: the client commits it
when the first model-accepted event arrives and abandons it — restoring
the pending entries — in the send's finally when the request never
reached the model. A full reset drops a prepared delivery so a late
abandon cannot leak a stale warning into a new prompt. The agent-core
round loop keeps its take-time accounting with an explicit commit since
it has no consumer-visible suspension between the take and the model
call.

Fixes #15537

* fix(core): defer the agent-core loop reminder commit to model acceptance

* fix(core): drop the ineffective pre-round reminder abandons

The three abandonLoopReminderDelivery() calls at the abort/maxTurns/time
checks could not change anything: each is followed by a break out of the
round loop, the detector is method-local, and abandon only moves entries
between two private maps without logging. They made the take-site comment
and the maxTurns control read as if they were what keeps `reminded`
truthful, so drop them and say what the control actually pins — no round
crosses a model-acceptance boundary, so nothing commits.

Also, from the same review round:

- abandon's doc block named the wrong function. A retry drops the streak
  through resetToolCallCount(), which deletes the three hard-coded keys;
  dropPendingReminders() cannot reach entries already taken out of the
  pending map.
- client.test.ts asserted not.toHaveBeenCalled() immediately after
  mockClear(), which cannot fail. Compare the call count with the value
  captured before the send instead, so the "suspended before turn.run"
  premise is actually pinned.
- cover the multi-entry path: commit credits every entry of a delivery and
  abandon restores all of them, not just the first.
- telemetry.md and both languages of the loop-protection design doc now
  say `reminded` is logged at model acceptance, and the "sent, never
  accepted, credited on a later send" case is added to the enumeration of
  not-counted cases.

---------

Co-authored-by: suzhiguo7 <suzhiguo7@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-11-02 00:43:23 +08:00
Shaojin Wen
6e49388c4a
docs(serve): refresh daemon usage guidance (#16056)
Replace obsolete alpha-era deployment and persistence limits with current behavior.
Document authenticated background startup, token sidecar lifecycle, and service-manager choices.

Co-authored-by: verify <verify@local>
2026-11-01 17:01:13 +08:00
Sergei P
6bef5a01c6
feat(web-shell): add "Add to Split View" to session menus (R1 fixes for #14878) (#15956)
* feat(web-shell): add sessions to split view from the sidebar

* fix(web-shell): validate split targets and retain pending additions

* fix(web-shell): report transient split-classification failures per session (#14775)

* fix(web-shell): harden the first split open: race, per-id memo, pending mirror, failure notice (#14775)

* fix(web-shell): disable no-op split adds, clarify the capacity hint, document split (#14775)

* fix(web-shell): fix split classification type errors (#14775)

* test(web-shell): fix split pending-navigation and archived-offer test refs (#14775)

* style(web-shell): prettier-format the split classification memo type (#14775)

* fix(web-shell): adapt controlled pane reveal to prop-derived pane set

The main merge (#14577) derives paneIds from the sessionIds prop in
controlled mode, so the reveal effect could no longer sync local state
(setPaneIds does not exist) and its render-time ref check was dead: the
ref is current by the time the effect runs. Track the previous commit in
an effect-time ref instead. Also restore the test-block closer the merge
dropped from SplitView.test.tsx.

* Fix split-persist e2e spec: assert disabled state for already-open session (bot R2 blocker)

The first smoke test clicked 'Add to Split View' on Session A after it
was already a split pane. The R1-10 gate disables that menu item with
the title 'This session is already open in Split View' (sidebar
splitAlreadyOpen), so Playwright retried a click on a disabled item.
Replace the click with toBeDisabled() + title assertions, reusing the
shape of the six-pane spec in the same file. The no-op guarantees
(pane count, draft, DOM persistence) are kept as-is.

---------

Co-authored-by: luozongmin <994718917@qq.com>
Co-authored-by: Сергей Панченко <s.panchenko@mcplat.ru>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-11-01 16:52:54 +08:00
XIQI
86f1338b86
feat(serve): batch workspace session live-state snapshots (#12513)
* feat(serve): batch workspace session live-state snapshots

* test(serve): include batch live-state capability in integration baseline

* fix(serve): close review gaps in the batch session live-state route (#12513)

Index POST /sessions/live-state in the REST reference's persisted-catalog
row so the docs contract sees the protocol section, give the batch route the
single-workspace route's server-side records (routing-failure warn on
untrusted members, daemonLog error on unexpected bridge failures), make the
mid-batch disconnect guard observable with an AbortController and a
macrotask yield between members, and move the duplicated member-failure,
generation-currency, and bound literals into workspace-route-runtime for
both plural batch routes. Six tests pin the new witnesses.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): align live-state scope docs and pin batch route guards (#12513)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(cli): pin grouped REST doc rows against bare SDK names and column shifts

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(cli): fail closed on grouped REST doc rows and ambiguous SDK qualifiers

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(serve): pin documented live-state batch bounds against silent drift (#12513)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(cli): isolate the suite from the operator's settings home (#12513)

The autofix verification gate runs the packages/cli suite with the host's
real HOME on a machine shared with other jobs. A malformed
$HOME/.qwen/settings.json there (observed content: "X") makes every
operator-settings reader fail closed at once — readOperatorSettingsScopes
is designed to refuse a corrupted policy file — and one such file failed
408 tests across 15 files in the measured gate rejection.

Pin a private empty QWEN_HOME per test-file process in the suite setup
when the environment did not select one, following the file's existing
doctrine for operator-owned environment. Tests that exercise QWEN_HOME
set, stub, or delete it themselves. settings.test.ts keys every
user-settings and .env path off a mocked os.homedir, so it now deletes
an ambient QWEN_HOME explicitly; with any ambient value set, 13 of its
tests fail regardless of this change.

* test(cli): harden the QWEN_HOME suite pin and batch maxima test (#12513)

- Pin on truthiness so an ambient empty QWEN_HOME no longer falls
  through to $HOME/.qwen, matching every production reader.
- Sweep the pinned home from afterAll instead of an 'exit' listener
  that vitest workers never fire (51 leaked dirs on src/config -> 0).
- Restore QWEN_DEBUG_LOG_FILE around loadCliConfig tests so the debug
  logger's fire-and-forget writes stop racing the sweep.
- Witness that the suite never resolves settings from the ambient
  $HOME/.qwen, and assert member shape in the 20-selector maxima half
  so twenty failed members cannot read as twenty snapshots.

* test(cli): keep AppContainer store work off the pinned per-file QWEN_HOME (#12513)

* test(managed-agent): settle fresh group leaders before age-judged sweeps (#12513)

A just-forked process can read in ps with procps's negative-elapsed
wraparound (e.g. 441077234-00:18:40), which parsePsElapsed deliberately
leaves undatable. An undatable leader judges every worker/group identity
'unknown', so an unwitnessed sweep conservatively holds the live process
and rejects unproven — flaking the real-process sweep tests within
milliseconds of the spawn. settledGroupTable now also waits for the
leader's row to be datable, and the four sweep sites that judged a fresh
process without the guard use it.

* fix(cli): keep the QWEN_HOME test pin unconditional and alive for the process lifetime (#12513)

The conditional pin honored an ambient QWEN_HOME, so agent-gate vitest
runs read and wrote the operator's live settings home; the teardown
sweep then deleted the live pin while un-awaited extension-store work
was still reading it, turning Test (ubuntu-latest) red on ENOENT. Pin
unconditionally, keep the pin for the process lifetime, reap stale pins
by age at setup, await the floated extensions-list handler, and add
teardown, pin, reaper, and memory-only-contract witnesses.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): keep the QWEN_HOME teardown witness inside test-setup.ts (#12513)

The previous commit registered the pin teardown witness as a second
setupFiles entry in packages/cli/vitest.config.ts, which deterministic
verification rejects as CI/verification machinery outside the PR
footprint. Revert vitest.config.ts and fold the witness into
test-setup.ts itself: vitest runs root-level afterAll hooks in reverse
registration order, so the hook registered immediately after the pin
runs last among this file's root hooks and still turns every file red
if a teardown sweep of the live pin is reintroduced below it
(mutation-probed). The separate witness setup file is deleted.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(integration): raise the installed distribution case ceiling

The tui-parity installed-distribution darwin-x64 leg failed on the
npx-lifecycle-enabled no-relaunch-fallback case with "Test timed out in
120000ms" (run 40537979533, job 122316223603). The five preceding cases
averaged ~117s on that host, right at the ceiling: startup alone can
spend the whole 90s ready-prompt budget there (#15677) and the /about
retry window is another 90s, so 120s cannot contain a healthy case.

Raise the per-case ceiling to 240s and keep the budget helper's note in
sync.

---------

Co-authored-by: 西岐 <fengqi.mfq@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
Co-authored-by: qwen-code-autofix[bot] <qwen-code-autofix[bot]@users.noreply.github.com>
Co-authored-by: Qwen Autofix <autofix@qwen-code.local>
2026-10-31 21:40:40 +08:00
Kun De
dba7d065ce
feat(web-shell): expose archived export and restore inline (#15872)
* feat(web-shell): expose archived export and restore inline

* test(web-shell): use inline restore in standalone E2E
2026-10-31 01:46:35 +08:00
Kun De
0976826090
fix(web-shell): align no-workspace category header with peers (#15897)
* fix(web-shell): align no-workspace category header with peers

* test(web-shell): preserve archived no-workspace header presentation

* docs(web-shell): show sidebar category header comparison
2026-10-30 08:23:06 +00:00
Shaojin Wen
a1f567a64f
feat(web-shell): extend one-click update coverage and surface manual steps (#15849)
* feat(web-shell): extend one-click update coverage and surface manual steps

* fix(web-shell): keep More menu open for the manual update panel

* fix(web-shell): shape-neutral update guidance and drawer overlay exemptions
2026-10-29 15:21:16 +00:00
Kun De
64e3aa7a18
fix(web-shell): standardize workspace and session display copy (#15315)
* fix(web-shell): standardize workspace and session display copy

* test(web-shell): sync workspace menu text expectations

* docs(web-shell): clarify the scope of display terminology

* docs(web-shell): align remaining display-label references

* test(web-shell): match the renamed new-session action

---------

Co-authored-by: 大知 <dekun.dk@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-10-29 09:05:04 +00:00
Jiankai Xiao
3131fcc230
feat(web-shell): allow embedding hosts to pick workspace directories (#15296)
* feat(web-shell): allow host workspace directory pickers

* docs(web-shell): add host directory picker demo screenshots

* fix(web-shell): preserve focus across host picker handoff

---------

Co-authored-by: Jiankai Xiao <jkxiao1997@gmail.com>
2026-10-29 10:38:04 +08:00
callmeYe
0ecdf0342d
fix(serve): revalidate skill snapshots in the background (#14890)
* fix(serve): revalidate skill snapshots in the background

* test(serve): include snapshot metadata in skills route response

* fix(serve): preserve foreground skill observations

* fix(serve): publish valid slow skill observations

* fix(serve): retain skill snapshots when observations fail

* fix(serve): report failed skill catalogs consistently (#14890)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): trace discarded warm skills errors and pin observation age (#14890)

* fix(cli): tighten skill snapshot cache and settings lock scopes

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): correct preheat contract and skills cache invalidation (#14890)

* fix(serve): evict invalidated skills enumeration and correct route docs (#14890)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): preserve skill toggle order and retained catalog fallback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): require an enumerated skill catalog for deletion (#14890)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: claudeethan <claudecodeethan@gmail.com>
Co-authored-by: Ethan Ye <ethanye@U-463KXYWL-1938.local>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-10-28 05:07:00 +00:00
qqqys
9a41afe0d4
feat: add persistent session Workflow mode (DW-04A) (#15422)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
Managed Agent gate / Select the Managed Agent tier (push) Waiting to run
Managed Agent gate / SDK Java () (push) Blocked by required conditions
Managed Agent gate / Managed Agent gate (push) Blocked by required conditions
pnpm Worktree Smoke / Install (macos-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (ubuntu-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (windows-latest) (push) Waiting to run
Verify Landlock helper / arm64 (push) Waiting to run
Verify Landlock helper / x64 (push) Waiting to run
Verify Landlock helper / Native Linux CLI boundary (push) Waiting to run
Verify Landlock helper / Native Windows settings and streams (push) Waiting to run
* feat(core): persist session Workflow mode and dispatch reminders

* feat(sdk): expose session Workflow mode through native and daemon controls

* feat(cli): add Workflow mode controls and end-to-end coverage

* fix(cli): preserve pasted text when recalling prompt history

* fix(sdk): complete Workflow mode routing and owner-safe replies

* test: verify Workflow mode recording and Mod permission boundaries

* test(sdk): select the secondary workspace ACP mount

* test(cli): assert headless Workflow mode eligibility

* fix(workflow): enforce owner trust for prompt mode writes

* fix(workflow): keep daemon write policy out of MCP metadata

* fix(workflow): preserve retry and restore state across lifecycle failures

* fix(workflow): align ACP retry types with shared loop

* test(workflow): isolate ACP cadence from followup suggestions

* fix(sdk): close query after initialization failure

* fix(cli): serialize workflow mode writes with session switches

* fix(sdk): keep mode errors async and reject pending reinitialization

* fix(cli): filter workflow mode from SSH raw session state

* fix(cli): preserve workflow mode startup failures

* fix(acp): refresh restored session config options

* fix(core): rearm workflow notices after cancellation

* fix(core): rearm workflow notices after failed switches

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-10-28 01:09:37 +08:00
ytahdn
6f53133d6d
feat(web-shell): stream resumable BTW answers (#15709)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-10-27 12:19:00 +00:00
Shaojin Wen
a92b4efe72
feat(web-shell): show /autofix-pr watches and stop them from the Web Shell (#15668)
* feat(web-shell): show /autofix-pr watches and stop them from the Web Shell

The daemon serves the /autofix-pr watches of a workspace's clone, as
`qwen autofix-pr status --no-work` lists them: GET /autofix-pr/watches
for the primary workspace and /workspaces/:workspace/autofix-pr/watches
for a registered, trusted one, advertised as `autofix_pr_watches`. It
finds the clone's main worktree with `git worktree list` and reads the
watches' files only; runStatus takes that root and no session instead of
the daemon's own directory and environment.

The Web Shell shows each watch on the card of its scheduled task and on a
new PR watches page in the sidebar: the PR, where the watch is (watching,
fixing in the background, a fix left behind, not checked recently, the PR
merged or closed, stopped), its pushes, its last check and round, and the
items waiting for a person. For a watch run by a conversation of this Web
Shell, Open session goes there and Stop watch sends `/autofix-pr stop <n>`
to it, as a scheduled task's manual run sends its prompt.

Docs: design Section 6.15 (EN and zh-CN), the user guide, and the serve
protocol.

* fix(web-shell): run no git for the watch listing, and match the gate's stale rule

- The daemon's listing runs no git command but the one that finds the
  clone: runStatus takes `git: false`, which skips the gitfile and
  submodule checks as well as the worktree scan, and the view leaves out
  the worktree path, gitTrusted, worktreeWork and pushTarget. It names the
  clone (`root`).
- State files, gate cursors and fix markers are read without following a
  link and without blocking, judged by what was opened: a FIFO put in
  their place can no longer hold the daemon, or a helper.
- A task is stale as its gate judges it: the watch ended, records no task
  where the task has an id, or records another one, a task with no id
  included.
- The PR watches page reads a locked workspace by its own route, finds a
  watch's task in any workspace it lists, lists a clone's watch once, and
  sends a watch no gate checks to a session here rather than a terminal.
- The rail offers the page only when the daemon lists watches, and the
  view goes back to the chat when it stops doing so.
- Docs follow.

* test(web-shell): drive the PR watches page and a watch's task card in Chromium

Against the mock daemon: the sidebar opens the page, each watch shows
where it is, a watch run elsewhere or by nothing says where to act, and
Stop watch sends /autofix-pr stop <n> to the session that owns it. The
scheduled-task card of the watch shows the same state.

* fix(web-shell): restore a watch's session in its own workspace, and fence the listing

- Open session and Stop watch, and a scheduled task's run and history,
  restore the session in the workspace of the task bound to it: from
  another workspace's conversation the daemon refused the restore. The
  PR watches page finds that task in whichever workspace holds it.
- The watch listing checks its runtime generation before and after it
  asks git: a workspace removed or replaced meanwhile lists nothing and
  answers 503 workspace_runtime_unavailable, on both routes. A failure is
  logged by the daemon.
- A task left behind by a take-over is shown stale on its card, as its
  gate stops it.
- The integration test's baseline capability list names
  autofix_pr_watches.

* fix(web-shell): name the one workspace of a single-project daemon for a watch's session

With one ordinary workspace the PR watches page and the scheduled-tasks
page recorded no workspace for a task, so Open session, Stop watch and a
task's Run now and history, started from a standalone or Live
conversation, restored the session in that context rather than the
workspace. Both pages now name the primary workspace when it is the only
one; its route stays the unqualified one.

* test(web-shell): capture the PR watches page and a watch's task card in the visual preview

No visual scenario answered GET /autofix-pr/watches, so the before/after
preview could not see either surface. The scenario serves three watches
(fixing in the background with an item for a person, run from a
terminal, merged with nothing running it) and the watch's scheduled task,
against the fixed capture clock.
2026-10-27 09:19:50 +08:00
Shaojin Wen
eea44208b8
ci: run every suite but CLI and core in the side test lane (#15570)
* ci: run every suite but CLI and core in the side test lane

The Linux test job's main lane ran the CLI suite and then, one after
another, core, about twenty smaller workspace suites and scripts/tests,
while the side lane (web-shell, then the standalone regression) went idle
long before the CLI suite finished. The main lane now runs only the CLI
and core suites; the side lane runs every other workspace's test:ci, the
standalone regression and scripts/tests, each at half the Vitest worker
cap. Every side-lane suite runs even after another fails.

* ci: print the side lane of a test step killed before its replay

The lane script replays the side lane's log only after the main lane
ends, so a step timeout or cancel lost every suite but the CLI and core
ones. The script now writes that log to SIDE_LANE_LOG when the caller
names one; ci.yml names it under runner.temp, outside the TMPDIR the step
removes, and a failure()/cancelled() step prints whatever is left of it.

The lane test also checks that the main lane's output is present before
it checks its order against the replay, and a comment that still placed
scripts/tests in the main lane now names the side lane.

* ci: keep the lane script's log out of a nested run

scripts/tests runs `npm run test:scripts:unit`, which runs the lane script
again, inside the step that names SIDE_LANE_LOG. The nested run inherited
that name and truncated, replayed and removed the outer side lane's log, so
the outer replay had nothing to print. The script now unsets the variable
once it has read it, and the lane test drives a nested run with a named log
to pin that: the nested run must not see the caller's log, and the outer
side lane's own output must still reach its replay.

The print-step test also pins the `if [ -f "${side_log}" ]` guard around the
cat, which is what keeps that step quiet on an ordinary red test job.
2026-10-27 00:01:58 +00:00
易良
7b69fccd74
feat(tools): let an explicitly configured search backend name its host (#15638)
`classifyDashScopeBaseUrl` accepted a fixed list of DashScope-family
suffixes, so an organization that routes model traffic through its own
gateway had no configuration-only way to point the built-in `web_search`
backend at that gateway: the tool stayed off with an `unknown-host`
notice, and the alternatives were patching the source or dropping the
built-in tool for an MCP search provider (#15299).

Add `tools.webSearch.allowedHosts` / `WEB_SEARCH_ALLOWED_HOSTS`: extra
suffixes matched exactly like the built-in ones (exact host, or any
subdomain of a listed suffix). Empty by default, so behaviour is
unchanged for anyone who does not set it, and the `https:` requirement
still runs first because the side request carries a bearer key.

The list reaches only the two places where the user named the backend:
the `WEB_SEARCH_BASE_URL` path and the `modelProviders` path. Automatic
derivation keeps the built-in list in both of its host tests — the entry
usability check and the auto-eligible host check — so a host nobody
declared is never adopted implicitly and never receives the main
provider's key.

Entries are trimmed, lower-cased, de-duplicated, and entries without a
dot are ignored, so a typo narrows the list instead of widening it (a
bare `com` would otherwise accept every `.com` host). Both rejection
notices now name the setting, which is what a user hitting the gate
needs.
2026-10-26 18:52:12 +08:00
Edenman
98312b31cc
feat(daemon): add cached workspace artifact queries and batch removal (#15594)
* feat(daemon): add cached workspace artifact queries and batch removal

* fix(daemon): Share artifact cache with single-session queries

* feat(daemon): Add artifact library session metadata and streaming

---------

Co-authored-by: 丁炳智 <dingbingzhi.dbz@alibaba-inc.com>
2026-10-26 15:41:39 +08:00
Shaojin Wen
ad8a542013
feat(web-shell): manage extra workspace directories from settings (#15393)
* feat(web-shell): manage extra workspace directories from settings

The CLI's `/directory add` extends a session's workspace context with extra
directories, persists them at workspace scope, and reloads memory. Web Shell
had no equivalent: the settings panel never rendered
`context.includeDirectories`, and no daemon route exposed it.

Add a workspace-scoped REST surface backed by a new ACP control method. The
handler validates the path, applies the delta to the workspace config and every
live session, persists `context.includeDirectories`, refreshes hierarchical
memory when configured, and notifies the model. The Web Shell exposes the list
under Settings -> Workspace and reuses the existing directory dialog for path
input, autocomplete, and the native picker.

The route is trust-gated. SSH, managed, standalone, and experimental
paired-engine daemons do not advertise or serve it. The primary directory
cannot be removed; a session that cd'd into an extra directory is skipped with
a warning, and a directory that disappeared from disk can still be removed from
the list.

* fix(ci): align capability baselines and keep settings keys out of the transcript bundle

`workspace_directories` is a new registered and conditional capability, so the
ordered baseline lists in the unit and integration suites need it at the
registry position, and the conditional drift test needs a predicate branch.

The Web Shell category label no longer uses a `settings.category.*` i18n key.
The transcript build shares the i18n dictionary and deliberately carries no
`settings.` literals; the section title already exists as a non-settings key.

* fix(cli): keep workspace directory edits consistent with persisted settings

Three review findings on the workspace-directory mutation:

- Re-read the workspace settings layer before deriving the next list. The ACP
  child's LoadedSettings snapshot can lag a write made by another owner, so an
  add or remove could overwrite or miss entries already saved on disk.
- Persist with `throwOnWriteFailure` and before touching live roots. A refused
  write now fails the mutation instead of reporting `changed: true` against an
  unchanged file.
- Resolve entries the way runtime configuration does (`~`, relative paths
  against the workspace root, then realpath) for display and matching, while
  persisting the original spelling. A relative entry such as `../first` is now
  removable from the UI, and removal still matches the persisted entry.

* fix(cli): resolve workspace directory entries like runtime settings

The second review round found two remaining P2s and one test-contract gap:

- Read, match, and remove entries from the settings layer with environment
  variables resolved (`workspace.settings`), so a `$VAR` entry is listed as the
  path sessions use and removal clears that path from live contexts. Persist
  the original spelling from `originalSettings`.
- Refuse adding the workspace's primary directory (`primary_directory`); it is
  always part of the workspace and previously persisted as an extra entry that
  DELETE then refused as `initial_directory`.
- Accept relative and `~` removal paths in the handler, not only in the route,
  and test the downstream handler with the relative spelling. The route test's
  bridge mock had masked that the handler still required an absolute path.

* test(cli): canonicalize temp roots in workspace directory tests

The route and handler fixtures composed expected paths from the raw
`os.tmpdir()` spelling. On macOS that is `/var/...` while the implementation
resolves entries through `realpath`, which returns `/private/var/...`, so the
three new cells failed only on macOS.

Realpath the temp root once at setup in both files, so fixture paths and the
implementation's resolved output share the same spelling. No production code
changes.

* fix(cli): list workspace directories through the selected runtime environment

A directory variable defined only in the workspace's `.env` reaches the ACP
child and its settings but not the daemon process. The GET route read settings
with `skipLoadEnvironment`, so it interpolated against the daemon's environment
and listed the unresolved placeholder while live sessions used the real
directory; deleting that displayed path answered 200 with `changed: false`.

Read the workspace scope through a settings snapshot built from the selected
runtime's environment (and its trust), matching what the mutation handler and
its sessions resolve. Cover a variable that exists only in the runtime overlay.

* fix(cli): drop the daemon-environment fallback from the directories list

The process.env guard in `packages/cli/src/serve/**` only permits registered
accesses. Reading `process.env` as the parent-process fallback for the
directories list tripped it and failed the Test lane.

Only a runtime overlay carries a workspace `.env`, so the overlay environment
is the whole source: a runtime without one resolves the persisted list against
an empty environment, which keeps placeholders rather than silently borrowing
the daemon's process-wide variables.

* fix(web-shell): close the four Critical review findings

- SSH detection used a `ssh://` cwd prefix, but registration rewrites an SSH
  workspace to a local mirror directory before it is served, so the gate never
  fired and the section rendered against a boundary that answers 501. The route
  now tests `routeFileSystemFactory.sshWorkspace` (as `capabilities.ts` does)
  and the section tests the workspace entry's `ssh` marker.
- The route mapped every unclassified bridge failure to a flat 500. The first
  mutation on a freshly opened workspace spawns a control channel, and that
  spawn times out or is refused while the child starts; those carry none of the
  feature's error kinds. The shared mapper is now the fallback, so a retryable
  state keeps its 503/504 + Retry-After semantics.
- The section did not serialize mutations: Add was gated only on the initial
  load, so an add could race a pending remove over the same settings
  read-modify-write and silently drop one change. One gate now covers both
  directions, with a ref so it holds before the next render.
- Browse rendered unconditionally, dropping the `native_directory_picker` gate
  the same dialog gets in App; a headless daemon serves this section but answers
  the picker with 501. Browse now follows the capability.

Tests pin each: the SSH route case uses the production shape (local mirror cwd +
runtime marker), the section case uses the capability marker, the fallback case
runs the real shared mapper, and a deferred remove asserts Add is disabled.

* fix(ci): type the shared-mapper mock and split the home-root trust gate

The prior commit typed the test harness's `sendBridgeError` option as a loose
`(res: unknown, ...)` function, which `tsc --build` rejects when it is handed
the real `SendBridgeError`: the whole `prepare` build failed, so every CI job
that installs dependencies went red. Type the option as `SendBridgeError`
instead.

Also separate two states the handler conflated: `workspaceSettingsActive` is
false both for an untrusted workspace and for a trusted workspace rooted at
`$HOME`, and only the first is a trust failure. Trust is now read from
`config.isTrustedFolder()`; a home-rooted workspace answers
`directory_management_unsupported` rather than an `untrusted_workspace` code no
grant can clear. `makeConfig` gains the `isTrustedFolder` the handler now calls.

The section seeds the dialog with the workspace root so browse mode lists
folders on open, and its placeholder no longer advertises an `ssh://` spelling
this route rejects.

* fix(web-shell): gate the settings category on the SSH marker in App

The Settings root still decided whether to supply the Workspace directories
section from `workspaceCwd.startsWith('ssh://')`. Registration rewrites an SSH
workspace to a local mirror directory, so that test never fires: for a
registered SSH workspace App passed a truthy element, SettingsMessage created
the category, and the child section then rendered nothing — a visible, selected
category with empty content and no list request.

Decide it from the selected workspace entry's `ssh` marker, the same signal the
section uses, so App passes `undefined` and the category is omitted.

* fix(cli): pair workspace directory entries by tail, not by index

`workspaceFile.settings` is the merged layer, and `context.includeDirectories`
merges with `MergeStrategy.CONCAT`, so a user-scope value lands ahead of the
workspace entries. The handler paired that merged array with the
workspace-scope array by index, so once a user scope also set the key, a
workspace entry resolved to an unrelated user path: the panel showed the wrong
directory and removal left the real root live in every session.

The workspace entries sit at the END of the merged array, so take that tail
instead. Each workspace entry then resolves to itself, and a `${VAR}` entry
still matches the path sessions use.

The regression case seeds a user-scope prefix ahead of one workspace entry and
asserts the removal clears it from both live contexts; index pairing fails it.
2026-10-25 16:59:44 +00:00
Shaojin Wen
3eff5f7793
feat(web-shell): create workspaces from a Git URL (#15361)
* feat(web-shell): create workspaces from a Git URL

The Add-workspace dialog could only register an existing directory or
create an empty scratch directory, so starting from a repository meant
leaving the browser, cloning on the daemon host, and registering the
path afterwards.

Add a Git source that starts an asynchronous clone job, reports progress,
and supports cancellation. A successful clone is registered through the
existing workspace route, so capacity, nesting, persistence, and the
daemon's trust policy stay in one place.

* fix(cli): classify git_workspace_clone in the capability parity matrix

The parity matrix landed on main after this branch, and every capability
tag needs a row. Managed workspace creation only creates empty
directories, so record the clone surface as an explicit Managed gap
rather than claiming parity.

* fix(cli): reserve active clone targets before overlapping work

An outer clone's failure/cancel cleanup removes its target recursively,
so a nested clone or workspace registration must not start underneath a
running job. Reserve the canonical target for the job lifetime, reject
overlapping clone and registration paths, and release the reservation
only after cleanup settles.

Also resolve the success path before publishing `succeeded`, so a reader
never observes a terminal snapshot without the `cwd` it registers.

* fix(web-shell): keep clone polling recoverable

A failed poll left the last known status running, so the panel offered
Cancel instead of the recheck that can recover the existing job. Prefer
the unknown-state action.

Reset the unmounted guard in the effect setup: StrictMode's development
replay otherwise leaves it set and discards every later poll, including a
successful clone.

* feat(web-shell): reuse the directory path as the clone parent

When the source switches to Git, treat the directory path as the clone
parent and append the repository name from the URL. A path whose basename
already matches stays as-is, and a manually edited or browsed target is
never overwritten by later URL edits.

* feat(web-shell): suggest folders for the clone target

Headless daemons do not provide a native directory picker, so the clone
target had to be typed in full. Reuse the directory suggestion list:
selecting an existing folder uses it as the parent and appends the
repository name, while a manually edited target still wins.

* fix(cli): keep clone progress monotonic

git restarts the percentage for some clone phases (remote counting,
then receiving), so the Web Shell progress bar could jump backwards.
Clamp the reported percent to the running maximum; the status message
still follows the current git line.

Also pin the clone admission guards left unpinned by review: reserved
roots, unknown request keys, promotion of a workspace under an active
clone target, file URLs that name a host, percentages above 100, and
the dialog fallback when the daemon capability disappears.

* fix(cli): reserve the clone target before claiming it

The claim created the target with mkdir, then awaited the existence check
and the final realpath, and only reserved the canonical path afterwards.
A concurrent ordinary registration could admit the freshly created
directory in that window and then lose it to the clone's cancel/failure
cleanup.

Reserve the canonical target inside claimCloneTarget before mkdir, share
the reservation with registration, and release it on every claim failure
and on post-claim admission failures. Add a regression test that overlaps
the registration with the blocked claim and a unit test that asserts the
reservation exists before the directory does.

Also update the serve integration capability envelope for the
host-conditional git_workspace_clone tag, mirroring the daemon's probe.

* fix(cli): register git_workspace_clone in serve capability guards

Test (ubuntu) failed five capability-registry guards because the new
git_workspace_clone tag was not registered alongside the existing
conditional features:

- document it in the qwen serve protocol table;
- allow the clone spawner's process-environment copy and SystemRoot
  lookup in the serve process.env guard;
- add it to EXPECTED_REGISTERED_FEATURES and the conditional-feature
  drift-insurance branch.

Verified: capabilities-docs-contract, process-env-guard and server
suite 1399/1399; typecheck, ESLint, Prettier clean.

* fix(cli): keep clone targets away from reserved and Live paths

A clone target that contains the reserved Conversations root was accepted
when that root did not exist yet: creating the target made the root's
parent part of the clone, and the Live runtime could then be published
inside it. Because the Live exemption skipped the "canonical inside
target" direction for active clone targets, cancelling or failing the
clone deleted the registered Conversations workspace.

Reject clone targets that contain a reserved root, even before the root
exists, and apply strict two-way nesting to active clone targets in the
owned-runtime admission regardless of provenance.

Verified: workspace-git-clone + workspace-management 196/196;
typecheck, ESLint, Prettier clean.

* fix(cli): resolve reserved roots through existing ancestors

The reserved-root containment check fell back to the unresolved spelling
when the Conversations root did not exist yet. A symlinked ancestor such
as ~/Documents then made the canonical clone target and the lexical
reserved path disagree: the clone was accepted, the Live runtime created
the root inside it, and cancelling the clone left ConversationWorkspace
with a cached identity for a directory that no longer existed.

Resolve each path through its nearest existing ancestor and append the
missing suffix before comparing, so a clone target containing a missing
reserved root is rejected before mkdir even when ancestors are symlinks.

Verified: workspace-git-clone + workspace-management 197/197;
typecheck, ESLint, Prettier clean.

* fix(cli): cover case aliases and dangling links in reserved roots

The reserved-root guard compared the reconstructed missing suffix with
case-sensitive isWithinRoot, so a case-variant ancestor such as
`Documents/qwen code` did not match `Documents/Qwen Code/Conversations`
on a case-insensitive filesystem. It also walked past a dangling symlink
ancestor, so a clone target that was the link's missing destination
could later make the link valid inside the clone's cleanup scope.

Compare canonical paths case-insensitively after resolving through the
nearest existing ancestor, and follow dangling symlinks while
reconstructing the missing suffix. Add regressions for both aliases.

Verified: workspace-git-clone + workspace-management 199/199;
typecheck, ESLint, Prettier clean.

* fix(cli): harden git clone credentials, config, and admission

- Strip URL credentials before git's argv and supply them through a
  command-line credential helper backed by the child environment, so the
  secret no longer appears in /proc/<pid>/cmdline or .git/config.
- Disable global/system git config for the clone child so a repository's
  .gitattributes cannot run host-configured filter commands during checkout.
- Keep the first fatal/error stderr line for clone failures instead of the
  trailing SSH boilerplate.
- Only treat Receiving objects/Resolving deltas as progress phases, so
  remote counting cannot pin the bar at 100.
- Strip OSC/C1 and all other control characters from clone messages; add
  the missing Windows taskkill 'error' listener.
- Require operator authority for clone cancellation; reject clone targets
  inside the managed scratch root or equal to a registered/in-flight
  workspace; keep case-insensitive containment scoped to clone targets.

Verified: workspace-git-clone + workspace-management 205/205;
typecheck, ESLint, Prettier clean.

* fix(web-shell): gate the clone flow during submit and IME input

- Disable the post-clone Back button while registration is in flight and
  guard backToForm, so it cannot reset a submitting flag owned by onAdd.
- Ignore clone-target key handling while an IME composition is active.
- Dismiss the clone-target suggestion list when the git form is submitted,
  so its Escape interceptor cannot stay armed over the progress view.

Verified: AddWorkspaceDialog 57/57; Prettier and ESLint clean.

* docs: align git clone guarantees with credential and config isolation

- Document the `{ kind: "git" }` form and its 202 job response in the
  `POST /workspaces` reference.
- State that URL credentials are moved out of git argv and the stored remote
  URL, and that global/system git config is disabled for the clone child so
  committed filters cannot run host commands during checkout.
- Mirror both changes in the Chinese design document.

* fix(cli): use bracket access for clone credential env

The strict index signature on NodeJS.ProcessEnv rejects dot access to
QWEN_GIT_USERNAME/QWEN_GIT_PASSWORD; all CI jobs that build the CLI
failed at tsc with TS4111 before running any test.

* fix(web-shell): cancel the running clone from the left action

- Route the progress panel's left Cancel through cancelClone while the job
  is running, so it stops the daemon job instead of only closing the dialog.
- Clear the pending poll timer at pollCloneJob entry and before cancel, and
  guard registration per job id, so a terminal snapshot cannot register the
  same clone twice.

Verified: AddWorkspaceDialog 57/57; Prettier and ESLint clean.

* fix(web-shell): harden git clone env and clone-job dialog states

Pin LC_ALL/LANG for the clone child and sweep host Git config channels (GIT_CONFIG_*, GIT_ASKPASS, GIT_SSH*, GIT_PROXY_COMMAND) so a localized git still reports parseable progress and a browser-chosen URL cannot borrow host credentials. Decode URL credentials defensively, reject control characters, and print the inline credential helper with printf so dash does not mangle tokens.

In the add-workspace dialog, tear the clone state down when the capability disappears, report a pruned clone job instead of offering recheck forever, honour a cancel that loses the race with a finished clone, and close instead of cancelling an unconfirmed job. Correct the host-authentication prose in the design docs, README and URL hint to match what the isolated clone child actually supports.

* fix(cli): resolve relative symlinks physically and keep cancel intent

- Resolve a relative symlink target from its physical parent before appending
  the missing suffix, so a dangling relative Documents link cannot hide the
  reserved Conversations root from clone admission.
- Keep the per-job cancellation intent across overlapping poll responses, so
  a second succeeded poll cannot auto-register after Cancel. Clear it only on
  an explicit Back/new submission.

Verified: route + clone 209/209; AddWorkspaceDialog 61/61; typecheck with a
6 GB heap; Prettier and ESLint clean.

---------

Co-authored-by: verify <verify@local>
2026-10-25 10:16:38 +00:00
Shaojin Wen
1df5031e70
perf(ci): overlap web-shell and standalone tests with the CLI suite (#15438)
* perf(ci): overlap web-shell and standalone tests with the CLI suite

The Linux Test job ran every workspace suite, the standalone runtime
regression and scripts/tests one after another. The CLI suite keeps its
workers busy for 13-20 minutes, while web-shell's suite spends most of its
run on App.test.tsx alone and the standalone regression starts one
runtime process at a time, so those two added their mostly idle minutes
straight to the job.

Run them in a side lane beside the other workspaces instead.
.github/scripts/ci/run-unit-test-lanes.sh starts web-shell and the
standalone regression in the background, runs every other workspace's
test:ci (listed with npm pkg get, in npm's workspace order) and then
scripts/tests, and replays the side lane's log after the main lane so
the two never interleave. The step runs the script under the same
setpriv wrapper, so every test command still runs without the DAC
override capabilities.

test:scripts now reaches its Vitest run through a new test:scripts:unit
script with a trailing --, so the main lane can run it without the
standalone regression while test:ci, test:ci:workspaces and test:scripts
run what they ran before.

* fix(ci): name the cause when the lane script cannot start a lane

- Fail with an ::error:: line when mktemp cannot create the side lane's
  log file, instead of running the main lane alone and dying in tail.
- Check the exit status of `npm pkg get` and of the parser. A failing
  `npm pkg get --json` prints an error object, which used to parse as an
  empty listing and report "Found no workspaces".
- Note in the header that the replay puts side-lane failures after the
  main lane's in main-failure-signature.mjs's title and search markers.
- Lane test: clear the caller's NODE_OPTIONS, assert the main lane's
  output is present before checking its order, and cover the failed
  listing and the missing temp dir.
- Cross-check npm's workspace resolution against the selector in
  scripts/workspaces.js.

* fix(ci): halve web-shell's worker cap beside the main lane

Each Vitest run applies VITEST_MAX_FORKS / VITEST_MAX_THREADS on its own,
so running two lanes at once doubled the step's worker cap, and on the
hosted runners, where no cap is set, put two all-CPUs pools on 4 vCPU.
The lane script now runs web-shell at half the cap, at least 1, and at 2
where the cap is unset; the main lane keeps the full cap.

- Refuse a workspace name with whitespace in it: the main lane's
  arguments are read one per line, so a line break would split a name
  into other arguments, flags among them.
- Lane test: web-shell and the main lane's suites now wait for each
  other, so a side lane serialized before or after the main lane fails;
  record each call's worker caps; run the script against npm's real
  listing and compare its main lane with the selector the release pins
  use; cover a name with a line break in it.
- ci.yml: the standalone runtime cache comment names the lane script's
  side lane, not test:scripts, as what runs the regression in this job.
- ci-variables.md: say the cap applies per Vitest run and what the
  two-lane step runs at its peak.
- release-workflow.test.js: follow test:scripts through
  test:scripts:unit to vitest; #15102 pinned the shape before the split.
2026-10-25 10:05:56 +00:00
Shaojin Wen
92459f93dc
feat(serve): persist max-sessions in settings.json (#15419)
* feat(serve): persist max-sessions in settings.json

`qwen serve --max-sessions` could only come from the command line, so a
restart without a launch script silently fell back to 32. Add
`serve.maxSessions` to settings.json and expose it in the Web Shell
settings panel.

Resolution is `--max-sessions` > `serve.maxSessions` > 32. The key is
operator-owned: a workspace settings file is stripped from the merge and
named on stderr at boot, and the serve fast path skips it structurally.
An invalid value is reported and treated as absent so a typo cannot take
the daemon down or silently disable backpressure; 0 disables the cap.
Dropping the yargs default is what keeps an omitted flag distinguishable
from an explicit 32.

The Web Shell allowlist carries the key because it is showInDialog: false.
The panel writes it at user scope; the existing workspace-scope refusal
still applies.

Design: docs/design/2026-10-24-serve-max-sessions-setting.md

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): keep operator settings when workspace settings are malformed

A workspace `.qwen/settings.json` containing `[]` (or invalid JSON) made
`loadServeFastPathSettings()` throw before it returned the already-read
user/system/system-defaults scopes. The serve boot catch then left
`bootSettings` undefined, so `serve.maxSessions: 3` was lost and the bridge
fell back to 32 — a repo-local file could effectively raise an
operator-owned cap.

Isolate the workspace read in the serve boot: `onWorkspaceSettingsError`
skips only that scope and reports it, while the default fast-path contract
still throws so the full settings loader can take over. Regression covers
both `[]` and malformed JSON with user `serve.maxSessions` / `serve.tokenQr`
set.

Real daemon: user 3 + workspace `[]` now reports 3 (was 32); the four
precedence/fallback scenarios still report 3/5/32/32.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(serve): pin the settings-sourced total cap and default

Two non-blocking triage notes:

- The schema default was pinned to a literal 32 while the adjacent
  sub-session test pins its default to the runtime constant. Assert it
  against advertisedMaxSessions(undefined) so a future DEFAULT_MAX_SESSIONS
  bump cannot leave the schema and vscode mirror advertising 32 silently.
- A settings-sourced per-workspace cap feeds deriveDefaultMaxTotalSessions,
  but no test exercised that path. Boot two workspaces at
  maxRegisteredWorkspaces: 2 with serve.maxSessions: 3 and assert
  /capabilities reports maxTotalSessions: 6.

Test-only.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-24 17:05:21 +08:00
Shaojin Wen
779a4d5d0d
feat(serve): carry a scheduled-task gate through the daemon and Web Shell (#15211)
Some checks are pending
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
Managed Agent gate / Select the Managed Agent tier (push) Waiting to run
Managed Agent gate / SDK Java () (push) Blocked by required conditions
Managed Agent gate / Managed Agent gate (push) Blocked by required conditions
SDK Java Contract Open PRs / Re-check open PRs against main (push) Waiting to run
SDK Java Flyway Open PRs / Re-check open PRs against main (push) Waiting to run
Verify Landlock helper / Native Linux CLI boundary (push) Waiting to run
Verify Landlock helper / Native Windows settings and streams (push) Waiting to run
Verify Landlock helper / arm64 (push) Waiting to run
Verify Landlock helper / x64 (push) Waiting to run
* feat(serve): carry a scheduled-task gate through the daemon and Web Shell

A gated scheduled task (PR 1a) could only be created from the TUI, and the
TUI refused a gate on a `sessionMode: "current"` task because nothing on the
daemon side knew the field. This threads `gate` through every path a task can
take in the daemon and surfaces it in the Web Shell.

- `cron_create` accepts a gate on a current-session task; the request reaches
  the daemon over the ACP bridge (`gate` must be a non-empty string within the
  2048-char cap, else `invalidParams`) and the daemon wraps the task with
  `withTaskGate`, so the downgrade fuse (`condition`) stays in step.
- REST: POST accepts `gate` (400 `invalid_gate`, 400 `gate_requires_recurring`
  on a one-shot), PATCH sets, replaces or clears it (`null`/`''`) and resets
  the gate status when the command changed; a gate edit on a legacy guarded
  task is refused with 409 because its `condition` is its own fail-closed
  marker, not a fuse. Task views carry `gate` and `gateStatus`.
- A manual run of a gated task has no scheduler in the loop, so the run prompt
  asks the model to run the gate itself first, under the usual tool approval,
  and stop when it prints nothing (`buildGatedManualRunPrompt`); per-run
  manual runs and the Web Shell's persistent-task "run now" both use it.
- Capability `scheduled_task_gate` (`{ since: 'v1' }`) advertises the field;
  the Web Shell shows the gate editor, card line and status only when the
  daemon lists it.
- `gateCommandError` moves the shape check into core so the tool, the bridge
  and the routes agree on what a gate may be. Its `&` check is
  `backgroundsAnyPart`, so the routes refuse a backgrounded wrapper script
  (`nohup bash -c './probe.sh &'`) exactly as `cron_create` does.

Tests cover the tool, the bridge, the ACP session, the routes (POST/PATCH/run
and the existing-session creator), the run prompt and the dialog. Docs: the
protocol capability table, the user guide's daemon section and the design
doc's status.

* test(serve): list scheduled_task_gate in the integration capability envelope

* fix(core): keep a current-session gate confirmation and reset a replaced gate's streak

A gate confirmed through the dialog for a `sessionMode: "current"` task was
blocked at its first fire: the daemon writes the task file and this
session's scheduler installs the job on its next reload, with nothing to
inherit the confirmation from. `cron_create` now hands it to the scheduler
(`approveGate`), which marks the job when it is loaded or when the reload
installs it, and only while the task's gate is that command.

A reload also kept the in-memory gate status when the file had none, even
after the gate command changed, so a replaced gate continued the old
failure streak and its first failure was counted without a notice. The
in-memory status now carries over only for an unchanged gate.

* refactor(serve): single-source the gate cap in the bridge and drop barrel churn

* fix(core): keep a replaced gate's late result off the new command's status

The gate status write matched the task by id only. When a PATCH replaced
the gate and dropped its status, the old command could still settle before
this session's watcher reloaded the task, and its write put the old
command's streak back onto the new gate: the new command's first failure
was then counted without a notice. The status is now written only while
the task's gate on disk is the command that produced it; the run record of
that fire is still appended.

* fix(core): keep the tick's gate status write on its own command

The tick persists a gated fire's stamp together with the job's in-memory
gate status, matched to the task by id only. When a PATCH replaced the
gate and dropped its status, a tick landing before this session's watcher
reload wrote the old command's streak back onto the new gate, so the new
command's first failure was counted without a notice. The tick now keeps
the command with the status and applies it only while the task's gate on
disk is still that command.

* fix(serve): send the gate only when edited and tighten what a gate may be

Web Shell:
- An edit sends `gate` only when the field changed it. The route reads any
  `gate` key as a gate edit, which a legacy guarded task refuses (409), so an
  untouched field made prompt and cron edits fail on those tasks.
- A one-shot edit shows no gate field and sends no gate (the daemon refuses
  a gate there and would discard the whole save).
- The field is a textarea, so a stored multi-line gate is not flattened on
  the next save.
- The card's status line leads with the outcome and reason while the gate
  fails or is blocked, and the hint states what the runner really does: exit
  0 with output delivers, empty stdout skips, a non-zero exit reports stderr,
  and unattended runs need a rule, a read-only command or auto/yolo.

Core: `gateCommandError` refuses a command of only comments (it runs as
nothing, so every fire would be quiet) and reads a trailing `\&` as a
literal while `\\&` still backgrounds.

Docs: `scheduled_task_gate` is unconditional, so it leaves the conditional
capability table (the docs contract test counted it there) for its own
paragraph with the recurring and legacy refusals, the `blocked` outcome and
its preconditions, and the per-method behaviour of an older daemon.

Tests cover each change, plus the shared validator's length branch, a gate
on a create bound to a caller session, and `approveGate` refusing a loaded
job whose command differs.

* fix(core): read a gate's comment scope from its shell structure

The comment-only check ran on the permission text, which joins a
wrapper's script with whatever follows the wrapper: `bash -c '# x' &&
printf 'ready\n'` read as one comment line and was refused, though the
command after the wrapper runs and prints. `runsOnlyComments` now checks a
plain command line by line and a wrapper by its own script, recursively,
counting anything after the script as a command.

* fix(serve): close audit gaps in gate validation, confirmation and edits

- A daemon that predates gated tasks drops a requested gate on a
  current-session create. The daemon now echoes the stored gate through
  the bridge to the child, and `cron_create` removes a task that came back
  without its gate and refuses, instead of leaving a task that runs at
  every slot.
- `stop()` keeps pending gate confirmations: a turn cancelled before the
  reload that installs the task stops and re-enables the scheduler.
- PATCH refuses a gated one-shot only when the edit makes it; a task
  already in that state keeps its other edits and can clear the gate.
- `backgroundsAnyPart` ignores comments and counts the backslashes before a
  trailing `&` at every level, wrapper scripts included: an `&` in a
  comment no longer refuses a gate, a quote in a comment no longer hides a
  real `&`, and `bash -c 'echo \\&'` is refused like its top-level form.
- Docs: PATCH clears a gate with null or "", the one-shot refusal is per
  edit, the `&` check is lexical, and binaries up to v0.19.8 ignore the
  downgrade fuse.

* fix: read gate scripts with and without comments and close audit edge cases

- Read every level of a gate twice, with comments stripped and raw, and
  refuse an `&` either reading finds: a `#` misread inside `$(...)`,
  `$'...'` or an escaped wrapper quote can no longer hide a real `&`.
  The check errs toward refusing, documented in the design, protocol and
  user docs.
- Treat plain words after a wrapper script as its positional parameters
  when checking for a comment-only gate.
- Name the ungated task left behind when removing it after a daemon
  dropped the gate fails.
- Re-sending an unchanged gate or recurring flag no longer counts as
  making a new one-shot gate.
- Web Shell: allow clearing the gate of a one-shot gated task, refuse
  to submit a typed gate once the daemon stops advertising gates, drop
  the client-side length cap, and fix the gate field comment.

* fix(core): fail a quiet gate whose background part still holds its output

The lexical `&` check cannot follow every spelling: a quote it misreads
inside "$(...)" or $'...', a script handed to eval, piped into a shell,
passed as `bash -c -- ...` or put together from several quoted pieces
all get past it, and such a gate settles quiet at every fire.

Back it with the runner: the shell service now marks a streamStdout
result whose output pipes were still open when the post-exit drain ran
out (`outputHeldAfterExit`), and a gate that exits 0 with nothing on
stdout in that state is reported as failed instead of quiet. A gate
that printed in the foreground is still delivered.

The docs now call the lexical check best-effort and describe the
backstop. Also moves approveGate's doc comment back onto it.

* fix(core): judge a gate's backstop on stdout alone and stop what it left behind

- The shell service now reads the stdout end on streamStdout runs, and
  reports `stdoutHeldAfterExit` only when stdout itself was still open as
  the post-exit drain ran out. A helper that silenced its stdout but kept
  stderr no longer turns every quiet fire into a failure.
- When a gate's stdout is still held, the runner kills the gate's process
  group: the gate is over by contract, and each fire would leave one more
  process behind.
- Quiet is judged on the stdout the session would see: output of escape
  sequences alone (`tput sgr0`) no longer wakes the session with an empty
  gate-output block.
- The manual-run instructions tell the model that a command which puts
  part of itself in the background counts as a failure, matching the
  scheduled fire.
- Docs: the backstop's stdout-only rule, the stopped process group, and
  the bwrap shell execution sandbox, whose PID namespace ends a background
  part with the command so only the lexical check applies there.

* fix(core): stop a gate's leftovers gently, keep cut and escaped output, align manual runs

- Leftovers: the gate's process group gets SIGTERM first, so a part that
  holds a lock can release it, then SIGKILL 200 ms later. Docs and the
  comment now say what is out of reach: a part in its own process group
  or session, and Windows.
- Quiet: a cut output is never quiet, since what was kept may be noise in
  front of what was cut. Sanitizing strips escape sequences line by line,
  so a title sequence left open cannot swallow the lines after it.
- Durations run on the monotonic clock.
- Manual runs: a gate that printed nothing because it put part of itself
  in the background is the failure; one that printed is followed, as the
  scheduled fire delivers it.
- Tests: the leftover-stop test keeps process.kill stubbed and its timer
  faked for the whole case, so no run signals a real process group.

* fix(core): judge manual gate runs on stdout, defang open structural tags, kill leftovers at exit

- Manual runs: the model runs the gate as `{ <gate>\n} 2>/dev/null`,
  since a scheduled fire reads only stdout; a warning on stderr no longer
  makes a manual run follow the task where the scheduled fire is quiet.
- Defang: a structural tag name is defanged however the tag goes on,
  `</gate-output >`, a tab, attributes or the line's end, all of which
  can still close the data block in XML.
- Leftovers: a process that exits before the SIGKILL is due sends it on
  its way out, so a part that ignored SIGTERM cannot outlive a headless
  run that ends with its last gate.
- A held stdout with nothing visible fails even when the output was cut.
- The leftover test restores process.kill and real timers in `finally`.

* fix(core): refuse a gate that carries a NUL character

A process argument cannot carry a NUL, so such a gate passed every check
and then failed to start at every fire. gateCommandError now refuses it,
which answers 400 invalid_gate on the routes and refuses it in
cron_create.

* fix: send a manual gate run as written, and defang only what parses as a tag

- Manual runs send the gate exactly as written again: the `{ … } 2>/dev/null`
  wrapper kept the session's permission rules from matching it (allow
  rules asked again, deny rules no longer applied, the read-only path
  refused it) and broke under cmd.exe and PowerShell. The prompt instead
  says only standard output counts and warnings on standard error do not.
- The structural tag defang matches a name followed by `>`, `/>`, an
  attribute or the end of the line, so `</gate-output >` is still defanged
  while `Promise<Result | null>` and `a <result && b` in monitored text no
  longer gain an invisible character.
- A held stdout whose cut output showed nothing says so, instead of
  claiming nothing was printed.

* fix(core): defang the gate's own wrappers in every tag form, keep Monitor's defang as it was

The gate block is not XML-escaped, so its defang is the whole defence, and
the shared Monitor regex only matched `<name>`: `</gate-output x>`,
`</gate-output/ >` or a name split by an invisible character still closed
the block. Widening the shared regex either missed forms or defanged
ordinary Monitor text (`Promise<Result>`), so monitor.ts is back to its
base, and the gate's sanitizer strips the invisible characters that could
hide a wrapper and defangs `gate-output` / `gate-stderr` whatever follows
the name.

Docs: a manual run applies `Bash(...)` rules and the read-only check to
the command as written; the scheduled fire checks it as a `Monitor`
command, and a `Monitor(...)` rule does not cover the shell call.

* fix(core): write every < in gate output as &lt; instead of chasing tag spellings

Defanging the wrappers' names kept missing forms: a joiner or bidi mark in
the name, attributes, a tag spelled in tag characters, and a strip list of
invisible characters both leaked and damaged ordinary text (Persian ZWNJ).
The gate block is data the model reads, so every `<` in it is now written
`&lt;` and its tag characters (U+E0000–E007F) are dropped: no tag can open
or close the block however it is spelled. The fire text and the failure
notice say so, and the docs describe it.

* fix(core): read a newline after a wrapper's script as a command separator

A gate such as `bash -c '# setup'` followed by a newline and a command was
refused as only a comment: the wrapper parse trimmed the newline away, so
the next command read as the script's positional words. The suffix keeps
its leading newline now (other readers trim it themselves).

* fix: let gate rules see a line after a wrapper script, and refuse any gate edit the daemon would drop

- Security: a gate's permission check read `bash -c 'echo hi'` followed by
  a newline and `rm …` as `echo hi rm …` (the script and its positional
  words), while the gate runs the text as written and the shell runs `rm`
  as the next command; deny rules missed it and the read-only check
  allowed it. The Monitor normalization now keeps that line on a line of
  its own in the safety command (only stricter for Monitor, whose spawned
  command is unchanged).
- A wrapper comment followed by a line of only comments is refused as
  only a comment again.
- Web Shell: with gates no longer advertised, any change to a task's gate
  (clearing one included) blocks the save with an error, instead of a
  request without the `gate` key that kept the old command.
- Docs: the rule fix and the §4.2 example texts.

* fix(core): refuse a command after a shell wrapper's script in a gate

The permission rules read a wrapper's script and what follows it as one
command line, which is what Monitor spawns, while a gate runs the text as
written: a command after a newline, `;`, `&&` or `|` runs on its own.
Joining the next line with a newline in the safety command still let a
script ending in `\`, an open quote, a here-document start, or a blank
other than space before the newline hide that command from deny rules and
the read-only check. A gate now keeps every command inside the wrapper's
script; the Monitor normalization is back to its base.

Web Shell: a gate change made before the daemon stopped advertising gates
stays visible, so the change the save refuses can be undone.

* test(core): type the gate refusal table so the spread rows check

* fix(core): refuse a gate the Monitor normalization would rewrite

The permission rules check a gate as the Monitor normalization rebuilds
it: a shell wrapper's script unquoted and joined into one line, which is
what Monitor spawns. A gate runs as written, and the two kept differing in
new ways: adjacent quotes end the script early, a separator glued to it
hides the next command, a trailing `\`, an open quote or a command after
it does the same, and deny rules and the read-only check then judged a
command other than the one that ran.

Instead of chasing each spelling, a gate must now be a command the
normalization leaves exactly as written: a shell wrapper (`bash -c '…'`,
also behind env assignments) is refused. A gate already runs in a shell,
so the wrapper adds nothing. This replaces the narrower check on commands
after the script.

* fix(core): name the command the rules would check when refusing a gate, and drop stale wrapper notes

A plain command the Monitor normalization misreads as a wrapper
(`bash /opt/probe.sh -c x` reads as `x`) was refused as "wrapped in a
shell" with nothing to say why; the error now shows what the permission
rules would check. The design docs no longer list wrapper spellings that
are refused at creation as getting past the background check, the zh-CN
paragraph loses the leftover from the earlier rule, and the risks name
the PermissionManager's own blind spots that a gate inherits from the
shell tool.

* fix(core): keep the refused gate's checked command whole when cut, and word the refusal as a reading, not a mismatch

The command named in a wrapper refusal is cut at 80 units without
splitting a surrogate pair, and the error says the rules would check it
"not the command as written" rather than "instead of what runs", which
was untrue for a plain wrapper. The gateCommandError doc lists every
refusal again. In the design docs a gate gets the decision of a monitor
call (not of a shell command), and the acceptance criterion on older
binaries matches the fuse: v0.19.9 on never fire a gated task.

* fix(core): report a gate again when its failure streak changes kind

Blocked and failed fires share one streak, and only its first fire was
reported. A gate created over REST is blocked until its rule is added;
the blocked notice says further blocked fires are counted silently until
the gate runs again, yet when it then ran and failed nothing was said.
A notice now opens a streak and follows each change between blocked and
failed. The envelope comment and design docs also stop claiming more
than `<` escaping gives: look-alike brackets stay as text in the block.

* fix(core): report a gate blocked for another reason, and store the quiet fire that ends a failure streak

A gate blocked by a deny rule and then, once the rule was removed, only
unapproved was never told the rule to add: both fires were 'blocked', so
the second counted silently. A blocked fire now notifies when its reason
changes too.

A quiet fire's status only reached the file with the next tick persist,
so a restart in between reloaded the old failure streak and counted the
next failure into it without a notice. The quiet fire that ends a streak
is now written at once.

The tool descriptions, the Web Shell hint, the protocol and user docs and
the design docs say when a failure is reported in the same terms.

* fix(core): book a gated fire at the slot it ran for

The run record and the job handed to the consumer re-read the live job's
lastFiredAt when the gate settled; a manual run or a later slot stamped
while the gate ran moved it on, so the run landed under another fire's
timestamp and a per-run session's annotation matched the wrong entry.
Both now use the fire's own slot, which the status already did.

Also corrects the design docs on how often a gated task rewrites the
tasks file, and two scheduler comments the new notice and persistence
rules had made stale.

* fix: title a gated per-run session from its task, book runs in time order, call a gated slot checked

A gated fire hands the session the gate's delivered text as its prompt,
and a per-run fire titled its fresh session from that text instead of
the task prompt a manual run uses; the fire now carries the task prompt
too. A gated run is booked when its gate settles, under its own slot,
which can be older than a manual run booked meanwhile, so the run ring
now keeps time order. And the Web Shell card read a gated task's slot as
its last run, though a quiet gate ran no prompt: it now reads as checked.

* fix: refuse a gate holding an unpaired surrogate, and say "1 failure"

Bash receives the gate as UTF-8, which cannot carry an unpaired UTF-16
surrogate, so such a gate ran as U+FFFD rather than the text the
permission rules checked; it is now refused with the other malformed
gates, as main refuses it for a hosted shell command. The Web Shell gate
line also said "1 failures" on a gate's first failure.

* docs(core): name where a session drops a delivered gate output with its fire

* fix(core): never start a gate's stderr tail with half a character, and name where a session drops a gate output

The failure notice keeps the last 2048 UTF-16 units of stderr, a cut that
could leave the low half of a surrogate pair at its start; the notice is
a model prompt, and an unpaired surrogate breaks its serialization. The
cut now drops that half. The design docs describe the places a session
drops a fire, and a delivered gate output with it, as examples rather
than a closed list: a user prompt, a cancel or the token-limit breaker
clear queued fires too.

* fix(core): drop half a character at the stderr tail's start on a short tail too

The runner keeps a rolling stderr buffer cut in UTF-16 units, which can
also leave the low half of a pair at its start; the previous fix only
dropped that half when the tail itself was cut. It is now dropped either
way.

* docs: say the rule a blocked gate names may not cover a compound command

The rule is the one Monitor's "always allow" would save, which for a
compound command (a test of a command substitution, an if, a heredoc)
may leave a part unmatched, so the gate stays blocked; such a probe
belongs in a script.
2026-10-23 06:51:24 +00:00
qqqys
7bda51a070
feat(core): add model.loopProtection and remind instead of stopping loops by default (#15016)
* feat(core): add model.loopProtection and remind before loop stops

Add the model.loopProtection setting (stop, warn, off). Under the default
stop, every stop point stays where it is; the always-on guards (identical
calls, shell inspection, daemon invalid tool parameters, stuck repetition
past the adaptive cap) first send the model one reminder with the next tool
results. warn only reminds, except for content and thought repetition; off
neither reminds nor stops. Budgets stop in every mode.

A new loop_protection telemetry event records each reminded, stopped and
observed decision. Loop stop hints and the TUI dialog note name the setting.

Refs #14828

* feat(core): default model.loopProtection to warn

A firing loop guard now reminds the model once and lets the turn continue
by default. Budgets still stop every mode, and content and thought
repetition still stops. Set model.loopProtection to "stop" to keep the
previous stop points.

Refs #14828

* fix(core): keep side queries out of the running turn's loop decisions

A concurrent /btw side query shares the client's loop detector. Its retry
dropped the turn's pending reminders, its stateful tool results could queue a
global-duplicate reminder into the turn, and under `off` it marked patterns
observed under the turn's prompt id. Route recordToolResult through the same
ownership gate, drop pending reminders once on the owner's Retry only, and
skip observed decisions for side queries.

Warn once when model.loopProtection is not a valid mode instead of silently
using the default, and update the consecutive-identical comment that still
described an unconditional halt.

* fix(core): keep reminders across side queries and guard unknown modes

A concurrent /btw side query starts as a UserQuery, so its reset cleared the
running turn's queued reminders and reminded/observed decisions. Under `warn`
the stuck turn then got no reminder at all. Keep that state on a side query's
reset, and log each reminder under the prompt id that queued it.

Validate model.loopProtection in core Config as well, since SDK and serve
embedders bypass the CLI check and an unknown mode acted as `off`.

For a stateful read tool with no recorded results, `warn` now records an
observed decision at the stop point instead of a reminder claiming the
result will not change; `stop` still fails safe and stops.

* fix(core): let late stateful results earn the warn reminder

Under `warn`, a stateful read that reached the stop point before its results
were recorded was observed under the 'identical' key, which then blocked the
reminder for the rest of the streak even after the results came back
unchanged. Record that case under its own key so the reminder can still be
sent once the evidence is in.

In daemon sessions, leave queued loop reminders untaken when the repeated
tool failure guard stops the batch, so they are not logged as `reminded`
without being sent.

Update the telemetry docs for `observed` under `warn`, note in
model.skipLoopDetection that model.loopProtection decides whether the
always-on guards remind or stop, and drop the stale DashScope #5019 rationale.
2026-10-21 13:11:16 +00:00
qqqys
b51ce2c305
feat(workflow): add per-turn human-origin workflow hints (#14876)
* feat(workflow): add per-turn human-origin workflow hints

* fix(workflow): preserve intent across recall and cached submissions

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-10-21 06:54:24 +00:00
Edenman
f1645116b0
feat(serve): add persisted workspace artifact library (#14867)
* feat(serve): add persisted workspace artifact library

* fix(serve): align artifact library capability contract

* fix(serve): preserve artifact library ownership and replay

---------

Co-authored-by: 丁炳智 <dingbingzhi.dbz@alibaba-inc.com>
2026-10-21 14:42:51 +08:00
callmeYe
a4937a3533
feat(web-shell): refresh model catalogs from the model selector (#14685)
* feat(web-shell): refresh model catalogs from the model selector

* fix(web-shell): stabilize model refresh state and capability tests

* fix(web-shell): preserve refreshed model catalogs and runtime scope

* fix(ci): sync upstream build heap budget

Sync the 6144 MB build heap ceiling from upstream PR #14822 (eeec911c20) after the Java 11 E2E retry again exhausted the 4096 MB heap in CLI compilation.
2026-10-20 23:01:55 +08:00
qqqys
bf118a1220
feat(core): report why loop protection stopped a turn (#14858)
Two loop types cover two causes each: turn_tool_call_cap is an explicit
model.maxToolCallsPerTurn, the adaptive backstop or a stuck halt past
the soft cap, and global_tool_call_duplicate is the heuristic duplicate
detector or the duplicate provider tool-call id breaker. Each of these
stops now carries an additive loopReason, and cap stops carry the call
count and the limit that applied, on the LoopDetected event, the
loop_detected telemetry and the daemon turn_error. The SDK keeps the
detail on transcript blocks instead of dropping the loop type.

The headless CLI, the TUI loop dialog, OpenTUI and Web Shell (en and
zh-CN) explain each cause and what to change. No detector fires or
stops at a different point. The export renderer budget rises to
2,050,000 for the new i18n keys (+5,139 bytes measured).

Part of #14828.
2026-10-20 09:58:07 +00:00
ytahdn
dc8b523d39
feat(web-shell): show background shell task runtime details (#14831)
* feat(web-shell): show background shell task runtime details

* fix(build): budget shared shell task runtime labels

* test(serve): sync shell runtime telemetry route counts

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-10-20 16:18:17 +08:00
Qiu
5ded4b8c2a
docs(telemetry): clarify OTLP HTTP uses JSON encoding (#14664)
Co-authored-by: 易良 <1204183885@qq.com>
2026-10-20 14:19:49 +08:00
ytahdn
6ec1b83044
feat(web-shell): delete selected message groups from session history (#14297)
* feat(web-shell): add grouped message deletion with durable history updates

* fix(web-shell): repair message deletion CI contracts

* docs(serve): index message deletion protocol routes

* fix(web-shell): harden grouped deletion recovery and export build

* fix(web-shell): allow selecting all message groups for deletion

* test(cli): observe hook cancellation rejection before waiting

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-10-20 13:06:23 +08:00
Lilian0122
798e5c9b66
fix(web-shell): preserve slash highlighting in user messages (#14171)
* fix(web-shell): preserve slash highlighting in user messages

Fixes #14161

* perf(web-shell): trim slash highlighting bundle overhead

Render highlighted text through a plain helper, keep captured line breaks, and only mount the URL renderer for text that can contain an explicit URL. Reuse the existing scoped data attribute for styling.

This reduces the transcript JavaScript guard count from 1,332,162 to 1,331,354 UTF-16 units. Preserve host fragment merging and add mixed-case URL and scheduled-task highlighting coverage.

* test(web-shell): pin slash-highlight replay path and styling contract

* test(web-shell): compare full declaration sets for composer slash parity

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: 铃蓝 <guolinliang.gll@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-19 22:39:03 +00:00
烽宁
06efd789b1
feat(telemetry): Record reasoning effort on LLM spans (#14718) 2026-10-20 01:07:37 +08:00
易良
7abcc60348
fix(sdk): size session create from the daemon session budget (#14588)
* fix(sdk): size session create from the daemon session budget

POST /session answers only after the daemon spawned the session's ACP
child and finished its initialize handshake, but the client charged that
wait to the generic 30s request deadline. The daemon bounds the handshake
with --initialize-timeout-ms (10s by default, 60s on the shared E2E
runners), so a slow spawn was abandoned while the daemon still completed
it and channel users saw "Could not create task".

Restore already sizes its deadline from the advertised
limits.sessionRestoreTimeoutMs plus headroom. Apply the same derived
deadline to session create and worktree reset, which spawn children for
the same kind of work, and keep an explicitly configured client timeout
winning. The channel-workers E2E test now also outlives the initialize
budget it gives the daemon.

Fixes #14584

* fix(web-shell): widen the create watchdog past the daemon handshake budget

Session create now waits on the daemon's ACP handshake budget (70s by
default) instead of the generic 30s request deadline, so a workspace create
action costs one capability preflight plus that larger create request. The
watchdog that bounds the whole action still assumed 2 x 30s and would
abandon a create the SDK now deliberately allows.

Raise CREATE_WATCHDOG_TIMEOUT_MS to cover preflight + create + headroom, and
update the deadline tests: the create and preflight budgets are no longer
equal by design, a hung create is bounded at 115s, and the SDK request
timeout case needs a session delay above the new create budget.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmvdyvii4fo

* fix(web-shell): size the create watchdog from the advertised session budget

The SDK now sizes a session create from the daemon's advertised session
budget rather than the generic 30s request deadline, so a workspace create
can span a 30s capability preflight plus a 70s create request. The create
watchdog was still a fixed 75s derived from the old "2 x 30s" shape, so the
browser reported a create timeout on a spawn the daemon was still finishing
-- the bug class the SDK change fixes, one layer up. It also reddened the
required Test lane: actions.test.ts guards that the watchdog covers
preflight + create + headroom, a floor that is now 115s.

Derive the watchdog from the same advertised budget the restore path already
reads, so the two clients stop diverging whenever a budget moves, and drop
the "keep in sync with DEFAULT_FETCH_TIMEOUT_MS" comment that no longer
describes where the number comes from.

The guard that asserted a preflight and a create share one budget now
asserts what the test is named for: the create budget is sized from the
session budget, and the watchdog covers the sum plus headroom. The two
neighbouring watchdog tests derive their expected deadline from the same
helper instead of hardcoding 75000ms, so the next budget move does not
silently re-break them. The restore and load watchdogs are untouched --
they were already derived and still sit at 75s for an old daemon.

* fix(sdk): stop a short restore budget from shortening a session create

`limits.sessionRestoreTimeoutMs` is the operator's restore knob, and the
server returns an explicit `--session-restore-timeout-ms` verbatim with no
floor -- only the `--initialize-timeout-ms` branch clamps to the default.
Sizing create and worktree reset from it alone meant a legal config like
`--initialize-timeout-ms 60000 --session-restore-timeout-ms 5000` gave both
routes a 15s client deadline, under the 30s generic request deadline they
used before this change, so a create that always worked was now abandoned.

Give the two spawn routes their own resolver that floors the derived budget
at the generic request deadline. Restore keeps the unfloored budget: pinning
it low is deliberate, and flooring the shared helper would have raised it.

The Web Shell create watchdog gets the same floor on its create half, so a
short restore pin yields a 75s watchdog (30s preflight + 30s create + 15s
headroom) rather than 60s, which would have re-opened the gap this PR closes.

* docs(sdk): narrow the timeout claims this change falsifies

Three statements described create as a short-lived 30s route and the Web
Shell creation limit as a fixed 75s, which stops being true once create is
sized from the advertised session budget and the watchdog is derived from it.

Correct the fetchTimeoutMs option doc, the session-operation resolver's own
contract (the advertised field is the restore knob, so for a spawn it hints
at the handshake cost rather than bounding it, and a client that creates
before reading capabilities still falls back to the 70s default), and the
daemon configuration page's Web Shell deadline section -- including scoping
the "--initialize-timeout-ms does not raise the create request" claim to the
capability preflight, which is the only leg it still holds for.

* test(e2e): defer the channel-worker deadline to main

#14587 already widened this file's budgets on main (300s ceiling), so this
branch's per-test comment would only have lowered it back to 120s. The client
fix this PR carries is covered by the SDK and Web Shell unit tests.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmve7t0jzg8

* docs(daemon): correct the create-deadline claims and pin the new budgets

R2-1: the Web Shell README section this page links to still documented the
replaced 75s fixed action limit, so a host integrator sizing a create against it
would tear down a spawn the daemon still completes. R1-6: qualify the
initialize-timeout claim with the explicit restore pin, the 60000 floor and the
un-read capabilities fallback. R2-3: pin the deliberately unfloored restore
route. R2-2: pin the advertised budget reaching the create watchdog through the
real call chain.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmvegdno2gr

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-19 13:38:12 +08:00
callmeYe
0e2886a844
feat(daemon): support session-targeted Skill and Extension refresh (#14050)
* feat(daemon): support session-targeted Skill and Extension refresh

* test(daemon): include session refresh in capability baseline

* docs(daemon): align session refresh references with contracts

* fix(serve): handle selected refresh lifecycle and lookup edge cases

---------

Co-authored-by: Ethan Ye <ethanye@U-463KXYWL-1938.local>
2026-10-19 10:59:52 +08:00
haochen
b4614845f0
fix(web-shell): delete attachments from an empty composer (#13947)
* fix(web-shell): delete attachments from an empty composer

* fix: address review feedback for #13947

* fix: follow up review findings for #13947

* fix(web-shell): preserve draft boundaries during keyboard deletion

---------

Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-10-16 18:23:26 +00:00
易良
808722d9d7
feat(web-shell): add side task archive controls (#14300)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
* feat(web-shell): add side task archive controls

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): harden side task archive controls

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(integration): pin session_archive_idle_guard in the serve capability baseline

capabilities.ts registers session_archive_idle_guard and server.test.ts already
lists it, but the second baseline list in the no-AK integration gate was not
updated, so the deep-equal diff failed on exactly that one capability.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmv9k9mxh7h

* fix(web-shell): archive and restore side tasks in their owning workspace

The side-task archive/restore callbacks called the unscoped client, whose
routes resolve ids against the legacy-primary catalog. A task owned by a
secondary workspace answered notFound without any state migration, and
because both callbacks counted notFound as a successful removal the UI
cleared state anyway. Group each request by the task's workspaceCwd and
call through the workspace-qualified route (standalone tasks keep the
legacy path); notFound now surfaces as a per-session error and the task
stays in the catalog.

Regression pins: qualified routing for archive and restore with a
secondary-workspace task and a standalone task in one call, and a
notFound answer that keeps the tab and reports the session id; the three
pre-existing archive tests move to workspace-scoped spies.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): address review follow-ups - separator, removal-set reset, overview batch archive

* docs(design): sync side-task archive design with workspace-qualified routing

Review pointed out that the design pair still described the pre-fix
contract: it listed not-found among the outcomes that leave the active
list, and never stated that archive and restore must be
workspace-qualified. That missing sentence is the same omission that
allowed the secondary-Workspace bug, so the docs now carry it.

Both languages state that each callback groups ids by the task's own
workspaceCwd and calls the workspace-qualified route, that ids with no
recorded workspace keep the legacy unscoped call, and that a notFound
answer now keeps the task listed and reports a per-session error instead
of clearing UI state for an operation the daemon never performed.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-17 00:05:57 +08:00
易良
a84dbff23e
feat(providers): add QwenCloud as the first /auth entry (#14073)
* feat(providers): add 千问AI平台 / QwenCloud as the first /auth entry

Adds QwenCloud as a new top-level provider group ahead of Alibaba
ModelStudio, following the product ruling in #14025: the two entries
coexist rather than one replacing the other, so saved ModelStudio
credentials, endpoints and model selections keep resolving to their
existing presets and no migration is needed.

Two new presets share the ModelStudio wizard shape (Access Method ->
Platform -> API Key -> models):

- qwencloud-standard  (pay-as-you-go, `sk-` / `sk-ws-` keys)
- qwencloud-token-plan (Credit subscription, `sk-sp-` keys)

Each offers China (千问AI平台) and International (QwenCloud) as an
explicit platform choice, so the UI language never decides the endpoint.
Coding Plan is not offered here — it is a ModelStudio product.

Endpoints were taken from the platform's own documentation and probed
before landing (all four return the DashScope `invalid_api_key` error
envelope):

- https://maas.qianwenaiapi.com/compatible-mode/v1
- https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1
- https://maas.qwencloudapi.com/compatible-mode/v1
- https://token-plan.maas.qwencloudapi.com/compatible-mode/v1

The new hosts are added to the DashScope provider detection and the
web-search endpoint allowlist so the built-in `web_search` backend keeps
working; both new env keys are added to the no-AK CI gate's cleared-env
list. Group wiring is mirrored across the ink and OpenTUI dialogs, the
VS Code provider picker, and the serve catalog consumed by Web Shell.

Closes #14025

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): add QwenCloud auth strings to en and zh-TW locales

The new auth entries introduced in the previous commit were only added to
zh.js. Since en.js is the source of translation keys and both zh and zh-TW
enforce strict key parity, the i18n check failed with "Extra key in zh.js"
for all eight strings.

Add the same keys to en.js as identity mappings and to zh-TW.js with
Traditional Chinese values, so all three locale files have an identical key
set.

* test(cli): follow the QwenCloud row in the OpenTUI auth menu tests

The new first /auth entry shifts the main menu by one row, but the tests
addressed the sub-menus by position, so most flows landed a row early and
the region-height assertions still named the old first row.

* feat(providers): localize the QwenCloud /auth entry label per locale

The top-level /auth entry was the only mixed CJK/Latin label in the menu
("千问AI平台 / QwenCloud"), and because en/zh mapped that key to itself it
was effectively hardcoded mixed in every locale. Route it through i18n
instead: en and other locales show "QwenCloud", zh shows "千问AI平台",
zh-TW shows "千問AI平台". Locale-neutral surfaces (serve catalog, VS Code
provider picker) carry the single Latin brand; the Chinese brand stays
visible in the explicit platform step ("China — 千问AI平台") and the docs.
The step-title flowTitle now goes through t() so the localized brand also
appears in "… · Step 1/3 · …".

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(providers): register QwenCloud hosts in telemetry and reasoning classifiers

The QwenCloud MaaS hosts and env keys were registered in the two host gates
this PR edits, but two further independent classifiers of the same hosts did
not learn about them, and both misclassify the entry this PR makes the
first-position default:

- telemetry/gen-ai-provider.ts had no providerFromHostname match for
  maas.qianwenaiapi.com / maas.qwencloudapi.com and no PROVIDER_BY_ENV_KEY
  entry for QWENCLOUD_API_KEY / QWENCLOUD_TOKEN_PLAN_API_KEY, so
  resolveGenAiProviderName fell through to its `default: return 'openai'`
  arm. Every span and log record for a QwenCloud install was attributed to
  OpenAI, while the identical ModelStudio path resolved to 'dashscope'.
- core/reasoning-overrides.ts keeps its own inline hostname regex without the
  new hosts, so a user-authored partial reasoning override on a qwen* model
  behind a QwenCloud baseUrl selected 'qwen-chat-template' -> toggleOnly ->
  invalidReasoning, which captureLatestReasoning catches and warns. The
  override was silently discarded; the same declaration on
  dashscope.aliyuncs.com resolves to 'dashscope-effort'.

Also widen the SDK-side mirror DaemonAuthProviderCatalog.groups[].id to
include 'qwencloud', which packages/cli/src/serve/types.ts and
auth-provider-helpers.ts already emit as the first group, and derive
web-shell's AuthGroupId from that union instead of re-declaring it, so the
SDK type is the single source of truth. A second literal union in web-shell
is why nothing reddened in-repo while the daemon emitted a group id its own
published wire type declared impossible.

The new suffixes go inside the existing anchored (^|\.)...$ group, so a forged
maas.qianwenaiapi.com.evil.com cannot match while the legitimate token-plan.
subscription subdomains still do. Both env keys map to the existing
'dashscope' provider value rather than a new one.

The hosts stay literals in gen-ai-provider.ts rather than importing
QWENCLOUD_MAAS_HOSTS: that module is a zero-import telemetry leaf, and
dashscope.ts already imports core/reasoning-overrides at runtime, so importing
back would cycle.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmv8fwg2z5f

* test(providers): pin QwenCloud host classification and registry membership

Cover behaviour this PR added that no test observed:

- dashscope.test.ts: the QwenCloud MaaS pay-as-you-go and token-plan hosts
  are DashScope origins, plus dot-boundary lookalike negatives.
- web-search.test.ts: the same four hosts pass the web-search gate.
- qwencloud-standard/token-plan.test.ts: both presets are registered in
  ALL_PROVIDERS and QWENCLOUD_PROVIDERS, and the qwencloud access-method
  group excludes Coding Plan (replaces a constant-true id/label check).

Each new assertion is mutation-verified: dropping isQwenCloudOrigin from the
dashscope return, weakening its host match, dropping the QWENCLOUD_MAAS_HOSTS
spread, removing the two registry entries, or flipping codingPlanProvider
uiGroup each turns exactly the intended test red.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmv8mbxgn5t

* test(providers,cli): pin QwenCloud preset separation and the auth catalog group

Coverage for lines this PR itself added (review round 1: R1-21, R1-26, R1-15):

- qwencloudStandardProvider.ownsModel is cross-checked against its QwenCloud
  Token Plan sibling and against a foreign envKey on its own endpoint. The two
  presets share uiGroup, catalog and protocol bucket, so that envKey gate is
  the only discriminator; dropping it kept the suite green while
  applyModelProvidersPatch would claim a Token Plan install.
- buildAuthProviderCatalog gets its first test: the QwenCloud group is first
  and carries both access methods. A misspelled group filter stayed green.
- The zero-row main-menu paint test now names row 0, which this PR re-pointed
  from Alibaba ModelStudio to QwenCloud while only rows 1 and 2 stayed pinned.

Each assertion was confirmed load-bearing by one mutation that the
pre-existing tests did not catch.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmv8oh38g5y

* docs(users): sweep the /auth menu enumeration in overview and quickstart

This PR re-numbered the /auth menu to four options in auth.md and
model-providers.md, but overview.md and quickstart.md still described three
entries with Alibaba ModelStudio as the recommended one (review round 1:
R1-4). The doc set contradicted itself, and a quickstart reader holding a
QwenCloud key was routed to the ModelStudio entry, whose endpoints reject it.

Both pages now list QwenCloud first and reserve the "recommended setup" claim
for it, matching auth.md.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmv8oh38g5y

* fix(providers): validate daemon credentials and close QwenCloud follow-ups (#14302)

* fix(serve): validate API key format before persisting in daemon install path

The CLI, ACP and VS Code surfaces each call provider.validateApiKey
before persisting credentials. The daemon route (POST
/workspace/auth/provider) did not, because validateApiKey is a function
on ProviderConfig and cannot be serialized to the wire — so no client
of this route can apply the check either.

Without this guard a key the provider's own entry rejects (e.g. a
Token Plan sk-sp- key aimed at the Standard API Key entry) gets
persisted silently, and the user only discovers the mismatch at the
first authenticated call.

Fix: call validateApiKey in the route after protocol validation and
before invoking installAuthProvider. Return 400 with code
'invalid_api_key' on failure, matching the other three surfaces.

Closes #14257 (item S3 / R2-13).

* fix(providers): address #14257 deferred Suggestions (D1/D2, T1/T3-T9, I1, P5, S6/S7)

Batch fix for actionable items from #14257:

Docs:
- D1: Add settings.json example to QwenCloud headless setup section
- D2: Add QwenCloud rows to web-search.md auto-activation table

Tests:
- T1: Pin flowTitle locale resolution in both dialog suites
- T3/T4: Assert QwenCloud sub-menu contents (Token Plan + Standard, no Coding Plan)
- T5: Add missing token-plan.maas.qwencloudapi.com endpoint to dashscope.test.ts
- T6: Add QwenCloud host tests to reasoning-overrides.test.ts
- T7: Assert providerState in qwencloud-standard preset test
- T8: Test ownsModel baseUrl conjunct independently of envKey
- T9: Test providerMatchesCredentials URL gate independently of envKey
- S7: Assert QwenCloud group in VS Code AuthMessageHandler picker

i18n:
- I1: Add validateApiKey messages to all 9 locale files

Product:
- P5: Fix qwen3.7-plus modalities to include video (matches alibaba-token-plan)

Code:
- S6: Update docstring enumerations in dashscope.ts and web-search.ts

Remaining items need human decisions: P1-P4 (wording/labels), S1-S5 (API surface),
T2 (ink suite gating is a pre-existing harness pattern).

* fix(auth): translate provider titles at render time

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(providers): handle optional preset models

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(providers): pin Token Plan providerState and ownsModel baseUrl leg

Addresses four review Suggestions on this PR:

- R1-10 (#14257 T7): the providerState stamp was asserted for the Standard
  preset only. Token Plan has two baseUrl options and a baseUrl-dependent
  modelNamePrefix, so stamp both routes.
- R1-11 (#14257 T8): the deciding baseUrl negative for ownsModel was added
  to the Standard suite only. The Token Plan negatives added here differ in
  envKey, so the baseUrl conjunct was never the deciding factor.
- R1-1: the web-search host note contradicted the two QwenCloud rows this
  PR adds to the table on the same page. Name the Model Studio and
  QwenCloud MaaS families the code actually accepts.
- R1-4: the daemon route comment claimed parity with the CLI/ACP/VS Code
  copies. Those resolve the endpoint first and validate the resolved URL;
  this route validates the client-supplied baseUrl. Comment only.

Both new Token Plan rows were mutation-checked one mutant at a time:
dropping baseUrl from resolveProviderState, stamping the provider default
endpoint instead of the requested one, and relaxing ownsModel to envKey-only
each turn exactly the intended row red. An additional cross-route
"versions must differ" row stayed green under all of them (model configs
already embed baseUrl), so it was dropped rather than shipped inert.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmv9zmmcj86

* test: make the QwenCloud witnesses discriminating; trim daemon key once

R1-9: the two Token Plan cases and the four lookalike cases never reached
the dashscope host regex - both blocks short-circuited in
validateReasoningDeclaration/validateReasoningCapabilities before
route.baseUrl was parsed, and the block comment misstated the Token Plan
endpoints as unregistered. Fold the Token Plan hosts into the QwenCloud
host table with a declaration that omits `thinking`/`profile`, so all four
registered endpoints now pin the anchored suffixes.

R1-5/R1-6: the Ink VIEW_TITLES fix had no CI-executed guard (every
keystroke case is skipped under CI=true) and its T1 case asserted only the
already-lazy flowTitle path. Add one ungated, keystroke-free render
assertion and assert the sub-menu heading between the two keystrokes.

R1-3: validate and forward the same trimmed key rather than validating the
trimmed value while forwarding the raw one.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmva623ox8i

* fix(auth): guard zh locale test cleanup, note hand-written provider caveat

R1-7: wrap the QwenCloud zh flow-title test body in try/finally so unmount()
and the locale reset still run when an assertion rejects, matching the shape
of the opentui twin added in the same change.

R1-2: note in the settings.json alternative that a hand-written provider entry
carries no capabilities stamp, so /effort offers the generic tiers rather than
the QwenCloud curated set.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmvaacf8g8p

* fix(providers): address #14073 round-3 deferred Suggestions

- docs/design/auth/motivation.md: QwenCloud-first /auth enumeration (S1)
- docs/users/quickstart.md: name QwenCloud credentials in Before you begin (S3)
- docs/users/configuration/auth.md: scope the Token Plan prefix rule to
  ModelStudio vs QwenCloud so Option 1 and Option 4 no longer read as
  contradictory (S4)
- qwencloud-standard.test.ts: correct the ownsModel comment — envKey and
  baseUrl conjuncts each exclude the sibling independently (S5)
- auth-provider-helpers.test.ts + alibaba-standard.test.ts: pin every
  catalog group's providerIds and the alibaba uiGroup (S6)
- web-search.test.ts: forged-suffix boundary cases for the QwenCloud host
  gate (S7)

S2 (6-latin-locale backfill) and S8 (web-shell group dict) verified as
no-ops: the sibling ModelStudio strings rely on the identical raw-key
fallback in the same locales/dictionary.

* fix(auth): localize provider API key errors

* fix(providers): seed Token Plan only with models its endpoint serves

A live GET /models on the China Token Plan route (real subscription key,
2026-10-16) serves neither kimi-k3 nor deepseek-v4-pro-0813 — the endpoint
offers kimi-k2.x and deepseek-v4-pro instead, matching the platform doc's
personal-edition table. The shared QWENCLOUD_MODELS seed list therefore
pre-filled two ids the wizard would install as dead entries whenever
discovery succeeded, and offered them in the offline fallback.

Filter the shared catalog down to the five confirmed-served models for the
Token Plan preset instead of duplicating stamps; the Standard (pay-as-you-go)
catalog keeps the full list, which no live data has falsified. Pinned by a
new preset test asserting the exact seed ids.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): preserve explicit models during provider discovery

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-16 21:34:43 +08:00
Boulanouar Walid
87de74860b
docs(tools): add looot to the web search MCP services (#13999)
Co-authored-by: 易良 <1204183885@qq.com>
2026-10-16 15:33:15 +08:00
ytahdn
87dcc4a708
feat(web-shell): add file-tree deletion with confirmation (#14158)
* feat(web-shell): add confirmed file deletion from file tree

* test(serve): include file deletion in capability baseline

* fix(web-shell): address file deletion review suggestions

* test(cli): synchronize child cancellation tests

* test(web-shell): refresh transcript budget after file actions

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-10-16 14:41:34 +08:00
ytahdn
17d5f774ea
feat(web-shell): add file creation and confirmed folder deletion (#14169)
* feat(web-shell): add file creation and confirmed folder deletion

* fix(web-shell): address file tree review and transcript regression

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-10-16 02:43:59 +00:00
Lucien
74033d0bd2
fix(sdk): release reconnect transports created after disposal (#13864)
* fix(sdk): release reconnect transports created after disposal

* fix(sdk): preserve closure errors during reconnect cleanup

* test(sdk): satisfy lint in reconnect disposal coverage

* test(sdk): model reconnect subscription failures during iteration
2026-10-14 14:33:22 +00:00
Long Jin
1ce7d71633
fix(core): preserve fuzzy edit line boundaries (#13484)
* fix(core): preserve blank lines after fuzzy edits

* test(core): cover repeated fuzzy edit matches

* fix(core): keep fuzzy edits within whole-line matches
2026-10-14 14:11:23 +00:00
Lucien
3319e819b9
fix(sdk-python): resolve Windows command shims on the effective PATH (#13862)
* fix(sdk-python): resolve Windows command shims on the effective PATH

* style(sdk-python): format Windows shim regression assertion

* fix(sdk-python): secure Windows command lookup and cleanup

* style(sdk-python): align test import grouping with CI

---------

Co-authored-by: 易良 <1204183885@qq.com>
2026-10-14 14:03:28 +00:00
Pulkit Chowdry
7326ac6e0b
Add SerpApi to web search MCP services list (#13104)
* Add SerpApi to web search MCP services list

* Updated best for, added a information that is sent to SerpApi.

* Moved the information sent to SerpApi part in the description to align with the format followed by other listings.
2026-10-13 16:21:47 +00:00
qqqys
c3c38cf583
feat(web-shell): bridge trajectory history and live lifecycle state (#13952)
* feat(web-shell): bridge trajectory history and live lifecycle state

* fix(web-shell): preserve trajectory recovery and bound transcript bundle

* fix(acp-bridge): preserve request lifecycle frames across compaction

* fix(web-shell): address trajectory recovery review findings

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-10-13 11:28:30 +00:00
haochen
008c0de746
docs(sdk): clarify empty core-tool allowlists across SDKs (#13949)
Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
2026-10-12 13:19:59 +00:00
Long Jin
f8c99414b2
fix(core): report monitor startup failures as tool errors (#13460)
* fix(core): report monitor startup failures as tool errors

* fix(core): classify monitor startup failures consistently
2026-10-11 14:21:47 +00:00
Shaojin Wen
23274922f5
feat(web-shell): add Send now for queued mid-turn messages (#13681)
* feat(web-shell): add Send now for queued mid-turn messages

A message typed while a turn runs waits for the model response that is
streaming and for its tool batch, so a correction typed into a long answer
reached the model seconds later, and Stop was the only faster path.

Typing keeps that behavior: the message stays queued, editable and
deletable until the next tool boundary. A new Send now action on a queued
row asks the daemon to deliver the queued messages at once.
POST /session/:id/mid-turn-messages/send-now, advertised by
session_mid_turn_send_now, sends the agent craft/midTurnSendNow. While a
foreground turn streams a response that has not started a tool call, the
agent drains the user's messages, cuts the response short, keeps the part
already streamed, and answers in the same turn. Running tools are never
interrupted, a response that started a tool call is never cut, queue-only
steering is not taken early, and a request is scoped to the turn that
received it. Input taken by a turn that is then stopped or fails is kept
in that turn's conversation.

The TypeScript SDK gains sendMidTurnMessagesNow on both clients.

* fix(web-shell): address review findings on Send now

- Restart the Stop-hook state when a continuation answers input that
  cut its response short, so the next Stop check is not hook-forced and
  the consecutive-block count and reasons start over.
- Give input a finished turn keeps unsent a 2 s resolution deadline of
  its own; the deadline also cancels media bridges.
- Requeue a failed send-now drain in its original order, and log a
  send-now request the agent rejects.
- In Web Shell, reconcile a row whose message already left the queue
  and ignore a repeated click while a request is out.
- State the tool-call carve-out wherever Send now is described, and
  document goal turns, the narrow no-answer window and token accounting.
- Add tests for the review's gaps, drive the drain deadline with fake
  timers, and add the route to the multi-workspace failure tables.

* fix(web-shell): keep send-now input a stopped tool boundary leaves unbuilt

When a turn is stopped while a tool-boundary drain resolves a message,
the drain now hands back the messages it has not built that an early
send-now drain took, so the end-of-turn keep records them. Input the
boundary drain took for itself is still dropped there, as before.

- Tests: the stopped boundary keeps that input once and carries nothing
  into the next turn; a Goal continuation is cut; a background
  notification turn is not; a failed send-now drain does not requeue a
  message that left the queue meanwhile.
- Bridge and SDK contracts: the cut applies to a foreground turn's
  streaming response, and `requested: true` does not mean a cut.
- Protocol and design docs: the timed-out send-now drain, resending a
  settled id, the 10-message drain limit, and the tool-boundary drop.

* test(web-shell): pin send-now behavior questioned in review

- Assert that cron and background-notification turns never open their
  response to interruption and that a send-now request arms nothing
  while they stream.
- A response cut short by queued input still answers that input when it
  ends with an error, while an error from a response its drain did not
  cut remains the turn's error. Explain in `watch` and the design doc why
  the cut response's error is absorbed: the cut does not always arrive as
  an AbortError, and the response was given up.
- A message whose deletion missed while a failing drain held it is still
  delivered, which matches the `{ removed: false }` answer the client
  got. Document this in the protocol reference.
- Rename a requeue test to say it covers a queue-only entry.

* docs(web-shell): scope the abort invariant and drain-held removals

- The main prompt loop's catch states that other AbortErrors surface.
  Note that a response cut short by queued input reaches that catch only
  when the turn itself is stopped meanwhile, because `watch` absorbs
  whatever a cut response throws.
- In the force-drain paragraph, add the drain-held case of
  `{ removed: false }`. Such an id must not be re-posted, and its listing
  in `settledMessageIds` is not final, because a failed drain returns it
  to the queue without an event.
2026-10-11 06:24:48 +00:00
Shaojin Wen
e31d5500f1
feat(managed-agent): H6a schedule and automation run record contract (#13536)
* feat(managed-agent): H6a schedule and automation run record contract

The H6a slice of #12827, ahead of any producer: both bodies register and
replay in TypeScript and Java while both domains stay disabled for
submission.

- managed-schedule v1 (chain scheduleId): owner scope, bounded goal, a
  five-field cron with an IANA timezone, append-only definition revisions
  (each successor exactly +1) with their digest, the prompt resource
  reference, the persistent/per_run target mode with the target frozen
  exactly for persistent, the skip/queue_one/allow overlap and
  none/latest/bounded catch-up vocabularies (the limit set exactly for
  bounded), an enabled flag, and a purely logical run block that freezes
  the definition for good once terminal.
- managed-automation_run v1 (chain automationRunId): the pinned
  definition revision, the closed occurrenceKey union — schedule:<slot>
  as a canonical UTC instant to the second (open question 1 answered),
  manual:<commandId>, webhook:<eventId> registered and refused — the
  frozen sessionMode/target, and the H0b run block: a durable dispatchId
  from the first revision, no executionCallId or definition pin, the
  delivery line past planned only once the run ended so a delivery never
  re-runs the model.
- The shared managed-automation-record/1 fixtures pin keys, fixed keys,
  vocabularies, 56 body cases and 22 successor transitions, replayed by
  the TypeScript gate and the Java contract test; the seven timezones
  anchor in each host tz database.
- The projection registry and its v1 fixtures grow the two bodies under
  additionalRecordBodies, as do the Java RECORD_BODIES; the domains stay
  out of MANAGED_SESSION_ENABLED_DOMAINS and commitExtensionRecord still
  refuses them — enablement is the H6b producer slice's explicit step.

Design: docs/design/2026-10-04-managed-automation.md (bilingual update).

* fix(managed-agent): close the H6a review round's contract findings

Round-1 review of the H6a record contract (#13536): one Critical, the rest
Suggestions, each verified against this head.

- R1-1 (Critical): Java boundedText charged one byte per unpaired
  surrogate (the default encoder folds it to '?') while Node's
  Buffer.byteLength charges the three of U+FFFD, so the two validators
  disagreed on identical wire bytes for a 1366-surrogate goal. Java now
  charges the same three bytes per unpaired surrogate. Shared boundary
  cases pin both sides (1365 accepted, 1366 high and low refused).
- R1-6/R1-7: the cron validator now checks per-field atoms semantically
  — digit/list/range/step atoms only, values inside each field's bounds,
  steps within 1–max, ranges never wrapping — with one honest refusal
  message per rule instead of one message for all. Fixtures pin
  */0, */60, - - - - -, 99 99 99 99 99, 0 24 and 17-9.
- R1-8: the timezone refusal no longer claims IANA validation; the
  contract is shape-only on purpose (hosts disagree which names resolve),
  named in the body comment and the design document.
- R1-9: the seven anchored timezones now replay through the validator as
  body cases, so the host tz anchors witness implementation behaviour,
  not only host-DB membership.
- R1-11: Java runs the catchUpLimit value check before the bounded
  pairing rule, the same order as TypeScript.
- R1-12: pinned that an automation_run task row reads definitionRevision
  null (the run block holds no definition pin); the authoritative
  revision lives on the record — asserted in both contract suites and
  named in the design document.
- R1-3: design documents synced to the shipped contract in both
  languages (occurrence identity, automationRunId chain identity,
  terminal freeze, count_limit framing, delivery target, per_run target,
  provenance marker, DST canonicalization tables reassigned to H6b).

Not changed, with replies on the threads: promptRef store verification
stays out of the record contract (cross-scope reference, resolved at H6b
definition admission), and timezone names stay shape-checked (host tz
databases disagree; semantic resolution pinned to H6b admission).
2026-10-07 08:29:15 +00:00
LUOSENGWA
8505479d87
docs(serve): fix stale permission-route note and document pending-prompt events (#13281)
docs/users/qwen-serve.md still described session-scoped permission routing
as Stage 1.5 future work and pointed integrators at the legacy process-global
vote route. Rewrite the note: the session-scoped route ships (Wave 4 / F3) and
is the recommended form for new multi-workspace integrations; the legacy route
remains for pre-F3 single-workspace clients with its 404 semantics spelled out
(the same body as a lost vote for a session owned by another runtime).

Add event-reference entries for pending_prompt_added / pending_prompt_started /
pending_prompt_completed to docs/developers/daemon/09-event-schema.md: payload
fields, the state union, the idle-session no-event caveat, and the "queue-view
bookkeeping, not a turn terminal" guidance (correlate turn_complete /
turn_error by promptId for completion).

Derived from the field notes and triage in #13279.
2026-10-07 04:45:41 +00:00