Commit graph

238 commits

Author SHA1 Message Date
jinye
542ef73fd3
chore(serve): Log session continuation admissions (#8932)
* chore(serve): Log session continuation admissions

Refs #8923

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

* test(serve): Tighten continuation log assertions

Refs #8923

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-11 14:34:08 +00:00
易良
89708569f7
fix: add structured error code to SessionNotFoundError for session-closing retry (#8884)
* fix: add structured error code to SessionNotFoundError responses

PR #8864 retried session switches while the target session is closing,
but relied on fragile string matching against the daemon's error message.
This commit:

1. Adds a `code` property to `SessionNotFoundError` — automatically set
   to `'session_closing'` when the extra message mentions "closing",
   otherwise `'session_not_found'`.

2. Includes `code` in the HTTP JSON response body so clients can
   distinguish closing (transient) from genuinely missing sessions
   without depending on error message text.

3. Updates the WebUI retry check in `DaemonSessionProvider` to use
   `errorBody.code === 'session_closing'` instead of matching
   `endsWith('The session is closing; retry after close completes')`.

4. Fixes an inconsistent error message in `rewindSession` that used the
   short `'The session is closing'` without the retry suffix.

Closes: #8864 (follow-up)

* fix(daemon): expose session closing code

* docs(serve): document session closing codes

* fix(acp): preserve closing code after restore waits

* fix: restore class pin in bridge test and update error taxonomy

- Add toBeInstanceOf(SessionNotFoundError) alongside toMatchObject
  to preserve the envelope type assertion
- Document session_closing code in 18-error-taxonomy.md

* chore: drop unrelated merge formatting
2026-08-11 13:01:25 +00:00
ytahdn
1a2c5026b2
fix(web-shell): reconcile mid-turn messages with daemon state (#8798)
* fix(web-shell): reconcile mid-turn messages with daemon state

* test(serve): update mid-turn capability expectation

* test(mid-turn): cover reconciliation mutants and restore serve protocol docs (#8798)

* fix(serve): close mid-turn promotion admission and delivery gaps (#8798)

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

* fix(serve): keep anonymous mid-turn enqueues off the shared queue surface (#8798)

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

* fix(sdk): account for mid-turn APIs in bundle budget

* test(acp-bridge): use vi.waitFor for async prompt-drain assertions (#8798)

* fix(serve): reconcile mid-turn steering safely

* fix: reconcile mid-turn messages safely

---------

Co-authored-by: ytahdn <ytahdn@users.noreply.github.com>
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-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>
2026-08-11 07:24:56 +00:00
Shaojin Wen
95e17691a9
chore(serve): remove the /demo debug page (#8805)
* chore(serve): remove the /demo debug page

The daemon has shipped a real browser UI for a while: `resolveWebShellDir()`
finds the bundled Web Shell assets and `mountWebShellAssets()` serves them at
`/`, so `qwen serve` already opens onto a full client. `/demo` stayed behind as
a 663-line inline-HTML console covering the same ground with none of the
reach — nobody drives the daemon through it, and `npm run dev:daemon` starts
the Web Shell dev server rather than the demo page.

Keeping it around costs more than the dead code. It is the only file in the
tree that pairs an event log with daemon HTTP, so work that starts as a Web
Shell observation lands there instead: #8762 was found while running `/review`
through the Web Shell and was fixed entirely inside the demo page's rendering,
with "no Web Shell changes" in its own risk note. Deleting the page removes
that decoy.

Nothing is lost for protocol-level debugging: `GET /session/:id/events`
streams the same raw frames the Events tab printed.

`/health` shared `routes/health-demo.ts` with the demo handler, so the module
is now `routes/health.ts` / `createHealthRoutes()` and drops its `getPort`
dependency. The rate-limit exemption, the boot breadcrumb, and the daemon docs
lose their `/demo` arms; the loopback self-origin shim regression test already
asserted through `/health` and only needed its title corrected.

* test(serve): pin the removed /demo contract and the pre-auth surface

Review follow-up. Three of the removal hunks shipped ungated, and two doc
sentences the removal rewrote were describing the pre-auth surface wrong —
both before and after the edit.

Deleting the `/demo` route took its assertions with it, so nothing failed if
the handler came back: the Web Shell suite only exercised a generic deep link,
and the rate-limit exemption could be widened again with the suite still green.
`/demo` is now pinned as what it became — an ordinary unknown path: a
non-navigation request 404s, a browser navigation is answered by the SPA
fallback like any other deep link, and once a token is configured (with or
without `--require-auth`) that navigation is refused with 401, because the
fallback sits behind the bearer. The rate-limit test pins that `/health` is the
only exempt GET, so re-adding a second pre-auth page to the predicate fails
instead of silently escaping the limiter. Each new assertion was checked by
reverting the hunk it guards and confirming it goes red.

The `--allow-origin '*'` warning and both `--allow-origin` doc paragraphs
enumerated `/health` as the residual tokenless surface and said nothing about
the Web Shell static assets, which are mounted before the bearer in every
launch mode and stay reachable even under `--require-auth` — the enumeration
also claimed `/health` stays pre-auth on non-loopback binds, where it is
registered behind the bearer and 401s. A probe across all three launch modes
established the actual matrix; the warning and the docs now match it and name
`--no-web` as the way to remove the residual browser surface. The warning text
is asserted by a test for the first time.

* fix(serve): correct Web Shell doc claims and re-pin the pre-auth CORS wall

Review follow-up. The removal rewrote the daemon docs around the Web
Shell, and three of the rewritten claims did not match what the runtime
actually does: §1 never said how the bearer reaches the browser (with
auth on, the plain URL loads a shell whose every API call 401s), §8
called the shell writable on any bind (on a non-loopback bind without
`--allow-origin` its POSTs hit the CORS wall and 403), and §8 served
`/session/:id` without the document-navigation qualifier its own code
enforces. The §9 call-chain diagram also still listed the deleted
`/demo` route, the developer flag references had no `--web`/`--no-web`
row despite the new guidance pointing at the flag, and both design docs
listed the JSON body parser ahead of post-auth `/health` while
`createServeApp()` registers them the other way round.

The deleted `/demo` CORS test was also the only assertion that a
pre-auth page sits behind the Origin wall — every surviving Origin test
targets an API path. Re-pin it for the shell root so a mount-order
regression fails instead of exposing the pre-auth HTML surface
cross-origin.

* fix(serve): finish demo rename sweep and scope pre-auth shell claims to loopback

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-Coder <qwen-coder@alibabacloud.com>
2026-08-10 13:31:15 +00:00
jinye
fa8cae5418
fix(serve): Allow approved external built-in text writes (#8852)
* fix(serve): allow approved external built-in text writes

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

* fix(serve): keep write provenance off startup bundle

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-10 12:19:58 +00:00
Shaojin Wen
77bd04bd61
fix(acp-bridge): bound live journal replay chunks (#8801)
* fix(acp-bridge): bound live journal replay chunks

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

* test(core): isolate shell retention sidecars

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

* test(integration): cover aggregated live journal replay

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

* test(core): isolate registry sidecars

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

* fix(acp-bridge): keep unmodeled chunk keys out of live journal merges

The merged live-journal entry is rebuilt by spread-merging the first and
last source events, which was only safe because producers happen to emit
exactly {sessionUpdate, content, _meta?} on mergeable chunks. Gate the
merge on that key set so unmodeled data/update fields keep entries
discrete instead of leaking into the aggregate. Also clarify the
live-journal truncation marker: its retained/truncated counts describe
source events, while the limits count replay entries.

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

* fix(acp-bridge): align replay boundaries for discrete and meta-shaped chunks

Turn compaction folded discrete thought chunks (and non-todo-stop-guard
discrete messages) into one text slot with the last chunk's meta, while
the live journal keeps every discrete chunk separate — resyncing from
compactedReplay mis-attributed text across background tasks. Guard both
chunk paths with the same hasDiscreteMessageMeta predicate the live
journal already uses. Also align the merge gate with the shapes the
shared meta builder emits: tolerate update-level timestamp/
serverTimestamp and qwenTranscript.planToolCallId, and treat an
empty-string parentToolCallId as top-level the way the extractor does.
Document that byte-cap truncation drops whole entries, so the retained
tail can be much smaller than the cap, and tighten the integration
assertion that became vacuous once entries merge source chunks.

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

* fix(acp-bridge): merge subagent chunks in live journal replay

SubAgentTracker stamps every streamed subagent fragment with
{ parentToolCallId, subagentType }, but the live-journal merge gate
only modeled parentToolCallId, so subagent chunks stayed discrete and
a high-fragment subagent stream could still trip history_truncated.
Model subagentType as a carried label (like the completed-turn path,
which merges by parentToolCallId alone) and cover the producer wire
shape in the merge tests.

* fix(acp-bridge): preserve TextContent metadata in live journal replay (#8801)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-10 09:52:17 +00:00
Shaojin Wen
b314d01f2d
fix(web-shell): stop rendering unrecognized daemon events in transcripts (#8812)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* fix(web-shell): stop rendering unrecognized daemon events in transcripts

The daemon UI normalizer projects any frame it has no case for into a
`debug` event carrying a raw JSON dump. webui's ChatViewer drops those
blocks, but Web Shell renders `status` and `debug` together as system
info, so every event kind the daemon ships ahead of the UI surfaces as
unreadable JSON in the middle of the conversation. This has been patched
per-symptom three times now: two string-prefix suppressions inside
`isIgnoredWebShellStatus`, plus #8790 for `usage_update`.

Give the normalizer's debug events a structured `debugReason` and let
Web Shell branch on it instead of pattern-matching text:

- `unrecognized_event` / `unrecognized_session_update` — the daemon runs
  ahead of this client; developer diagnostics, not conversation content.
  Web Shell no longer renders them.
- `malformed_payload` — a frame the client does know arrived unusable.
  That is a real defect signal, so it stays visible.

Debug events dispatched by clients themselves, such as Web Shell's own
model-switch summary, carry no `debugReason` and keep rendering.

The two `(unrecognized daemon event)` prefix checks are now covered by
`debugReason` and are removed; the `Model switched: ` check stays, since
`model.changed` projects to a `status` block rather than a debug one.

* fix(sdk): classify a discriminator-less session_update as malformed

Review of #8812 caught a hole in the new classification: `session_update`
payloads such as `{}` or `{ sessionUpdate: 42 }` reach the default branch
with `kind === undefined`, and stamping them `unrecognized_session_update`
made Web Shell hide the only diagnostic a malformed frame produces.
Reserve the unrecognized reason for a real unknown string kind.

Also update the top-level default-case comment, which still pointed
adapters at the debug text prefix, and add a reducer-level test proving
`debugReason` survives the UI-event → transcript-block boundary: the
normalizer tests inspect events and the Web Shell tests build blocks by
hand, so dropping the spread in transcript.ts would leave both green.

* fix(web-shell): keep filtering legacy debug blocks, tighten the reason split

Four review findings from #8812:

- `WebShellTranscript` is a public entry point taking already-projected
  blocks, so blocks from an SDK predating `debugReason` still arrive with
  no reason and started rendering again when the prefix checks were
  removed. Fall back to the stable ` (unrecognized daemon event): ` marker
  when no reason is present — which covers every unrecognized event type,
  not just the two previously suppressed by name. The old-shape fixture is
  restored (adding `debugReason` to it had hidden this path) and a
  dedicated legacy test now pins it.
- A whitespace-only discriminator is truthy, so `sessionUpdate: ' '` was
  classified unrecognized and hidden. Gate on `trim()`, matching the
  convention `getFirstString` already uses.
- Add the mirror invariant for the reducer: a client-dispatched debug
  event must produce a block with no `debugReason`. Defaulting the field
  in `appendStatusBlock` otherwise passes every other test while tagging
  the model-switch summary unrecognized.
- Guard the outermost public re-export. A type-only guard would not hold —
  vitest erases `export type` through esbuild and this package's tsconfig
  excludes `test/` — so ship the union as `DAEMON_UI_DEBUG_REASONS`,
  matching `DAEMON_ERROR_KINDS`, and assert it at runtime.

* fix(web-shell): suppress legacy usage_update/a2ui blocks with no debugReason

Follow-up verification on #8812 pointed out the marker fallback does not
close the original report. #8790 stopped the SDK inserting new
`usage_update` blocks, but `WebShellTranscript` renders whatever blocks its
caller passes, so a transcript persisted or projected before that still
holds them and the spam returns after upgrade.

The legacy `session_update` projection is `<kind>: <json>` with no marker to
key on, so match those by kind name instead. The list is closed on purpose —
`usage_update` and `a2ui`, the two known to have leaked — and requires the
`: {` shape, because a generic `<word>: {` rule would swallow legitimate
diagnostics. Blocks the normalizer classified still win on `debugReason`,
so `malformed_payload` and client-dispatched debug blocks stay visible.

Mutation-checked in both directions: dropping the fallback fails the legacy
test, and loosening the prefix to bare `usage_update:` fails the test that
pins prose and classified blocks staying visible.

* fix(web-shell): match the legacy projection shape, not a quoted marker

The legacy fallback was too broad in two ways, both reachable. It ran for
`status` blocks as well as `debug` ones, because this helper is called from
the shared `case 'status': case 'debug':` arm, and it matched the marker as
a substring anywhere in the text.

Probed at df0b757c3c: all four of these were dropped — a status line
quoting the marker, a legacy malformed payload relaying an upstream peer's
message that contains it, a client-dispatched summary quoting it, and a
status block whose text starts with `usage_update: {`. Exactly the
diagnostics this PR promises to keep.

Scope the text match to `debug` blocks, and anchor it to the whole legacy
projection (`<event-type> (unrecognized daemon event): <json>`) instead of
the bare marker. Classified blocks still win on `debugReason` before any of
this runs.

Mutation-checked: dropping the `kind === 'debug'` guard and restoring the
substring match each fail the new negative test.

* fix(web-shell): match legacy projections with non-object payloads

The anchored legacy pattern required the payload to start with `[`, `{` or
`"`, which only holds for objects and arrays. `DaemonEvent.data` is
`unknown`, and `stringifyJson` returns strings verbatim, serializes
primitives as `42` / `true` / `null`, and yields `''` for `undefined` — so
every non-object payload bypassed the compatibility fallback and rendered.

Drop the leading-character constraint. The event-type prefix plus the fixed
phrase, anchored at the start, is specific enough on its own, and the
negative cases for quoted markers and status blocks still pass.

Regression test covers object, array, string, number, boolean, null and
empty payloads. Mutation-checked: restoring the character class fails it.

* fix(web-shell): hide debug blocks by the unrecognized_ reason category

The debugReason filter enumerated the two current `unrecognized_*` values,
but the SDK contract this PR adds names reasons by category: `unrecognized_*`
is forward-compat noise to hide, `malformed_*` a defect signal to keep
visible. A reason a newer SDK adds would compile silently against the
two-literal comparison and render raw JSON again with both suites green.

Match the category prefix instead. Also drop the now-dead marker branch in
MessageList's mid-turn hide check: every block carrying that prefix is
filtered upstream in the adapter (reason-stamped via `debugReason`, legacy
via the anchored pattern), and the dedicated normalizer case emits a status
event keyed by `source`. Document `DaemonUiDebugReason` beside the sibling
closed enums in the daemon-ui docs, whose forward-compat bullet still
described the unstamped projection.

Regression test pins both directions of the category contract with reasons
outside the current enum.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-10 00:51:57 +00:00
jinye
a810f7e16c
fix(serve): Make session restore timeouts safe and observable (#8691)
* fix(serve): make session restore timeouts safe

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

* fix(cli): restore missing core mock exports in the ACP worktree suite

The restore-tracing change added `extractDaemonTraceContext` and
`withDaemonSpan` to `acpAgent.ts`, but `acpAgent.worktree.test.ts`
replaces `@qwen-code/qwen-code-core` with a full mock factory that never
listed them. `loadSession` then failed on an undefined export, taking all
three cases down and producing teardown rejections from the half-built
agent. The sibling suite was updated; this one was missed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): bound and disambiguate the abandoned restore lifecycle

Four follow-ups from review of the restore timeout work.

A startup budget may now raise the restore budget but never lower it.
Taking an explicitly configured `initializeTimeoutMs` as the restore
fallback meant a deployment that tightened its child-initialize check
still inherited a sub-default restore deadline — exactly the failure this
change exists to remove. An explicit `sessionRestoreTimeoutMs` still wins
outright, including below the default, for deployments that want restore
to fail fast. Validation now names the field actually at fault.

A restore fenced behind a timed-out predecessor is no longer reported as
an ordinary in-flight restore. It carries `reason:
awaiting_abandoned_cleanup` and a retry hint of one restore budget
(capped at 120s) instead of the ordinary 5 seconds, because the fence
cannot clear until the non-cancellable ACP request settles and a 5-second
cadence just spins the caller against a 409 it cannot resolve.

Whether a channel is condemned is now derived rather than sticky. A
timeout recorded `emptyReapPending` permanently, so any channel that had
ever seen one was guaranteed to be reaped once its remaining work
drained, forcing a cold respawn even when the late restore had landed and
closed cleanly. The reap condition is now computed from an outstanding
`unsettledAbandonedRestores` set, quarantine, or an ordinary pending
empty reap; real settlement clears the entry and hands the channel back
to the configured idle policy.

Abandonment no longer retains ownership without bound. One further
restore budget after the deadline, a still-unsettled restore marks the
channel `restoreSettlementOverdue`: existing sessions and workspace
control keep working, but fresh session work is refused so the channel
can drain, since closing the transport is the only lever that releases a
permanently hung request. Releasing capacity while hidden work runs would
allow unbounded oversubscription, and force-killing a channel with live
siblings would reintroduce the failure this work removes, so neither is
done. Fresh-admission blocking is now scanned across alive channels
rather than tracked in a single reference, so a second condemned channel
cannot silently displace the first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): keep the abandoned restore lifecycle off ids it no longer owns

Two correctness gaps in the abandoned-restore machinery introduced by this
PR, both reported by automated review and both confirmed by mutation
testing (each new test fails when its fix is reverted).

A caller-supplied `sessionId` is used verbatim by the agent, but
`spawnOrAttach` never consulted `inFlightRestores`. A fresh spawn could
therefore take an id that a restore still owns, in either lifecycle phase.
The consequences were silent: `abandonedRestoreIds` suppresses session
updates, guardrail events, and child notifications, so the new session
would have registered successfully and then emitted nothing; and a late
`settleAbandonedRestore` would have closed and tombstoned it out from
under its owner. Such a spawn is now rejected with the same
`RestoreInProgressError` and reason the restore path uses, so the caller
gets the correct retry hint for whichever phase is holding the id.

The cleanup path is guarded independently, because the request-level check
only covers the id the caller asked for and a session registers under the
id the child returns. An abandoned restore never reaches
`createSessionEntry` — the deadline rejects before registration — so any
live entry under that id belongs to someone else. Cleanup now detects that
and returns without closing or tombstoning, releasing its own bookkeeping
instead.

The notification fence has no TTL and was only cleared by
`markRestoreInFlight`, which covers a subsequent restore and nothing else.
`createSessionEntry` now clears it for every registration route, so a
legitimate owner of the id is never handed a session that silently drops
everything the child sends it.

Also tightens two tests that could not observe the values they pin. The
SDK default restore timeout admitted any value in (30s, 70s]; it is now
split at the exact boundary, so collapsing the default onto the 60s server
budget — which would make the client abort race the daemon's own deadline
and cost the caller its structured 504 — fails. And the advertised-budget
propagation from capabilities through to the SDK call had no live-path
assertion; dropping the capabilities argument at the real call site left
every existing test green. The `as never` casts are replaced with typed
`DaemonCapabilities` values so a field rename fails typecheck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): let a condemned channel drain without its wedged child

Merging main's active-work close protocol (#8588) into this PR's abandoned
restore bound produced a deadlock that neither side has on its own, and the
conflict resolution was committed without running tests.

`maybeCloseIdleSession` now routes through `confirmChildUnheld`, which asks
the child whether it still holds work before closing a session nobody is
attached to. That is right in general and wrong for a channel this PR has
already condemned. `restoreSettlementOverdue` and quarantine exist precisely
because the child stopped being answerable, and their whole premise is that
visible work drains so the channel can be reaped — closing the transport is
the only thing that can release a restore we cannot cancel. Making that
drain depend on a round trip to the wedged child inverts it: a child stuck
in a non-cancellable restore is exactly the one that cannot reply inside
`ACTIVE_WORK_CLOSE_TIMEOUT_MS`, so the sessions never close, the channel
never drains, the reap never fires, and the bound never takes effect.

A channel condemned by the restore lifecycle now skips the round trip and
proceeds to local teardown. Nothing is attached to the session by then —
`maybeCloseIdleSession` gates on that — and the sibling-safety invariant is
untouched: this closes sessions whose clients have already left, it does not
force-kill a channel that still has live ones.

The regression test drives an overdue channel whose child never answers the
close-if-unheld probe and asserts the detach still reaps it. Reverting the
guard reproduces the deadlock as a test timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(serve): pin the restore-timeout contract the review found unasserted

Automated review identified eleven places where the restore-timeout work's
behavior was correct but unpinned — each with a mutation that ships green.
Every fix below was verified the same way: apply the mutation, watch the new
assertion fail, revert, watch it pass.

The timeout path's telemetry had no coverage at all, which is the sharpest
gap given that observability is what this work exists to deliver. A shared
recorder now asserts the public timeout result and its kill_empty-vs-
fence_shared signal, the late arrival, and the cleanup outcome for both the
closed and quarantined cases.

The deadline timer's cancellation on a successful restore was likewise
unpinned: deleting both `clearTimeout` calls kept the whole suite green,
while in production the stale timer fires one budget after a successful
restore and abandons a live session — fencing its frames, closing its event
bus, and emitting a spurious timeout. A success-path test now advances past
the deadline and asserts no second public result.

Three more bridge assertions proved less than they claimed: the concurrent-
restore case never checked that the abandoned restore settles, the
workspace-control case never checked that the deferred reap eventually
fires, and the resolver never pinned the accepting side of the MAX boundary
(a `>` to `>=` mutation rejects the largest legal delay at boot). The
workspace-control case also needed a positive channel idle budget, since
with the default zero the idle-timer kill substitutes for the reap junction
under test; its assertions are rewritten around the derived reap semantics
rather than the sticky flag they predate.

Outside the bridge: the scheduled-task timeout wiring had no test, so
deleting the arguments silently fell back to the helpers' own defaults; the
cold restore path never asserted that `live_restore_ms` is absent; the SDK's
per-request validation and its over-ceiling clamp were untested; the WebUI
watchdog test jumped straight to its own value, staying green for any
watchdog at or below it, including the 30s attach value that would recreate
the original symptom in the browser; and the two new known error types were
unexercised, so dropping either would relabel every restore-timeout and
quarantine error as unknown.

Two review items are deliberately not taken here and are recorded in the
design doc's non-goals instead: transcript materialization is still not
separately attributable from `config_setup`, which needs instrumentation
inside the core session loader that P1/P2 restructures anyway, and sibling
event-loop latency during a large restore remains unmeasured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): bound the condemned-channel close and complete the fence contract

Second automated review round, on the code the first round produced. One
Critical and twelve suggestions; all verified by mutation before and after.

**The Critical is a regression I introduced.** Letting a condemned channel
skip the bounded hold probe routed it into `closeSessionImpl`, whose agent
close is unbounded when it throws on failure — so the fix traded a bounded
wait on a wedged child for an unbounded one. A settlement-overdue channel
with an unresponsive child would hang `detachClient` forever, strand the
session in `closing`, never drain, never reap, and 503 every new session
until restart: strictly worse than before. `CloseSessionOpts` now carries an
`agentCloseTimeoutMs` that the condemned path sets, so a hang lands in the
existing unknown-outcome recovery, which kills the channel — the teardown
the drain was waiting for. The earlier test missed this because its fake
child still answered the plain close; it now answers nothing at all, and
asserts the detach itself returns.

**The fence was invisible on the transports clients actually use.**
`toRpcError` had no `RestoreInProgressError` case, so over acp-http and
acp-ws — which SDK negotiation prefers over REST — the fence degraded to an
opaque internal 500 with no code, reason, or hint, and the backoff contract
this work documents was impossible to honor.

**Two retry hints still advertised five seconds for states that outlive a
budget.** The restore 504 creates the fence, and quarantine lasts until the
channel drains; a fresh-id caller never reaches the 409 that carries the
real hint, so its header was the only signal it got. Both now derive from
the budget through one shared clamp helper, which also replaces the formula
that was inlined in the bridge and gives the documented 5-120s bounds a
test.

**A spawn collision reported an operation the caller never issued**, naming
the restore owner's action as both the active and the requested one and
telling the caller to retry an endpoint it never called.

The rest: five places still described the initialize-timeout fallback as a
plain chain rather than raise-only, contradicting sibling docs shipped in
this same PR; the design doc omitted the retry-hint clamp; the protocol
reference omitted the new spawn emission site; the error taxonomy omitted
`restore_settlement_overdue`, which matters because its audience is
monitoring. Test-only gaps: the dynamic 409 had no HTTP-layer coverage, the
120-second cap was unpinned, and the SDK's precedence of an explicit global
timeout over the advertised budget was pinned only branch-by-branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): preserve restore session ownership handoff

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-09 15:47:09 +00:00
jinye
60458f5e37
fix(serve): Coordinate caller-supplied session IDs (#8415)
* fix(serve): coordinate caller-supplied session IDs

Complete daemon-wide admission across REST, ACP, workspace generations, SDKs, and MCP.

Closes #8411

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

* test(serve): wire session bridges in hot-reload harness

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

* fix(serve): address review round for caller-supplied session IDs (#8415)

Restore the observability and fail-loud guarantees flagged in review:
log every session-id admission routing failure, name the live foreign
owner workspace in restore conflicts, make the ACP dispatcher's
admission dependency required so load/resume cannot run on a mount
without one, and require mountAcpHttp hosts to inject the daemon-wide
admission instead of silently building a weak fallback. Harden the SDK
WS transport against environments without global fetch and against
non-capabilities 200 envelopes, and align the design doc with the
implemented restore-sharing and persistence-failure semantics.

* fix(sdk): harden session ID capability fallback

Preserve REST capability errors, fail closed on malformed envelopes, retain restore routing diagnostics, and align retry documentation.

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

* fix(serve): normalize restored session IDs

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

* fix(session): preserve mixed-case legacy session access

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 07:31:30 +00:00
callmeYe
39377fcff3
feat(daemon): add batch skill toggle API (#8664)
* feat(daemon): add batch skill toggle API

* test(serve): update capability integration baseline

* fix(daemon): apply skill batches atomically

* test(daemon): pin Skill batch toggle contracts and fix docs examples

* test(daemon): pin Skill batch toggle mutants flagged in review

* test(daemon): cover Skill batch toggle edge cases

* docs(daemon): clarify Skill batch toggle contract notes from review

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

* test(daemon): pin Skill batch toggle cap semantics and SDK surface shape

* test(daemon): pin Skill batch toggle mutants flagged in round-5 review

---------

Co-authored-by: github-actions[bot] <github-actions[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>
2026-08-08 23:22:04 +00:00
jinye
59b750fc4d
feat(serve): Expose active work state (#8588)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(serve): expose active work state

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

* refactor(serve): rebuild active-work reporting on channel-wide snapshots

Reworks the active-work signal after review. Three changes of substance.

Drops the 45s heartbeat watchdog entirely. It inferred "this channel is
dead" from "one Session stopped reporting" and killed the whole channel,
taking every Session on that process with it — including on a suspend,
a long event-loop stall, or a single dropped notification. Channel
liveness is a transport concern and gets its own mechanism.

Replaces the per-Session boolean with a channel-wide snapshot of named
holds, derived on every report from the owners of the work (the
registry's unfinalized set, the notification queue) rather than from a
ledger kept alongside them. Full snapshots make a dropped report
self-correcting in both directions, and a Session's absence from one is
positive evidence the child released it. Agent holds now use
hasUnfinalizedTasks()'s predicate, closing the cancel to
finalizeCancelled() window where a cancelled agent looked idle and its
terminal notification could be stranded.

Leaves prompts out of the child's report: the daemon accepts, queues,
dispatches, and settles them, so its own count is authoritative and
covers the FIFO wait the child cannot see. A snapshot is flushed ahead
of the prompt response so a hold the prompt left behind is on the wire
before the daemon drops that count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(serve): confirm idle before closing, and grade the health signal

Completes the active-work rework with the two facts a restart controller
was still missing and the one guarantee automatic cleanup was missing.

Automatic cleanup no longer destroys a Session on the strength of a
cached snapshot. It asks the child to close only if unheld, and the
child answers under its own close gate — with the gate held no prompt is
admitted and no automatic turn starts, so a hold cannot appear between
the check and the teardown. A refusal hands back the current holds and
the daemon adopts them. An unanswered request is neither retried nor
assumed: the Session stays, and the next snapshot settles it, because a
Session absent from one has provably been released. Every automatic path
— detach, attach rollback, prompt settle, notification settle, a child
reporting itself idle — now funnels through one decision point instead
of four near-copies.

Health gains activeWorkReporting and activeWorkStaleMs. Without them
activeWork:false cannot be told apart from "no child told me anything",
which is the one case where acting on it is unsafe. Freshness is graded
by the daemon rather than the controller, since the cadence is negotiated
per channel; a stale snapshot or a child omitting a category degrades the
grade instead of silently narrowing what the boolean covers.

Tests: acp-bridge 489/489, acpAgent 383/383, Session 534/534,
serve suites 1188 with one pre-existing cross-file flake in the Live
Appshot integration tests (reproduces on the unmodified tree, failing a
different test each run).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): contain snapshot-collection failures, and repair two Session mocks

CI caught two things the local runs missed.

The reporter's snapshot construction was unguarded. Only the send was
wrapped, so a throw while collecting a Session's holds escaped through
setInterval and queueMicrotask as an uncaught exception — capable of
taking down the ACP child — and through flush() into the prompt path,
turning a reporting problem into a failed prompt. Collection is now
wrapped and a failed snapshot is abandoned whole rather than sent
partially: a Session missing from a report reads as released, and one
reported with no holds reads as safe to close, so publishing a partial
snapshot would actively invite the daemon to destroy live work. Sending
nothing lets the daemon's copy age instead, which its freshness grading
already treats as untrustworthy and retains. flush() no longer rejects.

Session.review-lease and Session.worktree mock the background-task
registry without setStatusChangeCallback, so constructing a Session threw.
That break arrived with the original commit, which verified only
Session.test.ts; the sibling Session.*.test.ts files were never run. Both
mocks now carry the methods the constructor and the hold collector need.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(serve): prove the reporter contains collection and transport failures

The previous commit added the guard but could not have demonstrated it:
the same commit also gave the acpAgent Session mock a
collectActiveWorkHolds, removing the very condition that triggered the
throw. The unhandled error disappearing was therefore explained by the
mock alone, and active-work-reporter.ts had no tests at all.

These cover the escape routes that matter — the interval timer, the
coalescing microtask, and flush() on the prompt path — plus the choice to
abandon a whole snapshot rather than send a partial one, since a session
omitted from a report reads as released and one reported with no holds
reads as safe to close.

Verified by removing the guard: five of the nine fail with the collection
error escaping, and pass again once it is restored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): make every automatic teardown ask before destroying

Self-review of the previous revision found that this PR had promoted a
cached child report from a hint into the authority that permits destroying
a Session. Four teardown paths consulted it, their guards disagreed with
each other, and each was weaker than what main had. The four are one
defect with four exits, so they are fixed as one change.

Absence from a snapshot no longer authorizes teardown. Because reports
are complete, a Session the child omits holds nothing on the child side —
so absence and reported-with-no-holds are the same fact and now take the
same path. The separate absence loop is gone; it lacked the subscriber
and client guards `maybeCloseIdleSession` applies, so one snapshot could
destroy a Session with a live SSE subscriber and a registered client.
That contradicted this PR's own claim that an unreported Session is
retained, and the old test asserted the destruction. Both are corrected.

A conditional close is now marked in flight across the whole confirm-then-
teardown span, and attach, prompt, and rewind refuse a Session in that
state exactly as they refuse one already closing. `closeSessionImpl` sets
`closing` synchronously, but the round trip in front of it is an await of
up to ten seconds; on main the guard sequence ran straight into teardown,
so splitting it is what opened the window.

A snapshot older than the freshness window stops counting as evidence.
Staleness was already computed, but only to grade health, never to gate
destruction — so a child that went quiet after one empty report left a
cache that permitted reaping indefinitely. Never-reported and gone-quiet
now land in the same retained bucket. Reclaiming a channel that has truly
stopped answering belongs to transport liveness, not here.

The idle reaper asks the child too. Its TTL says the client stopped
caring, which is not the same as the child having nothing left to run.

Health coverage is exposed as counts and graded once daemon-wide, because
grades do not compose: a runtime with zero Sessions is vacuously `full`,
and folding that in let an empty workspace vouch for another workspace's
unreported Sessions. `activeWorkStaleMs` now measures only covered
Sessions, so it can no longer report positive staleness beside a grade
saying nothing is covered.

Also: bound snapshot `sessions[]` and `holds[]` so a buggy child cannot
make the daemon walk an unbounded structure per report, and retract the
background-task status callback by identity rather than blanking a
single-slot setter the TUI also uses.

Tests: the absence test now asserts retention under a registered client
and under a live subscriber; new regressions cover the recovered lost
close response, the stale-snapshot gate, admission refusal during a
conditional close, the reaper's confirmation, the oversized-snapshot
discard, and the mixed empty/uncovered health aggregate.

* fix(serve): make unknown a reason to ask, not a reason to skip

Triage review found that the design doc, the PR description, and the
comment on `entryHasActiveWork` all promised the daemon *asks* the child
about a Session it has not heard about, while no code path ever did:
`entryHasActiveWork` returns true when the child's side is unknown, and
the cleanup path returned early on exactly that. The finding predates the
guard rework and survived it unchanged.

Skipping on unknown looks like the safe direction and is in fact the worse
failure. Nothing resolves it — a Session on a channel that went quiet is
retained forever, and the idle reaper skips it too, so there is no path
out at all. Asking resolves it definitively: the child answers under its
own close gate whether or not its snapshots are arriving, the round trip
is bounded, and every non-answer still retains.

So the predicate is split by what it actually knows. `childReportsHeldWork`
is positive knowledge only; `childWorkIsUnknown` is the absence of a
gradeable report. The health surface ORs both, because a controller must
never read "nobody told me" as "nothing is running". Automatic cleanup
blocks only on known work and lets unknown through to `confirmChildUnheld`.

Also moves `parseActiveWorkSnapshot` out from between two import blocks
(pure relocation, no logic change) and aligns the doc wording, including
the shared-guard table, with what the code now does.

* fix(serve): close three teardown races the confirm window opened

Review found three ways the conditional close can still destroy a live
Session. All three share a cause: the round trip turned a synchronous
guard-then-teardown into an awaited span, and three things that were
previously impossible to observe mid-teardown now are.

**A restore in flight looks exactly like an abandoned Session.**
`session/load` registers the entry before awaiting `artifacts.restore()`
and `seedSessionUpdates()`, and registers its first client only after —
so for that whole window there are no clients, no subscribers, nothing
held, and the child answers the conditional close truthfully. The
snapshot trigger this PR added fires inside it. Excluded in
`entryIsAutoCloseCandidate` rather than at the snapshot trigger, so the
reaper's TTL elapsing inside a slow restore is covered too.
`pendingRestoreIds` already existed but was read only by
`hasNoChannelWork`, never by the close funnel.

**Teardown re-resolved the target by id without re-checking identity.**
`closeSessionImpl` does a fresh `byId.get`, and the id can be
re-registered to a different entry during the round trip: an explicit
kill removes this one (kill ignores the in-flight flag by design, keeping
its force semantics) and a `session/load` for the same persisted id
registers a fresh one. The stale continuation then tore down the newly
restored Session under its just-attached client. One identity re-check
after the await.

**The restore path was not upgraded to the new admission predicate.**
`sendPrompt`, `rewindSession`, and single-scope attach check
`isClosingOrAuthorizingClose`; `restoreSession` still checked bare
`closing` at both its guards, so a client could attach inside the window
and lose the session under it. That directly contradicted the
`closeIfChildUnheld` comment claiming every admission path checks the
flag. Its `racedEntry` branch had no closing guard at all — a narrower
pre-existing hole, same defect, same predicate.

Regression test covers the restore-path admission refusal. The other two
need a mid-restore snapshot and a kill-then-reload interleave that the
mocked-channel harness cannot stage honestly; both are pinned by reading
the code paths, which is weaker and worth saying.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 06:34:20 +00:00
Dragon
681e30d54f
docs: clarify SDK interrupt behavior (#8711) 2026-08-08 01:48:16 +00:00
BaboBen
edb420393e
fix(channels): manage DingTalk interactive card config (#8517)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* fix(channels): manage DingTalk interactive card config

* test(cli): cover nested channel object validation

* fix(channels): harden nested management metadata

* fix(channels): isolate invalid management descriptors

* fix(channels): isolate invalid management metadata from channel runtime

* fix(channels): reject reserved unknown keys in management config upserts

* fix(channels): harden channel management validation and editor checks

Reject management descriptors that lack a fields array at registration so
broken plugins are stripped to unmanageable instead of being advertised as
manageable and failing every upsert with an unmapped TypeError. Reserve the
top-level "type" field key and require enum fields to declare at least one
option, both of which the settings store could never accept. Treat
whitespace-only number drafts as empty in the channel editor, consistent
with the module's other emptiness checks.

Also give the SDK descriptor mirror test a runtime wire-shape walk over the
built-in catalog, add the parser's timeout rejection boundary, and restore
the exact built-in catalog membership assertion.

* fix(channels): validate management field shapes and editor bounds (#8517)

* fix(channels): align management validation layers and pin gate behavior (#8517)

Read envResolvable by truthiness in the settings store so it matches the
registration gate and the editor, instead of rejecting the advertised
environment references of untyped plugins. Fail closed at registration on
non-finite exclusiveMinimum values, empty object property lists, and async
validateConfig functions, all of which would otherwise advertise a field
or save path that can never succeed. Strip invalid management metadata
over a prototype-preserving copy so class-instance plugins keep their
createChannel implementation.

Move the unchanged-value preservation exemption ahead of the object shape
rejection so a stored non-record value (for example a hand-written null)
no longer locks every unrelated management edit of that channel. Clamp
DingTalk question-card timeouts at the maximum setTimeout delay, since
Node treats larger delays as one millisecond and would expire cards
instantly.

Pin the previously untested load-bearing behaviors: per-key previous
threading in the recursive validation, the preservation exemption's
precedence over nested required enforcement, nested "type" properties,
depth-2 nesting rules, and the nested-only constraints of the daemon
descriptor wire contract.

* fix(channels): close reserved-key preservation gaps and pin gate behavior (#8517)

* test(cli): tolerate IPv6-less hosts in serve ::1 bind tests (#8517)

The self-hosted CI containers can have no IPv6 loopback, where the two
runQwenServe tests that bind ::1 fail with EADDRNOTAVAIL. Probe the
interfaces once and skip only the IPv6-dependent binds there; every
assertion still runs on IPv6-capable hosts.

* fix(channels): align descriptor type contracts with runtime validation (#8517)

The registry already rejects object fields without a non-empty
properties array and enums without unique options, but the descriptor
types still admitted both, so TS-authored plugins only learned about
it when registration stripped their management surface. Make
`properties` required, give enums a dedicated descriptor member with
required `options`, and drop the never-honored `envResolvable` flag
from number descriptors, in both channel-base and the SDK mirror, and
export the descriptor sub-types through the webui barrels. Also map a
throwing `validateConfig` to the usual invalid-config error and pin
the store contracts that had no distinguishing tests: omitting a
parent object drops the stored object without checking its nested
required, writes replace nested values wholesale, unchanged stored
scalars are still re-validated, and valid plugins register by
original reference.

* fix(channels): defuse validateConfig rejection leak and close descriptor gate gaps (#8517)

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

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-07 15:47:20 +00:00
qqqys
3edecac116
feat(channels): support group pairing (#8440)
* feat(channels): support group pairing

* fix(channels): address group pairing review

* fix(web-shell): show group pairing management

* fix(channels): recheck group pairing before history backfill

* test(channels): verify group approval isolation

* fix(channels): address group pairing review findings and pin behaviors (#8440)

- Grandfather the group allowlist file in PairingStore legacy migration
- Offer the pairing groupPolicy option in github/gitlab descriptors
- Re-export DaemonChannelPairingSubject from the webui barrels
- Refresh the GroupGate doc comment and channel docs rows
- Pin the unpinned group pairing behaviors called out in review:
  subject dedup, trigger matrix, notification content/cap/failure/
  thread routing, DM negative space under groupPolicy pairing,
  stored DM loop authz, pairing-enabled guard negative space,
  approval/revocation HTTP bodies, descriptor-driven gate branch,
  and the web-shell group approval mirrors
- Add a compile-time assertion for the revocation request union

* fix(channels): address group pairing review findings (#8440)

- Accept 'pairing' in the GitLab connect warning, descriptor help text,
  and gitlab.md: todos dispatch after one-time group approval.
- Model group approvals in the web-shell e2e mock daemon (approve by
  subject type, GET returns senderIds+groupIds, DELETE accepts groupId)
  and exercise the group pairing flow in the channels spec.
- Add 'pairing' to the groupPolicy enumerations in the plugins and
  per-channel docs (telegram, feishu, dingtalk, qqbot, wecom).
- Update the channel pairing CLI help to cover group requests.
- Cap pending pairing requests at one per sender so a single member
  cannot occupy every shared pending slot.

* fix(channels): address group pairing review findings round 7 (#8440)

* fix(channels): address group pairing review findings round 8 (#8440)

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

* fix(channels): address group pairing review findings round 9 (#8440)

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-07 09:20:18 +00:00
jinye
bf3abdee81
fix(serve): Allow approved same-host text reads outside workspace (#8620)
Some checks failed
npm cache producer / Save npm cache (push) Has been cancelled
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* fix(serve): allow same-host daemon text reads

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

* docs(serve): address review on same-host text reads

Record what the read capability does not fix: #8618 still reproduces for
the write and edit family, whose delegated writes are refused after the
user has already approved the diff. Give the daemon's pre-approval SSE
fan-out its own bullet in the user-facing security section, restore the
sentence stating that environment isolation is not an OS security
boundary, and make the design doc the single owner of the tradeoff list
so tuning a limit cannot leave stale copies behind.

Test fixtures no longer land in the developer's real home directory, the
assertion pinned to localized rejection copy is dropped, and the combined
capability case is split so deleting the write half cannot silently
remove read coverage.

* fix(test): declare REPO_ROOT and bind the external-read session to the daemon's workspace

The external-read regression test referenced REPO_ROOT twice without
declaring it, which made it unrunnable everywhere:

- On a developer box the ReferenceError was swallowed by the bare catch
  in findExternalReadBase(), every candidate was discarded, and the test
  reported a green skip -- exactly the silently-disabled security test
  the CI loud-fail added last round was meant to prevent. The guard was
  defeated three lines above itself.
- On CI that loud-fail branch threw at module scope, so the file failed
  to collect and took the four pre-existing tests down with it.

Declare REPO_ROOT the way every other daemon integration test does.

The session also asked for `workspaceCwd: REPO_ROOT` while beforeAll
binds the daemon with `--workspace workspaceDir`, so the create returned
400 Workspace mismatch even once the constant existed. The read under
test is external because externalReadDir sits outside the bound
workspace, not because the session claims a wider one.

Finally, collect each candidate's rejection reason instead of dropping
it, and fold it into both branches: the CI throw names why every
candidate failed and the developer-box skip warns with the same text.
A bare catch cannot tell "no /var/tmp on this image" from a bug in the
function, and the second reads as a green skip.

Reported by @wenshao, who reproduced all three consequences against a
real qwen serve daemon on Linux and supplied the repair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 07:33:16 +00:00
jinye
2eb5cd6df5
feat(serve): observe daemon and child memory against real denominators (#8423)
* feat(serve): observe daemon memory pressure against a real denominator

The daemon samples its own RSS and heap but has nothing to divide them
by, so nothing in `/daemon/status` says whether a figure is fine or
nearly fatal. #8245 landed the denominator (`limits.memory`); this turns
it into a reading.

`runtime.memory.pressure` reports `level`, `ratio`, `source`, and the six
raw figures behind them. The level is the worse of two independent
ratios, because the two failure modes are independent: a container dies
by RSS against its cgroup limit, while a process on a large host can
exhaust V8's heap long before RSS is a meaningful fraction of the
machine. Reporting only one hides whichever failure the deployment is
actually heading for. `source` names which ratio produced the level, and
`unknown` says the daemon could not measure itself — which a consumer
must not read as healthy.

The denominator is `availableMemoryMb`, not `effectiveBudgetMb`: pressure
asks how close this process is to being killed, and what kills it is the
cgroup limit or host memory. An operator's budget is a policy number, so
classifying against it would report `critical` for a daemon in no danger.

`--memory-pressure-mode` is `off | observe`, default `observe`. Both
modes report every figure; only `observe` also raises the
`daemon_memory_pressure` warning, so `off` leaves the top-level `status`
rollup untouched — the thresholds are inherited from an interactive-CLI
monitor and are not yet calibrated for a long-running daemon, and a
deployment that alerts on `status` needs the reading without the verdict.
There is deliberately no `enforce`: nothing here remediates, and a value
a caller can pass but never use is a dead switch. It arrives with the
enforcement.

Scope is the daemon root process only. `childRssCoverage` still reads
`primary_only` and says so on the wire; aggregate child RSS and channel
workers are separate measurements and land separately.

Severity is `warning` at every level including `critical`, because
`error` would make `rollupStatus` return `error` for the whole daemon —
too strong a claim to stake on uncalibrated thresholds.

Refs #8051.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(serve): report aggregate ACP child RSS, not just the primary's (#8462)

* test(serve): close the under-determined assertions review probed

The automated review mutation-probed this diff and found several
assertions that were live but under-determined — each mutant it names
kept the whole suite green. All confirmed locally, and all now fail:

- Deleting `level !== 'normal'` from the issue gate raised
  daemon_memory_pressure on a healthy daemon and flipped top-level
  status to warning on every response — the exact false positive
  `--memory-pressure-mode off` exists to opt out of. Now covered on both
  sides: nothing raised at a realistic denominator, exactly one warning
  at a denominator sized to land this process in `soft`.
- Summing children over `list()` instead of `listManaged()` dropped a
  draining-but-process-holding workspace while `activeAcpChildren` still
  counted it. The draining bridge now reports RSS, so the byte count can
  only come from that child.
- The message's denominator ternary had no coverage; inverting it sent
  an operator hunting RSS growth during a heap-driven incident.
- A truthiness guard on `ageMs` turned a measured-fresh reading (age
  exactly 0, when a status read lands in the sampler's millisecond) into
  `null`, which the field's own docs say never means fresh.
- The multi-contributor age test listed ages ascending, so a
  plain-overwrite accumulator produced the same answer as Math.max.
  Reordered descending, which kills last-wins and first-wins both.

Two declaration-only hunks — the issue-code union member and the
`pressure` field — were guarded by tsc alone, which vitest does not run.
Both are now pinned at runtime by asserting the code string and the full
key set.

Also fixes a real display defect: `toFixed(0)` renders a ratio of 0.795
as "hard at 80%", and 80% is critical's documented threshold. One
decimal, so the number and the level cannot contradict each other.

And corrects a JSDoc claim of mine that was simply wrong: `pressure` is
absent not only for direct-embed but on the bootstrap /daemon/status
route, which omits runtime.memory wholesale even though the budget is
resolved — and that window is not just startup, since a daemon whose
runtime fails to start serves the bootstrap app for its lifetime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(serve): model a per-child heap partition of the daemon budget (#8508)

* feat(serve): add the child-heap admission primitives, unwired

Groundwork for #8182 step 2. Nothing calls any of this yet, so no child
is sized differently and no spawn is refused.

`ProcessRegistry.committedProcessCount` counts attached children plus
reservations that have not attached. That is the figure admission has to
key on: `reserve()` inserts its token synchronously before `spawn()`, so
two racing spawns each see the other, while neither appears in
`activeProcessCount` until its child attaches. A child leaves the count
on exit rather than when `terminate()` starts, so a channel swap counts
twice while the old process winds down — deliberate, since its memory is
still resident.

`getAcpMemoryArgs(explicitMb?)` takes an optional share that bypasses
both the module cache and the raise-only guard. Both bypasses are
load-bearing. The cache, because the share depends on how many children
are live now rather than on the host. The guard, because a
budget-derived share is normally *below* the daemon's own heap limit, so
routing it through `targetMB > currentLimitMB` would drop the flag,
silently restore the overcommit, and leave every test green — the trap
against a multi-GB runner, and mutation-checking it by reinstating the
guard fails two tests.

`createChildHeapPolicy` holds the mode, the budget, and the would-be
refusal counter, and answers `decide(concurrentChildren)`. The refusal
is derived from the unclamped quotient, not from
`recommendedChildShareMb`, because that function clamps *up* to the
512 MB floor: past the point where the pool stops covering the count its
answer saturates and can no longer distinguish "barely does not fit"
from "wildly does not fit".

`ChildHeapPoolExhaustedError` with both transport mappings — REST 503
with Retry-After, ACP `child_heap_pool_exhausted` — added together,
since the two mappings are hand-written and drift silently otherwise.
Refusing at spawn rather than at registration is the correction #8182
demands: registration allocates nothing, so this surfaces as "no new
session in this workspace right now", which is true and retryable.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(serve): size each ACP child by concurrently live children

Wires the primitives from the previous commit into the spawn path, behind
`--child-heap-mode off | observe | enforce`, default `observe`.

Under `enforce` a child's `--max-old-space-size` is a share of the child
pool divided by the children concurrently committed at the moment it
spawns — read from the shared ProcessRegistry after `reserve()`, so two
racing spawns each see the other. When the pool cannot cover another
child at the 512 MB floor the spawn is refused with
ChildHeapPoolExhaustedError, which is what turns a per-child ceiling into
an aggregate bound: concurrent children can never exceed pool/512.

Keyed on concurrency, never on registrations. A dormant workspace has no
child, so it costs nothing — the specific correction #8182 records
against the withdrawn proposal, which would have shrunk a lone live child
to 614 MB because of 24 idle registrations.

Default `observe` computes the share and the admission decision and
applies neither, counting the refusals that would have happened. The
divisor has never been checked against a real multi-workspace deployment,
and a non-zero count is how an operator learns enforcement would have
broken them without being broken. It also catches the case worth
worrying about: a channel swap counts the dying child alongside its
replacement, so on a saturated pool enforcement could refuse a restart
and leave that workspace with no child at all. Excluding terminating
children would authorise real overcommit to dodge a hypothetical
refusal, so the count reports it instead.

Ceilings already granted are not revisited — V8 cannot lower them — so
granted ceilings transiently exceed the pool. Acceptable: the flag is a
ceiling, not a reservation, and a workspace with no live sessions has no
child and picks up the current share on its next spawn.

`limits.memory.enforced` stops being a required literal `false`. #8245
made it one so a client could never mistake that namespace for
enforcement that had not shipped; it has now, so the field is a boolean
derived from the mode — and stays `false` under `observe`, which applies
nothing.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(serve): correct the claims child-heap enforcement makes false

Two sentences in the protocol doc described the memory section as
unconditionally observational: "a required `enforced: false`", and "no
child spawn argument derives from these values, and no request is
refused on their basis". Both are false under `--child-heap-mode
enforce`, so both are rewritten rather than left to rot — `enforced` is
now documented as the boolean that answers exactly this, and the refusal
is documented with its wire shape on both transports.

Also documents `childHeap.refusals` as the calibration signal, since a
would-be-refusal count is useless if operators do not know to read it
before switching to `enforce`; the flag row in the three operator docs;
and the design doc's Part 1, which listed applying a share as a
compatibility risk without recording how that was resolved.

The end-to-end test asserts the policy reaches a real booted daemon's
status with `enforced: false` under the default mode — the wire type in
that test is a hand-written mirror, so its `enforced: false` literal had
to widen too, which is the check that caught the type not being widened
everywhere.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(serve): cover both branches of the enforced tripwire

`enforced` was only ever asserted false — the unit tests build no policy
and the end-to-end daemon runs the default `observe` mode, so the branch
that makes the field worth having was untested. Hardcoding it back to
`false` passed everything.

Also pins `childHeap: null` as distinct from a policy in `off` mode: the
first says no policy exists (direct-embed, or the bootstrap window before
the runtime is built), the second says one exists and computes nothing.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): partition the child pool so granted ceilings stay inside it

Review was right that the previous design did not deliver the aggregate
bound it claimed. Sizing each child by the count live at *its* spawn
bounds the child count but not the memory: V8 cannot lower a running
child's ceiling, so grants accumulate as P + P/2 + P/3 + ... = P x H(n).
Reproduced exactly — 9557 MB authorised against a 3687 MB pool at seven
children on an 8 GB host, and 61355 MB against 15360 MB at the limit on
32 GB. That is 2.6x and 4x the pool, which is what the policy exists to
prevent.

Grant accounting alone does not fix it: the first child would take the
whole pool and the second would be refused immediately. Keeping the
invariant requires early children not to receive the whole pool, so the
ceiling is now a fixed partition — childPoolMb / maxConcurrentChildren,
constant for every child, with maxConcurrentChildren itself derived from
the pool and capped at MAX_DAEMON_WORKSPACES. The sum is then
n x ceiling <= pool by construction, with no ledger of outstanding
grants and no dependence on arrival order. Tested as an invariant across
four host sizes: fill the daemon to its admission limit and the
authorised total still fits.

The cost is deliberate and now documented rather than hidden: a lone
workspace on a 32 GB host gets 614 MB rather than the pool, because any
child may still be running when the house fills. An 8 GB host admits
seven concurrent children at 526 MB each.

Also from review:

- The policy is no longer built for an injected `deps.bridge`. That
  bridge carries its own channel and never reaches the factory the
  policy rides on, so status could report `enforced: true` while nothing
  was being sized.
- Both transport mappings now have direct tests. They are hand-written
  beside each other and drift silently; the spawn-policy tests cannot
  catch a wire regression.
- Swept the "does not size any child" claim, which enforce makes false,
  out of the CLI help text, ServeOptions docs, the two operator tables,
  and the e2e header comment. The 17-configuration table realigns
  wholesale because that cell was its widest — whitespace only.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(serve): model the child heap partition, defer applying it

Review established that the refusal counter cannot tell an operator
whether enforcement is safe, and that is the ground the enforcing mode
stood on. While observing, children run on the host-derived ceiling
(16384 MB on a 32 GB host), so a workload needing 2 GB of old space is
healthy with zero refusals and OOMs the moment a 614 MB partition is
applied. The counter measures admission pressure, not ceiling adequacy.

Rather than ship a switch with no safe way to decide when to turn it on,
`enforce` is removed. `--child-heap-mode` is `off | observe`, and the
mode that would apply the partition arrives with the measurement that
justifies it: peak old-space per child, compared against the modeled
ceiling. That is a real measurement chain — the child reports rss and
cpu today, and `--max-old-space-size` bounds old space specifically, so
neither rss nor heapUsed answers the question.

With nothing applying the partition, the machinery that existed only to
apply it goes too rather than shipping unreachable:
`getAcpMemoryArgs(explicitMb?)`, `ChildHeapPoolExhaustedError` and both
transport mappings, and `limits.memory.enforced` reverts to the required
literal `false` it was before. The spawn path is untouched again; the
factory asks the policy what it would decide purely so the count is
real.

Also fixes the zero-pool defect review found, which the removed clamp
caused: forcing at least one admissible child on a 512 MB host — where
the root reserve consumes the whole 256 MB budget — produced a ceiling
of 0, and `--max-old-space-size=0` is V8's *default* heap, not a zero
ceiling. A pool that cannot cover one child at the floor now reports
`maxConcurrentChildren: 0` and `perChildCeilingMb: null`, and the test
that enshrined the old behaviour is inverted.

Status now publishes `maxConcurrentChildren` and `perChildCeilingMb`, so
an operator can judge the partition against their own workload — the
substitute for a counter that cannot judge it for them. Every claim that
a zero refusal count means the partition is safe to apply is removed
from the flag help, the operator docs, the protocol doc, and the design
doc.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): repair the child-heap assertion and the reservation leak

Three findings review raised against #8508 after the partition became
observation-only, all still live on this branch now that it has merged.

The status assertion in `run-qwen-serve.test.ts` failed on head: it used
`toEqual` against `{ mode, refusals }` while the wire also carries
`maxConcurrentChildren` and `perChildCeilingMb`, so the suite was red at
217 passed / 1 failed. The local type restating the wire shape was short
the same two fields. Both are filled in, and the assertion stays `toEqual`
so an unannounced field still fails it — the two derived figures get
matchers because this suite boots a real daemon and the pool follows the
machine. What they have to satisfy is now pinned separately: a fixed
ceiling times the number admitted must fit inside the pool it partitions,
which is the whole reason the partition bounds anything.

`decide()` and `getAcpMemoryArgs()` ran between `reserve()` and the `try`
that cancels the reservation. `childHeapPolicy` is a public
`createSpawnChannelFactory` option, so `decide()` is caller code and may
throw; the spawn then rejected with the token held for the process
lifetime, inflating `committedProcessCount` for every later spawn. Both
calls move inside the `try`. The regression test is mutation-verified —
reverting the move gives `expected 1 to be +0`.

`ServeOptions.memoryBudgetMb` still promised a `childHeapMode: 'enforce'`
that sizes children and refuses spawns. No such mode exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): report no child-heap partition under `off`

`snapshot()` returned `maxConcurrentChildren` and `perChildCeilingMb`
unconditionally, so a daemon run with `--child-heap-mode off` still
published a partition — 7 children at 526 MB on an 8 GB host — under a
mode whose documentation says "do not model it". Review raised it, and it
mattered more than it looked: with `enforce` gone, `off` and `observe`
differed only in whether `refusals` incremented, so nothing on the wire
distinguished a model that was switched off from one in force.

Both figures are now `null` under `off`, which required widening
`maxConcurrentChildren` to `number | null` in the daemon type and the SDK
mirror. `null` rather than `0`: zero is already the computed answer for a
pool too small to host one child at the 512 MB floor, and collapsing the
two would tell an operator who disabled the model that their host cannot
run anything. That leaves three distinguishable states — no policy at all
(`childHeap: null`), a policy modeling nothing (`mode: 'off'` with null
figures), and a live model — and each now has a test.

The `off` unit test previously asserted only `refusals`, so its name
("models nothing at all when off") promised more than it checked. It now
covers the figures, with a sibling test pinning 7 / 526 under `observe` on
the same budget so nulling them unconditionally cannot satisfy both.
Mutation-verified in both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): never model a child heap ceiling below the documented minimum

`perChildCeilingMb` is `min(floor(pool / maxConcurrentChildren),
legacyChildCeilingMb)`. The first term is at least `MIN_CHILD_HEAP_MB` by
construction; the second is `floor(available / 2)` and is not, so the
`Math.min` could publish a ceiling *below* the `minChildHeapMb` sitting beside
it in the same snapshot:

    avail=768  --memory-budget-mb 1024  pool=512  legacyCeil=384  perChild=384
    avail=1023 --memory-budget-mb 1024  pool=767  legacyCeil=511  perChild=511

Unreachable from a derived budget — the pool reaches 0 first — but an explicit
budget has a floor of 1024 while available memory does not, and
`docs/users/qwen-serve.md` tells operators on exactly these hosts to pass that
flag. The documented remedy is what reaches the band.

Refuse the model rather than shrink under the floor, with
`maxConcurrentChildren` zeroed in lockstep: a ceiling no child may run at is
not a partition, and "one child fits" beside a null ceiling is the same
contradiction from the other side. Nothing is applied today so the impact was a
wrong published figure, but this is the number the partition asks to be judged
by and the one an `enforce` mode would hand to `--max-old-space-size`.

The existing matrix resolves derived budgets only, which is why the mutation
sweep came back clean; add the `budgetMb` axis, asserting in each case the
shape that makes it reachable, and pin the inclusive boundary (1024/1024 ->
one child at 512) so nulling unconditionally cannot pass instead.

Also, in the same review pass:

- Split usable-gauge handling into numerator and denominator. Coercing an
  unusable numerator to 0 published `rssBytes: 0, rssRatio: 0, level: 'normal',
  source: 'rss'` — a daemon that measured nothing, indistinguishable from an
  idle one, which is the confusion `source: 'unknown'` and `sampled: 0` exist
  to prevent everywhere else here. An unusable numerator now retires its own
  side. Zero stays a reading for a numerator and not for a denominator.
- Document that `rssRatio` divides by host total under
  `availableMemorySource: 'host'`, so it is a lower bound on real pressure
  there — a denominator problem no threshold calibration addresses.
- Document that `refusals` counts channel swaps at full occupancy (the
  terminating child is counted until it exits) and equals the total spawn count
  on a host too small to model a partition. Deliberately not fixed by giving
  the comparison swap headroom, which would admit a 26th ceiling against a
  25-child pool.
- Keep the sampler's rejection handler as a documented backstop — the shipped
  `refreshChildResource` never rejects, but it is an optional `async` interface
  member, so a foreign implementation throwing early would otherwise surface as
  an unhandled rejection — and give it the workspace so it is attributable
  across the fan-out.
- Test hygiene: drop a duplicated `enforced` assertion; replace a host-
  dependent `expect.any(Number)` with a key-set pin plus a branch, since a
  small host now legitimately reports no partition; use `vi.spyOn(Date, 'now')`
  over direct assignment; reuse the exported `ChildHeapMode` on the child-heap
  side, leaving the independent `memoryPressureMode` switch alone.

Reported by @wenshao.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-07 06:10:37 +00:00
jinye
037b4d9c03
feat(daemon): Add SSE stream and client observability (#8572)
* feat(daemon): add SSE stream observability

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

* fix(daemon): Address SSE observability review feedback

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-06 16:44:15 +00:00
顾盼
a5c637b749
feat(web-shell): add native Live Voice (#7859)
* feat(web-shell): add native Live Voice

* fix(web-shell): address review feedback for Live Voice PR (#7859)

- Quote all strings in electron-builder.yml to fix yamllint CI failure
- Gate discovery publish on liveVoiceEnabledAtBoot to avoid writing
  bearer token to disk when Live Voice is disabled (M1)
- Add child identity guard to CommandMonitor stdout/stderr handlers
  to prevent stale helper output from corrupting the new buffer (M4)
- Add exponential backoff to sent-completion delivery retry (M3)
- Skip broadcastState when setCallState/setTranscript value is
  unchanged to reduce per-audio-delta overhead (H1)
- Document sent-mode completion notification in module docstring (H2)
- Remove dead protocol/nonce aliases from readDiscoveryFile
- Fix single instance lock fall-through with process.exit(0)

* fix(cli): register realtime_voice in docs contract and env guard (#7859)

* fix(web-shell): address review feedback for Live Voice PR (#7859)

* fix(cli): discard orphaned isolated dir when parent restore fails (#7859)

* fix(web-shell): address review feedback for Live Voice PR (#7859)

* fix(serve): harden live turn recovery

* fix(desktop): restore Live Host native build

* fix(live): align native host and session isolation

* fix(acp): preserve live worker continuation lineage

* fix(live): classify provider close reasons

* fix(serve): discard unused recovered conversation dirs

* fix(live): isolate authorized realtime responses

* fix(live): preserve realtime response authority

* feat(web-shell): complete Live Voice onboarding

* fix(live): persist realtime-owned dialogue

* fix(live): preserve final speech while stopping

* Revert "fix(web-shell): address review feedback for Live Voice PR (#7859)"

This reverts commit 7110bec6b034c702bca6e28e35b93c7f70e729cd.

* Revert "fix(cli): discard orphaned isolated dir when parent restore fails (#7859)"

This reverts commit 85165f1b2ddfaa311b8be91acdd76a6f388f6204.

* Revert "fix(web-shell): address review feedback for Live Voice PR (#7859)"

This reverts commit 9199fa633e102bb8f24e4b216d322be4323eb3fc.

* Revert "fix(cli): register realtime_voice in docs contract and env guard (#7859)"

This reverts commit 6b6b1718352ef01a98a73976b5c7c4433fd14c35.

* Revert "fix(web-shell): address review feedback for Live Voice PR (#7859)"

This reverts commit e083779105199d26de3afd8ad00719a08efe3099.

* revert(live): remove remaining takeover behavior

* revert(live): restore pre-rollback implementation

* test(cli): align Live diagnostics env guard

* test(release): cover Live Host publication

* fix(ci): re-sign Live Host package before verification

* fix(serve): scope sent completion notifications to Live

* fix(web-shell): preserve live setup errors

* fix(live): align realtime backend speech lifecycle

* ci(live): publish Live Host independently

* test(cli): mock Live speech bridge handler

* test(release): align Live Host workflow contract

* fix(live): address release and lifecycle review findings

* fix(live): release completed call tracking

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-05 08:33:22 +00:00
ChiGao
5631f4b112
feat(serve): add a required external tool guard provider (#8125)
* feat(serve): add required external tool guard

* fix(serve): keep guard constants off fast path closure

* test(serve): cover guard startup options

* refactor(acp-bridge): centralize external tool guard validation and ack value (#8125)

* fix(core): align MCP reconnect timeout test with safe replay policy (#8125)

The reconnect-on-timeout test still built its mock tools without server
trust or tool annotations, which the safe replay change now requires
before automatically replaying a connection-loss failure. Update the
fixtures the same way the surrounding reconnect tests were updated,
keeping the test's original assertion that a timeout on a known
disconnected server goes through the reconnect path. Mirrors the same
alignment already landed on main.

* fix(cli): alias externalToolGuard subpath for vitest source resolution (#8125)

This PR added `@qwen-code/acp-bridge/externalToolGuard` imports to cli
serve/acp modules but not the vitest source alias every other acp-bridge
subpath carries. Without it, any vitest run whose acp-bridge dist is
stale or absent fails to resolve the import and the five serve test
files die at transform time. Add the alias following the documented
convention in the config so tests read the live source.

* fix(serve): reject non-ASCII external tool guard bearer tokens

A token outside the ASCII range passed construction but made the
handshake throw ERR_INVALID_CHAR when interpolated into the
Authorization header, blocking qwen serve startup in required mode
with an unexplained error. Enforce printable ASCII (0x21-0x7E) at
validation time so the configuration fails fast with a clear message.

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-04 14:37:13 +00:00
jinye
0cb109f513
fix(core): Avoid replaying unsafe MCP tool calls (#8387)
* fix(core): Avoid replaying unsafe MCP tool calls

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

* fix(core): Revalidate MCP replay after reconnect

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-03 11:04:38 +00:00
jinye
d1648b3af9
feat(telemetry): Track tool execution outcomes (#8180)
* feat(telemetry): track tool execution outcomes

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

* fix(telemetry): address execution-status review feedback (#8180)

- Update stale nonInteractiveToolExecutor expectations for executionStatus (red CI)
- Scope the cancelled span-status short-circuit to tool_call events so other
  cancelled events carrying an error keep ERROR status
- Record loop-detection skips as UNKNOWN, not EXECUTION_DENIED, keeping the
  denial metric accurate
- Assert execution_status on the resolved-with-error PostToolBatch path
- Raise tool-call observer-failure logging from warn to error
- Clarify subagent-projection exclusion and JSONL compatibility in design doc

* fix(core): address review findings 3-6 on tool execution status (#8180)

- recordToolExecutionMetrics now merges common attributes (session.id
  opt-in) like every other counter in metrics.ts
- Lift TOOL_FAILURE_KIND_ATTRIBUTE / TOOL_FAILURE_KIND_CANCELLED into
  telemetry/constants.ts so coreToolScheduler and session-tracing share
  one definition
- Add debugLogger to runToolTelemetrySink catch (was silent)
- Replace delete-based absence in withPostToolBatchStop with conditional
  spread

* fix(core): address remaining review findings on tool execution status (#8180)

- Pass the frozen executionStatus variable instead of the literal
  'success' in the post-hook-stop error response, keeping the frozen
  value the single source of truth (finding 4)
- Force-finalize the deferred PostToolBatch parent span in the abort
  drain, since that terminal path cancels the batch hook that otherwise
  owns the span; documents the invariant at the call site (finding 6)
- Comment the loop-detection guard so the permission-cancellation
  exclusion from invalid-param loop detection is explicit (finding 9)
- Rename the design doc to the dated docs/design convention and note
  the schedule()/handleConfirmationResponse() resolution contract
  change for embedders (finding 2, doc convention)

* docs(core): note schedule() resolution contract in tool execution status design (#8180)

Record the embedder-facing behavior change that schedule() and
handleConfirmationResponse() resolve with a terminal error call rather
than rejecting, so a failing tool no longer aborts its siblings.

* fix(telemetry): address review feedback for tool execution status (#8180)

- Document the new tool_call attributes (call_id, execution_status), the
  qwen-code.tool.execution.count metric, the tool.execution span attributes,
  and the tool.failure_kind=cancelled span field in telemetry.md.
- Pass ToolErrorType explicitly at loop-detection skip sites instead of
  inferring it from the skip message string, so copy edits cannot silently
  reclassify loop skips as approval denials.
- Simplify withPostToolBatchStop response construction (drop the
  destructure-and-reattach used to preserve a missing execution status).
- Add a debug breadcrumb when a PostToolBatch stop has no span to attach to,
  and a one-time warning when PostToolBatch hook detection fails open.
- Drop the try/catch wrapping the pure isTelemetrySdkInitialized getter.
- Clarify the design doc invalid-combination wording and note that the
  execution-failure SLI cannot be attributed to a specific tool.
- Add a regression test pinning that schedule() resolves (not rejects) when a
  tool execution throws.

* fix(telemetry): address round-7 review feedback for tool execution status (#8180)

* fix(telemetry): restore type-safety fallback for executionErrorType (#8180)

* fix(telemetry): align tool execution failure outcomes

Keep Core and ACP cancellation arbitration consistent, preserve structured post-processing errors, and restore QwenLogger MCP metadata privacy.

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

* fix(core): address review suggestions for tool execution status (#8180)

* test(core,cli): strengthen test-efficacy for tool execution status (#8180)

* fix(core): address review suggestions for tool execution status (#8180)

- Improve post-processing cancellation message to indicate the tool had
  already completed, preventing silent model redo of completed work
- Remove dead !isExecutionTimeout conjunct in Session.ts PostToolUse
  cancellation check (unreachable: timeout always sets toolResult.error)
- Replace construct-then-delete with destructuring in withPostToolBatchStop
- Move all failure-kind constants to telemetry/constants.ts so the full
  documented vocabulary lives in one place
- Re-export StructuredToolError from tool-error.ts instead of importing
  from the unrelated priorReadEnforcement module
- Add JSDoc to normalizeToolCallEvent documenting key-absent semantics
- Add ordering-safety comment to createParentAbortRace microtask guarantee
- Document endToolExecutionSpan not_started guard as defence-in-depth
- Document PostToolBatch span leak window in finalizeToolSpan
- Add design doc note about hand-placed cancellation check invariant
- Add test for unknown execution_status normalization path
- Revert unrelated generate-notices.js formatting change

* fix(core): address review feedback for tool execution status (#8180)

- Gate cancel message on executionThrew so the model sees 'User
  cancelled tool execution.' when execute() rejected under abort,
  reserving 'already completed' wording for post-processing cancels
- Move StructuredToolError into tool-error.ts to break the
  tool-error ↔ priorReadEnforcement module cycle
- Revert unrelated Prettier reformat in generate-notices.js

* test(core): pin both tool cancellation notices; extract them as constants

afd349ca gated the cancel message on executionThrew but left the two
wordings as bare literals at four sites and added no test. That is the
exact shape the bug had: it was introduced by editing one literal and
missing the others.

Extract TOOL_CANCELLED_{BEFORE,AFTER}_COMPLETION_MESSAGE so the four
sites cannot drift, and add regression tests for both paths — a tool
interrupted mid-flight (execute() rejected under abort) must report
"User cancelled tool execution.", while a cancel after execute()
returned must report that the output was discarded. The mid-flight test
fails against the pre-afd349ca behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(core): keep MCP reconnect for a timeout on a dead transport

Classifying every `-32001` as EXECUTION_TIMEOUT skips
handleReconnectOnError, which previously recovered one real case: the
transport dies mid-request, the SDK request times out because no
response will ever arrive, and the server is already recorded
DISCONNECTED. That reconnected and retried; now it hard-fails and the
user has to retry by hand.

Divert back to the reconnect path only on positive evidence the
transport is dead. Note that getMCPServerStatus() reports DISCONNECTED
for servers it has never seen, so the guard checks for a *recorded*
DISCONNECTED — the naive comparison misroutes every timeout from a
server whose status was never registered, which broke four existing
timeout tests when tried.

A timeout on a healthy server is still EXECUTION_TIMEOUT: retrying it
after a reconnect would just double the wait. The client-side idle
timeout keeps classifying unconditionally; it is our own timer, not a
transport signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(core): address blocking review feedback for tool execution status (#8180)

Two blocking items from the maintainer review:

1. Post-processing cancellations dropped persistedOutputFiles (and
   visionBridgeNotice) along with the model-visible output, orphaning
   files the tool had already spilled to disk. createCancelledResponse
   now carries both, and every cancelAfterPostProcessing site passes
   what it has; the settle-then-abort and hook-stop paths do the same.

2. A -32001 that lands while the parent signal is aborted is the SDK's
   abort rejection or a timeout that raced with a cancel; classifying
   it EXECUTION_TIMEOUT would count user cancels against the timeout
   SLI. isExecutionTimeoutFailure now defers to the abort in both
   catch blocks, regardless of which side settled the race first. The
   two tests that pinned the opposite timeout-wins ordering are
   updated to the abort-wins semantics the review asked for.

Co-Authored-By: Qwen Code <noreply@alibaba-inc.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.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-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <253268222+qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Qwen Code <noreply@alibaba-inc.com>
2026-08-03 10:21:44 +00:00
Shaojin Wen
72bd3dccc2
ci: remove broken legacy scheduled PR triage workflow (#8434)
The Gemini-era scheduled PR triage workflow has been dead weight for a
long time:

- Its only business value — syncing labels from the linked issue to the
  PR — never fires: gh exports closingIssuesReferences as a flat array,
  so the script's '.closingIssuesReferences.nodes[0].number' jq path
  always errors, the error is swallowed by 2>/dev/null, and every PR
  falls into the "No linked issue found" branch. The latest production
  run logged 157 "No linked issue" hits and zero label syncs, despite
  many of those PRs having linked issues.
- LABELS_TO_REMOVE is computed but never applied, PRS_NEEDING_COMMENT is
  never appended to, and the prs_needing_comment job output has no
  consumer — the rest of the script is dead code.
- It burns 1+N API calls against every open PR every 15 minutes.
- The id-token: write permission is a leftover from the Gemini/GCP OIDC
  era; nothing in the bash script uses it.

Real PR triage lives in qwen-triage.yml. Remove the workflow and its
script, drop the stale docs section describing behavior it never had,
and pin the file into the legacy-workflow regression list.

Co-authored-by: verify <verify@local>
2026-08-03 10:20:42 +00:00
jinye
4338120100
feat(serve): resolve and report the daemon memory budget (#8245)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(serve): resolve and report the daemon memory budget

The daemon has no notion of how much memory it has. It samples its own
RSS and heap every five seconds, and polls the primary ACP child's RSS,
but there is no limit anywhere to divide those by: no cgroup read, no
heap-size limit, no ratio, no `limits.*` memory field. Every number it
reports is an absolute byte count with nothing to compare against, so
"how close to exhaustion is this daemon" cannot be answered from
`/daemon/status` at all.

Resolve one set of figures at boot and report them. Configured and
effective budgets are separate: the effective value is capped at resolved
cgroup or host memory, so an operator passing a budget larger than the
machine gets a denominator the machine can actually back, with the
discrepancy visible rather than silently resolved. A derived budget below
the documented minimum is reported as `insufficientMemory` rather than
clamped upward, which would have invented capacity that does not exist —
a 768 MB host would otherwise report a 1 GB budget and poison every ratio
computed from it.

`limits.memory` carries the static figures, including `legacyCeilingMb`:
the ceiling an ACP child receives today with no budget involved, so the
gap between current behavior and any future policy is measurable before
that policy exists. `runtime.memory` carries live counts and the advisory
per-child share at both the registered and the live child count.

Nothing here sizes a child. Dividing the pool by a workspace count is not
a sound policy on its own, and the advisory shares exist to show why: on
a 32 GB host with 25 registered workspaces and only the preheated primary
live, a registered-count divisor would cut that child from 16384 MB to
614 MB for memory no dormant workspace is holding, while the per-child
floor still lets 25 children authorise more than the pool. Registration
is not allocation; a real policy needs admission at spawn time keyed on
concurrently live children, and it needs this data to be designed
against.

Applying such a share is also a compatibility change even with no
refusals, since it alters child GC and OOM behavior — so it is not
something this change should slip in under the heading of reporting.

Refs #8182
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): report honest memory counts and guard the registered share (#8245)

* fix(serve): reuse workspace snapshots when counting active children

Counting active ACP children from `listManaged()` is right — `list()` only
returns entries in `active` state, so a workspace mid-drain, mid-replacement,
or blocked still holds a live child that `list()` drops. But taking the count
by calling `getDaemonStatusSnapshot()` again per managed runtime undoes the
existing reuse of the primary bridge's snapshot, and `getDaemonStatusSnapshot`
rebuilds the whole session array on every call.

The second pass also reads the tree at a different instant than the rest of
the response, so `activeAcpChildren` could disagree with the session and
channel figures beside it.

Reuse the snapshots already taken instead, keyed by bridge, and fall back to a
fresh call only for a managed runtime the first pass missed — which is exactly
the non-`active` entry the `listManaged()` change exists to catch.

The reuse this restores was already guarded by a test asserting one snapshot
call per bridge, but that test resolves no memory budget, and the second pass
ran only on the budget path — so it stayed green while every production
`/daemon/status` call did the work twice. Added a case that resolves a budget
and asserts the same property; it fails against the previous commit with
"expected 1 times, but got 2 times".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): address review feedback on daemon memory budget (#8245)

* fix(serve): import isValidMemoryBudgetMb in serve command (#8245)

* fix(serve): address review feedback on daemon memory budget (#8245)

* fix(serve): stub isChannelLive on serve test fake bridges (#8245)

* fix(serve): address review feedback on daemon memory budget (#8245)

- Make the stderr-gate test host-independent by pinning os.totalmem
  through a vi.mock toggle instead of reading the runner's cgroup
- Add a spawn-path constant parity test enforcing that
  getAcpMemoryArgs and legacyChildCeilingMb agree on the fraction
  and cap, converting the comment-only invariant into a test
- Narrow the mirror comment to name only the two constants that
  actually have spawn-path counterparts
- Accept availableMemorySource through the resolveDaemonMemoryBudget
  seam so the constrained path is testable end-to-end
- Report maxChildHeapMb alongside minChildHeapMb on the wire so
  clients can distinguish the 16 GB cap from a large host
- Move the listManaged/list comment to the computation it describes
- Add a cross-reference at the opts literal for the late-assigned
  daemonMemoryBudget field

* fix(serve): address review feedback on daemon memory budget (#8245)

* fix(serve): address review — split fraction constant, sharpen parity test, deduplicate error, populate bootstrap memory (#8245)

* fix(serve): address review — document maxChildHeapMb, make parity test order-independent (#8245)

* fix(serve): address review — split parity test into two files to avoid cold re-import timeout (#8245)

* fix(serve): address review — correct session count, compatibility scope, bootstrap memory docs (#8245)

* fix(serve): address review — align help text framing, document default cap, fix stale comment (#8245)

* fix(serve): address review — add positive memory-budget validation test, correct activeAcpChildren docs (#8245)

* fix(serve): address review — document childRssBytes stale tail after watcher detach (#8245)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Autofix <qwen-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-02 04:19:31 +00:00
Dragon
4c6e2518a3
docs: refresh architecture overview (#8325)
* docs: refresh architecture overview

* docs: complete architecture package table
2026-08-02 02:45:00 +00:00
ytahdn
554c5e44ba
feat(web-shell): support mutable default mid-turn messages (#8229)
* feat(web-shell): support mutable default mid-turn messages

* fix(serve): register mid-turn removal telemetry route

* test(serve): update telemetry route totals

* fix(test): add session_mid_turn_message_mutation to expected features list

* fix(webui): forward clientId on cross-session mid-turn removal (#8229)

- Forward the session clientId in the cross-session removeMidTurnMessage
  branch so the bridge's exact-originator match can succeed; without it the
  removal resolved to an undefined originator and could never remove the
  message stamped at enqueue.
- Strip a misaligned/malformed messageIds from mid_turn_message_injected in
  asKnownDaemonEvent instead of rejecting the whole event, mirroring the
  sidechannel parser so a buggy daemon can't silently lose the injection
  signal.
- Log a mid-turn removal miss in the bridge like the enqueue/pending-removal
  siblings, to make removal races diagnosable from daemon logs.

* fix(web-shell): exclude annotations from mid-turn path and harden idle cleanup (#8229)

* fix(web-shell): add container-type to .queuedPrompts so @container query applies (#8229)

* fix(web-shell): harden mid-turn dedupe and capability gate per review (#8229)

- removeInjectedFromQueue now matches by id first (position-independent)
  and falls back to text only when no id match exists, so two same-text
  sends can't remove the wrong row and double-deliver.
- Thread canMutateMidTurn into useQueuedPrompts and gate the mid-turn
  delete/edit mutation on it, so the keyboard path can't hit a DELETE
  route the daemon doesn't advertise.
- asMidTurnMessageInjectedData omits a malformed messageIds key instead
  of leaving a present undefined, matching the sidechannel parser.
- Narrow MidTurnQueueItem.midTurnState, document the load-bearing effect
  order, and make clearQueuedPrompts return false on a no-op clear.

* fix: harden mid-turn removal per review (log escape, cross-session client id) (#8229)

- Escape the caller-controlled messageId (and sessionId) in the mid-turn
  removal-miss stderr line to prevent log injection (CWE-117).
- Forward the target session's persisted client id on cross-session mid-turn
  removal so the bridge's exact-originator match no longer rejects valid
  removals after a session switch with per-session client ids.
- Strengthen tests: distinct-id independence for two queued messages, deferred
  removal proving the composer waits for daemon removal, and the active-turn
  delete failed-action flag.

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
2026-08-01 10:41:29 +00:00
destire-mio
412eae24b4
feat(core): add project-level fork profiles (#8148)
* feat(core): add project-level fork profiles

* fix(core): harden fork profile loading

---------

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
2026-08-01 02:20:51 +00:00
Shaojin Wen
77d8a27eda
feat(daemon): raise default max sessions from 20 to 32 (#8235)
* feat(daemon): raise default max sessions from 20 to 32

* fix(daemon): update test assertion and docs for new default max sessions (32)

* fix(daemon): sync default max sessions

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-31 17:47:51 +00:00
qqqys
7189a68334
fix(serve): isolate managed memory by selected workspace (#8056)
* fix(serve): isolate managed memory by workspace

* feat(serve): add memory project scope option

* test(serve): fix clean build type assertion

* test(serve): cover untrusted workspace memory tasks

* test(memory): cover remaining workspace paths

* test(serve): cover unavailable memory lanes

* test(memory): isolate default scope

* fix(serve): address review feedback on workspace memory isolation (#8056)

- Create secondary ACP mounts on demand for dynamically-registered
  workspaces so the qualified memory routes work beyond boot-time
  runtimes (rename getWorkspaceRememberLane → ensureWorkspaceRememberLane)
- Remove the symlink-alias machinery from getAutoMemoryRoot: it had no
  producer, relaxed a documented invariant, and could throw on the
  per-turn hot path; workspace mode now uses the same plain path.join
  as git-root mode
- Move memoryProjectScope validation into the pre-listen block
- Use the shared sendWorkspaceRuntimeUnavailable helper in server.ts
- Revert an unrelated test mock change; fix misleading 'compatibility
  fallback' wording in ServeOptions
- Add workspace_qualified_memory capability tag, docs for the new flag
  and env var

* docs(serve): add workspace_qualified_memory to conditional features table (#8056)

* fix(serve): address follow-up review feedback on workspace memory isolation (#8056)

* fix(serve): address follow-up review feedback on workspace memory isolation (#8056)

Extract MEMORY_PROJECT_SCOPES const and MemoryProjectScope type in core
so yargs choices, ServeOptions, ServeArgs, and the runQwenServe guard
share one source of truth (reduces drift risk from five to three edit
points; the fast-path guard keeps inline comparisons because an import
boundary test forbids core imports on the lightweight startup path).

Document memory-project-scope caveats in the user-facing docs: daemon
vs standalone CLI split-brain, sanitizeCwd punctuation collisions, and
flag vs env normalization differences. Add the per-lane MAX_PENDING
resource note to the developer configuration reference.

* fix(cli): add missing sessionRuntimeBaseDir to late-add workspace test (#8056)

* test(serve): cover untrusted forget/dream and no-lane memory poll (#8056)

* fix(core): extract MEMORY_PROJECT_SCOPES into zero-import leaf module (#8056)

Importing the constant as a value from the core barrel turned it into a
real static edge that pulled the entire 5.6 MB barrel into the serve
pre-listen bundle closure, breaking the fast-path gate.

Move MEMORY_PROJECT_SCOPES and MemoryProjectScope into a new
memory/scopes.ts with no imports of its own, re-export from paths.ts so
the barrel surface is unchanged, add a ./memoryScopes subpath export,
and switch run-qwen-serve.ts to the narrow import.

Also derive the unknown-scope guard in resolveWorkspaceProjectScope()
from the constant instead of hardcoding 'git-root'.

* test(cli): pin non-allocating contract in workspace memory poll test (#8056)

---------

Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.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 Autofix <qwen-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-31 15:06:38 +00:00
jinye
95657feb6e
feat(core): add GenAI time-to-first-chunk tracing (#8150)
* feat(core): add GenAI time-to-first-chunk tracing

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

* fix(core): address review feedback on GenAI streaming telemetry (#8150)

* fix(core): correct ttft_ms migration claim in telemetry docs (#8150)

* fix(core): address review feedback on GenAI streaming telemetry (#8150)

* fix(core): address review feedback on GenAI streaming telemetry (#8150)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-31 13:46:46 +00:00
Dragon
c50137cf86
fix(core): prevent subagents from asking users (#8219) 2026-07-31 12:39:04 +00:00
jinye
08798b4832
feat(core): Normalize tool-call terminal telemetry (#8176)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(core): normalize tool-call terminal telemetry

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

* fix(desktop): handle cancelled tool telemetry replay

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-31 11:51:14 +00:00
destire-mio
079ce5346a
feat(agent): add fork tool execution allowlist (#8066)
* feat(agent): add fork tool execution allowlist

* fix(agent): address fork allowlist review feedback

---------

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-30 13:58:51 +00:00
jinye
c50120985b
fix(serve): Prevent repeated workspace skill rescans (#8080)
* fix(serve): prevent repeated workspace skill rescans

Make workspace skill status reads use committed snapshots and move refresh work to explicit mutation paths. Add generation-safe daemon caching, conditional HTTP responses, SDK revalidation, and multi-session extension refresh safeguards.

Refs #8079

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

* fix(serve): narrow the workspace-skills read model and close its regressions

Follow-up to the previous commit on this branch, from reviewing it.

Subtractions — these were separable from the fix and carried more surface
than value, so they move out of this change:

- Revert the ETag / If-None-Match layer (CORS allow+expose headers, the SDK
  conditional JSON cache, the browser bundle budget bump). Express already
  emits an ETag and answers 304 for these routes, so the only new behavior
  was the SDK cache. It saves transfer bytes but no daemon work — the ETag is
  a hash of the already-serialized body — and it shipped without a paired
  `Cache-Control`, which is what actually keeps an intermediary from serving
  a stale snapshot of an authenticated, mutable resource. The SDK cache was
  also unbounded, with no eviction or clear entry point.
- Revert moving `extensions_final` ahead of skill initialization in
  `Config.initialize`. In non-safe, non-bare mode `extensions_initial` is
  already the same argument-less `refreshCache()`, and it runs
  `applyStoreActivation`, so `getActiveExtensions()` is fully populated
  before skills are enumerated either way. The move changed only startup
  event order (and pushed permissionManager past the extension refresh) for
  every surface including the interactive CLI.

Regression fixes — the read went pure, but two of its inputs lost their only
path back to disk:

- Extension sources have no watcher, unlike skills. With the per-read
  `extensionManager.refreshCache()` gone, an extension installed, removed,
  enabled, or disabled outside the daemon would never reach the snapshot
  until the child restarted — and because extension-level skills are derived
  from the extension set, a skill-watcher tick could not recover it either.
  Adds `ExtensionManager.refreshCacheIfSourcesChanged()`: a stat-based
  fingerprint over the extension directory entries, each manifest, the
  enablement file, and the store state, which refreshes only when they moved.
  A status read pays one readdir plus one stat per entry instead of a
  directory scan and a full parse, and stays self-healing.

  The baseline is the pre-load fingerprint, so a change landing during a
  refresh stays visible to the next check instead of being masked by a
  post-load stat. The directory and store halves are captured at different
  points because a refresh writes the store itself but never the manifests.

- Revalidation is skipped in safe and bare mode, and the whole of it —
  including that mode check — sits inside its error boundary. Those modes never
  populate the extension cache by design, while the snapshot derives extension
  skills from `getExtensions()`, so revalidating there would have loaded the
  extensions the mode exists to exclude. Keeping the mode check outside the
  boundary would also have let a config missing those accessors fail a read.

- `initialized: true` with an empty list when the config has no
  `SkillManager` is now `initialized: false`. The daemon latches any
  initialized answer into `lastWorkspaceSkillsStatus` and then prefers it
  over its own local enumeration, so the old value could suppress the
  fallback permanently.

Also:

- The retained-snapshot path bumped the freshness timestamp without checking
  its generation, so a read that started before an invalidation could push
  out the TTL of a snapshot a later read had committed — letting a
  post-mutation snapshot go unrevalidated for longer than the window.
- `setWorkspaceSkillEnabled` folded `configsFailed` into `sessionsFailed`,
  but it sends `reason: 'settings'`, which never refreshes a skill cache, so
  the term was structurally zero. Report `configsFailed` from the `content`
  path instead, where it can actually be non-zero.
- Documents the settings-freshness gap this read model accepts: enablement
  now comes from the child's in-memory `LoadedSettings`, which `SettingsWatcher`
  keeps current for the User and Workspace scopes but not for System /
  SystemDefaults (locked-skill policy) or an untrusted workspace.

Tests: adds a real-filesystem guard that drives 50 consecutive cached reads
and asserts zero additional readdir/readFile calls — the mocked suites could
only prove `refreshCache` was not *called*, which is not the invariant that
broke. Adds coverage for the fingerprint gate (steady state, install,
removal, in-place manifest edit, concurrent callers, and the mid-refresh
race), for the null-manager, moved-sources, and safe/bare-mode read paths, and
for the generation guard. The generation-guard and safe/bare-mode tests were
each verified to fail with their fix reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:52:37 +00:00
jinye
f3ad4fcffb
feat(serve): page large text files by byte cursor (#8002)
* fix(serve): allow bounded reads of large text files

* fix(serve): bound large-text reads by scan cost, not by which knob was set

Follow-up to the bounded large-text read path. Three changes:

Gate on any explicit window argument, not on `limit`. Gating on `limit`
had the cost model backwards in both directions: `{ line: 900_000_000,
limit: 20 }` was admitted despite walking the whole file, while
`{ maxBytes: 4096 }` — satisfiable from the first 4 KiB — was refused. A
read with no window argument at all still fails, since a caller that
believes it holds the whole file may write it back truncated.

Add MAX_TEXT_SCAN_BYTES (8 MiB). MAX_READ_BYTES caps what a read
returns; nothing capped what it cost. Line offsets are resolved by
scanning from byte 0, so a query param could turn into an
uninterruptible multi-second scan of an arbitrarily large file — and on
Windows hold a read handle for that span, blocking renames and deletes.
Past the budget the read is refused with `file_too_large` pointing at
readBytes, which reaches any offset in O(1).

Tolerate appends on streamed windows. Requiring whole-file size/mtime
stability after reading a prefix rejected reads whose returned bytes
were still valid, and the case it rejected — tailing a live log — is the
one this path exists for. Streamed windows now assert inode identity
plus "did not shrink"; truncation and replacement are still rejected.

Also: non-UTF-8 large text now returns `binary_file` rather than
`file_too_large`, so a client retrying on 413 with a smaller window
can't loop forever; and `readFileWithLineAndLimit` throws instead of
silently ignoring a caller-supplied `fileHandle` on the by-path
fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(core): thread the descriptor instead of forking text-read helpers

PR #7947 pinned large-text reads to one inode by threading a caller-owned
FileHandle into readTextRange as an optional field, plus a second field,
forceStreaming, to suppress the buffering fast path. Two optional fields
produced four combinations: one meaningful, one used by a single test, one
unreachable, and — in readFileWithLineAndLimit — one that silently fell
through to a by-path read, defeating the reason the caller opened a handle.

Unify the two encoding detectors. detectFileEncoding now takes a path or a
borrowed handle, so detectFileHandleEncoding is deleted along with the
message discrepancy between them: an encoding iconv-lite cannot load now
raises LargeNonUtf8TextError naming that encoding rather than deferring to
the decoder's generic invalid-utf8 variant. Both still refuse the file, and
the Serve boundary maps both to binary_file.

Split the reader into readTextRange (path) and readTextRangeFromHandle
(always streams, both byte bounds required). The unreachable combination and
its untested readFileHandleBuffer are gone, and with no fileHandle parameter
left for readFileWithLineAndLimit to ignore, the RangeError guarding that
fallthrough is deleted too — the trap can no longer be expressed.

CoreReadTextFileHandleRequest drops its required stats field. Nothing
downstream read it, and because the ACP request type it extends permits
extra properties, TypeScript accepted the dead argument silently.

readFileHandleChunks becomes chunksFromHandle(fh, from) — the one seam
byte-cursor text paging needs.

No observable change at the Serve boundary: its 222 tests pass unmodified.
Two fileSystemService tests were deleted rather than repaired; they asserted
the arguments readFileWithLineAndLimit received, which is nothing once the
handle path stops calling it. Their coverage lives in read-text-range.test.ts
against real files and in workspace-file-system.test.ts at the real boundary.

258 production lines in core, net -71 overall.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(core): make CoreReadTextFileHandleRequest standalone

Self-audit follow-up to f55c867a. Two fields survived the reshape that the
handle path never reads:

- `stats` was documented as required ("must pass the Stats captured from that
  handle") and nothing downstream read it. The handle path always streams, so
  it never needs a size to choose a strategy, and the encoding probe does its
  own fstat.
- `path` became dead once readTextRangeFromHandle replaced the path-plus-handle
  call. Errors are labelled with the path by the Serve boundary that owns it.

Neither was caught by the compiler: the ACP ReadTextFileRequest the type
derived from permits extra properties, so the CLI kept passing both silently.
That is the argument for declaring the type standalone rather than Omit-ing
four of six inherited fields and quietly re-admitting the rest.

Also record the second behaviour delta of the detector merge in the design
doc: detectFileEncoding catches I/O errors and falls back to 'utf-8', where
detectFileHandleEncoding let them propagate. The failure is not lost — a handle
that fails the 8 KiB probe fails the streaming read immediately after — but a
different call now reports it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(serve): page large text files by byte cursor

Line offsets address a byte stream, so `readText` resolves them by scanning
from byte 0. Paging a large log that way is O(n^2) across pages, and past
MAX_TEXT_SCAN_BYTES (8 MiB) a deep page is refused outright — agents had no
O(1) path short of dropping to GET /file/bytes and splitting lines themselves,
losing encoding handling, multibyte safety, and the binary_file refusal.

A response that leaves content behind now returns `hasMore`, and where a file
byte offset is derivable, an opaque `nextCursor`. Passing it back as `cursor`
resumes in O(1). Page 1 is an ordinary `limit` read, so clients never compute
byte offsets themselves, and a paging loop does not break when a file happens
to be small.

The cursor is unsigned base64url JSON carrying {off, size, dev, ino}, matching
encodeOrganizedCursor rather than the HMAC-signed transcript codec: the path is
re-resolved through the workspace boundary on every request, so a forged cursor
can only move the offset within a file the caller may already read — what
GET /file/bytes?offset= allows today. What the payload is for is staleness:
a replaced or truncated file yields hash_mismatch instead of bytes from the
wrong place, while an append leaves an outstanding cursor valid — the case the
feature exists for.

Every minted cursor points at the start of a line. When a single line exceeds
maxOutputBytes the reader emits a truncated prefix and skips to the next line
rather than resuming mid-line, because a mid-line cursor makes the following
page snap forward and silently drop the rest of that line at the seam. Windows
cut mid-line by a byte cap therefore report hasMore with no cursor, as do
non-UTF-8 snapshot reads whose decoded text is a UTF-8 re-encoding with no
mapping back to file offsets. That is why hasMore is a field rather than a
restatement of nextCursor.

Cursor reads branch before the size check, not by widening the window gate:
a cursor read of a file under MAX_READ_BYTES would otherwise land on the
snapshot path, which knows only line/limit, and silently return line 0.

Adds the workspace_file_read_cursor capability, per the convention that new
behavior gets a new tag, and retargets the scan-budget hint at cursor paging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(core): advance UTF-8 cursors after truncation

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

* docs(serve): clarify cursor bootstrap limits

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

* fix(sdk): raise daemon browser bundle budget

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

* test(serve): cover ACP cursor dispatch and cursor binary_file mapping (#8002)

* fix(core): only set sawCrlf for emitted lines in cursor paging (#8002)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
2026-07-30 12:07:05 +00:00
qqqys
c97026040e
feat(channels): add pairing approval management API (#8045)
* feat(channels): add pairing approval management API

* fix(sdk): expose pairing approval types

Re-export the new approval and revocation types from the public SDK entry, and pin the qualified workspace DELETE request body in regression coverage.

---------

Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
2026-07-29 14:55:03 +00:00
OrbitZore
ec9c36ef82
feat(channels): add GitLab polling channel adapter (#7862)
* feat(channels): add GitLab polling channel adapter

Poll GitLab todos via @gitbeaker/rest, dispatch notes through the
existing PollingChannelBase pipeline. Key design points:

- action_prompt_template config drives event filtering and metadata
  rendering (unconfigured actions are skipped)
- Per-repo cursor (repo[chatId].last_read) as notes window lower bound,
  global lastProcessedAt for todo-level dedup
- mark_done after successful processing; failure skips mark_done for
  retry on next poll
- Mention gating delegated to base GroupGate (adapter only sets
  isMentioned flag)
- First-contact body fallback for todos with no notes (e.g. mention in
  issue description)

* fix(channels/gitlab): persist cursor after each successful todo

Call saveCursor() immediately after advancing lastProcessedAt so that
progress is durable even if the process crashes mid-poll. Also removes
the local watermark variable in favor of direct assignment.

* fix(channels/gitlab): persist cursor on every advancement including skips

* fix(channels/gitlab): address review critical issues

- Remove non-functional proxyAgent (gitbeaker doesn't support it)
- Construct repo_url from host + path (API doesn't return web_url)
- Handle directly_addressed action (falls back to mentioned template)
- First-contact fetches target description instead of using todo.body
- Move todo.project dereference inside try block
- Filter confidential notes
- Update channel-registry.test.ts for gitlab entry

* fix(channels/gitlab): address review suggestions

- Warn on connect if action_prompt_template is not configured
- Guard todo.target.iid before use
- Skip paths now mark_done (best-effort) to clean GitLab UI
- Remove postErrorComment (avoids duplicate comments on retry)
- Fetch only first page of notes (desc, maxPages:1, perPage:100)
  instead of paginating entire note history
- Extract fetchRecentNotes for single-page windowed enumeration

* refactor(channels/gitlab): simplify to todo.body dispatch, add description mention support

- Remove notes API fetching; dispatch todo.body directly
- Detect description mentions via target_url anchor (#note_ absence)
- Always fetch target description for %description% metadata
- Remove per-repo cursor; dedup via cursor + mark_done only
- Cursor advances regardless of success/failure (no retry)
- Use zod for cursor validation
- Rename template vars to GitLab terminology:
  %project% %project_url% %target_type% %iid% %title% %description% %todo_id%
- Support %% escape for literal percent

* docs(channels): add GitLab adapter documentation

- New user guide: docs/users/features/channels/gitlab.md
- Update _meta.ts navigation
- Update developer adapter matrix and SDK list

* fix(channels/gitlab): use correct Issues.show(issueIid, { projectId }) signature

* chore: regenerate NOTICES.txt for new gitlab channel dependencies

* fix(channels/gitlab): address review suggestions

- Add todo.project null guard (item 2)
- Single-pass regex for %% escape + %var% substitution (item 4)
- sendThreadMessage throws directly on undefined threadId (item 5)
- Dedup fetchDescription with per-poll cache (item 6)
- Remove per-todo saveCursor; base class saves after pollOnce (item 7)
- Add undefined threadId test (item 8)
- Expand confidential notes limitation in docs (item 3)

* test(channels/gitlab): add mention tests, directly_addressed coverage, skip assertions, temp cleanup

- New mention.test.ts: 14 cases for testBotMention/stripBotMention/escapeRegex
- Add directly_addressed fallback test
- Skip tests now assert TodoLists.done + cursor advancement
- afterEach cleans up mkdtempSync temp dirs

* fix(channels/gitlab): address review round 4

- Non-mention actions (assigned, etc.) set forceMentioned=true to bypass GroupGate
- Merge dead note-filter tests into single 'skips todo authored by bot'
- Log fetchDescription errors to stderr instead of silent swallow
- Post error comment on issue/MR when handleInbound fails (best-effort)

* fix(channels/gitlab): always force isMentioned=true, remove regex re-derivation

The action_prompt_template config is already the event filter, and
GitLab has already decided the mention when creating the todo.
Re-deriving isMentioned via regex on todo.body causes permanent
message loss when the regex misses (description mention + fetch
failure, group mentions). Always set forceMentioned=true so
GroupGate never drops a todo that passed the template filter.

* fix(channels/gitlab): propagate fetchDescription errors for description mentions

For note mentions, description is metadata-only — fetch failure is
logged and swallowed. For description mentions, description IS the
message — fetch failure now propagates to the outer catch, which
posts the ⚠️ error comment so the user knows to re-mention.

* perf(channels/gitlab): clean up stale todos, skip unnecessary fetchDescription

- Mark stale todos (updated_at <= cursor) as done on each poll to
  prevent perpetual re-fetching of pre-existing pending todos
- Skip fetchDescription for note mentions when template does not
  contain %description%, saving one API call per todo
- Update docs: stale todo cleanup, error comment on failure

* docs(channels/gitlab): clarify requireMention is bypassed, template is the real filter

* Apply suggestions from code review

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(channels/gitlab): use todo ID cursor instead of timestamp to eliminate equal-timestamp loss

Timestamp-based cursors (second granularity) could silently destroy
todos sharing the same updated_at as the cursor boundary. Switch to
monotonically increasing todo IDs which are unique and collision-free.

Add initialized flag to preserve first-start drain semantics: pre-existing
pending todos are marked done without dispatch on the first poll cycle.

* fix(channels/gitlab): harden first-poll drain, add ordering tests, fix lockfile

- Replace Math.max(...spread) with reduce to avoid RangeError on large
  backlogs (~100k+ todos). Move initialized=true after the drain work so
  any throw retries the drain instead of falling through to dispatch.
- Add unit tests: identical-timestamp delivery and id-order-when-updated_at-disagrees
  (kills M2 sort mutant).
- Align lockfile: file:../base → ^0.21.0 for channel-base dep.

* fix(channels/gitlab): include dot in mention lookahead for GitLab usernames

GitLab usernames may contain dots (e.g. bot.name). The lookahead
character class inherited from GitHub omitted '.', causing @bot.name
to match as @bot. Add '.' to the negated class.

* docs(channels/gitlab): align docs with ID cursor and drain semantics

- Add first-poll drain as step 2 in How It Works
- Clarify GroupGate always passes (isMentioned forced true)
- Document initialized flag in Known Limitations

* Apply suggestions from code review

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(channels/gitlab): align package version and channel-base dependency to 0.21.1

Bump version from 0.21.0 to 0.21.1 to match other channel packages after
upstream merge. Pin @qwen-code/channel-base to exact 0.21.1 instead of
^0.21.0, matching the convention used by other published channels.

* fix(channels/gitlab): regenerate lockfile to match package.json versions

Manually add only gitlab-related lockfile entries (workspace, @gitbeaker
packages, transitive deps, channel-gitlab link) without unrelated npm
normalization churn.

* test(channels/gitlab): add regression tests for first-poll drain hardening

Two tests that kill the M1 (Math.max spread RangeError) and M2 (flag
ordering) mutants which survived the original 46-test suite:

- 150k todo drain verifies reduce() handles large backlogs without
  RangeError and without dispatching
- Drain throw verifies initialized stays false so the next poll retries
  the drain instead of falling through to dispatch

Test file duration: ~40ms → ~170ms.

* docs(channels/gitlab): clarify groupPolicy must be "open" and add runtime warning

The default groupPolicy "disabled" silently drops all mentions — todos are
marked done and cursor advances, but no dispatch occurs. Fix misleading docs
that said "GroupGate always passes" (only true at groupPolicy: "open") and
add a connect()-time warning when groupPolicy is not "open".

* fix(channels/gitlab): correct xcase integrity hash in lockfile

The manually added xcase entry had a typo in the sha512 hash (ys → ks),
causing npm ci EINTEGRITY failures in CI.

* fix(channels/gitlab): correct requester-utils integrity hash in lockfile

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

* fix(channels/gitlab): allow groupPolicy "allowlist" in warning and docs

The groupPolicy warning and docs incorrectly stated that groupPolicy
must be "open". In reality "allowlist" with the project listed also
works because isMentioned is forced true and GroupGate only requires
the group to be listed. Also fix the inaccurate "no error is logged"
claim — ChannelBase logs preflight rejected reason=group_disabled.

Fixes R5-🟡3 from PR #7862 review.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-29 14:28:31 +00:00
jinye
4615f84d73
fix(serve): allow bounded reads of large text files (#7947)
* fix(serve): allow bounded reads of large text files

* fix(serve): bound large-text reads by scan cost, not by which knob was set

Follow-up to the bounded large-text read path. Three changes:

Gate on any explicit window argument, not on `limit`. Gating on `limit`
had the cost model backwards in both directions: `{ line: 900_000_000,
limit: 20 }` was admitted despite walking the whole file, while
`{ maxBytes: 4096 }` — satisfiable from the first 4 KiB — was refused. A
read with no window argument at all still fails, since a caller that
believes it holds the whole file may write it back truncated.

Add MAX_TEXT_SCAN_BYTES (8 MiB). MAX_READ_BYTES caps what a read
returns; nothing capped what it cost. Line offsets are resolved by
scanning from byte 0, so a query param could turn into an
uninterruptible multi-second scan of an arbitrarily large file — and on
Windows hold a read handle for that span, blocking renames and deletes.
Past the budget the read is refused with `file_too_large` pointing at
readBytes, which reaches any offset in O(1).

Tolerate appends on streamed windows. Requiring whole-file size/mtime
stability after reading a prefix rejected reads whose returned bytes
were still valid, and the case it rejected — tailing a live log — is the
one this path exists for. Streamed windows now assert inode identity
plus "did not shrink"; truncation and replacement are still rejected.

Also: non-UTF-8 large text now returns `binary_file` rather than
`file_too_large`, so a client retrying on 413 with a smaller window
can't loop forever; and `readFileWithLineAndLimit` throws instead of
silently ignoring a caller-supplied `fileHandle` on the by-path
fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): harden large text range snapshots

Treat caller-owned file handles as bounded streaming reads, cap them to the captured file size, and reuse the chunk buffer.

Restore strict Serve snapshot stability and align returned-slice metadata with the full-snapshot path.

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

* fix(serve): make large text ranges snapshot-safe

* fix(serve): harden large-text ctime tests and document buffer reuse (#7947)

Address review feedback on the large-text range read PR:

- Pause before restoring mtime in the two ctime-dependent mutation tests so the change-time advances past the pre-read snapshot even on coarse-resolution filesystems, removing a latent flake in the same-size-overwrite precondition. The assertions are unchanged.

- Document at the readFileHandleChunks yield site that the 512 KiB buffer is reused across iterations, so yielded views must be decoded or copied before advancing the generator.

* docs(serve): soften same-size rewrite guarantee to coarse-clock best-effort (#7947)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
2026-07-29 07:49:51 +00:00
jinye
788e5cd3a8
feat(core): add ARMS session user ID (#7921)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-28 11:10:38 +00:00
ytahdn
8785216be5
feat(web-shell): add monitor task details (#7817)
* feat(web-shell): add monitor task details

* fix(web-shell): align monitor tab title with merged snapshot and reset expansion (#7817)

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-27 12:30:17 +00:00
Shaojin Wen
4958120c21
docs(channels): Document loops and proactive delivery (#7628)
* docs(channels): document loops and proactive delivery

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

* docs(channels): clarify standalone vs daemon loop storage paths (#7628)

---------

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-07-26 07:32:18 +00:00
samuelhsin
3a6c8e0c03
feat(skills): add overridable default-disabled state (#7357)
* feat(skills): add overridable default-disabled state

* fix(skills): address review feedback on default-disabled PR (#7357)

- Fix disabledChanged comparison in SkillsManagerDialog to use
  previousDisabled (locked names filtered) instead of workspaceDisabled,
  preventing spurious settings writes when a skill is disabled at both
  workspace and higher scope
- Import SettingScope as a value instead of string-casting literals in
  skill-settings.ts for compile-time safety
- Add dual-key change test: enabling a workspace-hard-disabled
  default-disabled skill produces both skills.disabled and
  skills.enabled changes in one operation
- Add legacy inactive-extension branch tests: reject when
  disabledReason is undefined and skill is not in settings
  disablements; allow when it is disabled by settings

* fix(cli): address skills picker review feedback (#7357)

Extract the skills picker's workspace persistence computation into a tested pure function so orphaned workspace disables (skills not currently loaded) are explicitly preserved and pinned by a regression test. Also add an integration test asserting a workspace-scope hard disable surfaces disabledReason 'hard' through the full loadSettings -> resolveSkillSettings -> mapSkillConfigToStatus pipeline.

* fix(cli): resolve skill disablements in safe mode for status API (#7357)

* fix(cli): dynamically import skill-settings in serve to keep fast-path closure clean (#7357)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix[bot]@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
2026-07-25 18:29:20 +00:00
Shaojin Wen
a8a28a1137
fix(acp-bridge): raise live journal caps and expose as daemon config (#7715)
The live journal (DAEMON-009) caps were too conservative for real-world
agent turns: 2000 events / 2 MiB caused 79% event loss on a typical
long turn (9647 events). Raise defaults to 10 000 events / 8 MiB and
expose them as --max-journal-events / --max-journal-bytes CLI flags,
following the same config path as --compacted-replay-max-bytes.

Also fix stale docs that described the liveJournal as uncapped.
2026-07-25 14:14:09 +00:00
OrbitZore
62e009a952
feat(channels): GitHub polling adapter with notification-as-wakeup architecture (#7632)
* feat(channels): add GitHub polling adapter with notification-as-wakeup architecture

Introduce a GitHub channel adapter that monitors notifications and
responds to @mentions on issues/PRs by posting comments. Uses
last_read_at as a per-thread watermark for comment enumeration,
replacing the unreliable latest_comment_url approach.

Foundation changes to ChannelBase:
- sendThreadMessage for thread-targeted delivery (IM adapters unchanged)
- Envelope.metadata appended to prompt after command parsing
- chat_thread session scope (channel:chatId:threadId) prevents
  cross-repo session collision
- polling-helpers: testBotMention/stripBotMention (separate detection
  from stripping, no whitespace collapsing), cursor persistence,
  abortableSleep

GitHub adapter design:
- Notifications as wake-up signals only (unread filtering)
- listComments enumeration with last_read_at watermark
- Bot self-comment filtering, case-insensitive mention regex
- In-memory recentlyProcessed set for mark-read failure dedup
- First-contact: new issue body @bot triggers processing
- Error comment + cursor advance on handleInbound failure
- pollInterval minimum 60s, exponential backoff 2s-30s

* refactor(channels): extract PollingChannelBase from polling-helpers

Replace the loose polling-helpers module with a PollingChannelBase<Cursor>
abstract class that encapsulates the poll loop, cursor persistence (JSON,
atomic write), exponential backoff, and start/stop lifecycle. Subclasses
implement only pollOnce() and createInitialCursor().

- Delete polling-helpers.ts (cursor fns + abortableSleep moved into base)
- Move mention utilities (testBotMention/stripBotMention) to github pkg
- GithubAdapter now extends PollingChannelBase<{ lastProcessedAt }>

* fix(channels): remove Gitea/GitLab mention from sendThreadMessage JSDoc

* fix(channels): match /pulls/N in notification subject URL

GitHub PR notifications use /repos/{owner}/{repo}/pulls/{N} in
subject.url, not /issues/{N}. The regex only matched /issues/,
causing PR notifications to be skipped and marked read.

Also sets threadId to 'pr:N' for PRs (was always 'issue:N').

* test(channels): add PR body first-contact unit test

Verify that PR notifications with @mention in the body (not a comment)
correctly trigger the first-contact path: extractFromSubjectUrl matches
/pulls/N, listComments returns empty, tryFirstContactBody fetches the
PR body and dispatches to handleInbound with threadId 'pr:N'.

* feat(channels): read pollInterval from channel config in PollingChannelBase

Move pollInterval config reading from GithubAdapter to the base class.
The user's configured pollInterval in settings.json is now respected
directly without a minimum enforcement. Defaults to 60000ms when not
configured.

* fix(channels): prepend metadata before prompt text

Agent sees issue/PR context (type, title, URL) before the user's
request, improving comprehension. Metadata is still appended after
slash-command parsing so commands are not affected.

* refactor(channels): route all ChannelBase delivery through sendThreadMessage

Replace all internal sendMessage calls with sendThreadMessage, passing
envelope.threadId (or target.threadId / undefined) so polling adapters
can deliver to the correct thread. IM adapters are unaffected — the
default sendThreadMessage falls through to sendMessage.

* docs(channels): document sendThreadMessage delivery architecture

* fix(channels): address review findings

- Cap recentlyProcessed Set at 10k entries to prevent unbounded growth
- Validate cursor JSON shape (non-null object) in loadCursorFromDisk
- sendThreadMessage falls through to sendMessage when threadId is
  undefined instead of silently dropping
- Remove duplicate pollInterval from GithubConfig (now in ChannelConfig)
- Fix chat_thread routing key trailing colon when threadId is undefined

* docs(channels): fix metadata JSDoc — prepended, not appended

* fix(channels): use recentlyProcessed dedup for first-contact body

Replace the fragile createdAt-vs-cursor check in tryFirstContactBody
with the recentlyProcessed set. The cursor advances globally based on
notification updated_at — when a different notification with a later
updated_at is processed first, the cursor can advance past the issue's
created_at, causing the first-contact check to incorrectly skip the
issue body (forget reply bug, found in E2E TC-2b).

* refactor(channels): two-layer dedup for GitHub adapter

Layer 1: global cursor filters notifications by updated_at (sorted
ascending, old first). Layer 2: server-side last_read_at filters
comments by created_at (sorted ascending).

- Delete recentlyProcessed Set (no longer needed)
- Sort notifications by updated_at ascending before processing
- Sort comments by created_at ascending before processing
- Pass latest comment created_at to markThreadAsRead as last_read_at

* fix(channels): address review findings on GitHub adapter

Blockers:
- sessionScope: add defaultSessionScope to ChannelPlugin, apply in
  parseChannelConfig so router and adapter agree on 'chat_thread'
- channel-registry.test.ts: add 'github' to expected type list

Should-fix:
- Replace per-thread markThreadAsRead (PATCH) with bulk
  markNotificationsAsRead (PUT /notifications + last_read_at).
  API errors stop the batch without marking failed notifications
  read; handleInbound errors still advance (error comment posted).
- connect() throws on bot identity failure instead of failing open
- metadata appended after promptText (inside sender attribution)
- isSharedSessionTarget includes 'chat_thread' scope

Nits:
- startPollLoop re-entrancy guard
- clean-package-build-artifacts.js includes github
- index.ts re-exports GithubChannel

* fix(channels): use max updated_at of all fetched notifications as last_read_at

Prevents re-fetching the same notifications in the next poll cycle.
The bulk PUT /notifications marks all fetched notifications as read
up to the max updated_at, regardless of per-notification success.

* fix(channels): address review round 2 findings

- #12: loadCursorFromDisk rejects arrays
- #13: pollInterval validates positive finite number
- #19: first-contact gate uses dispatchedMention flag (not newComments.length)
- #25: stripBotMention no longer trims (preserves indentation)
- #27: remove adapter-level requireMention, unify on GroupGate
- #31: add chat_thread SessionRouter routing key tests
- #33: clear metadata on collect-mode synthetic envelope
- #35: fix PollingChannelBase.test import path
- #36: add @octokit/rest to 15-channel-adapters.md dependencies

* docs(channels): document known limitations for GitHub adapter

- First start skips existing unread notifications (cursor = now)
- Requires classic PAT (fine-grained PATs lack notifications API)
- PR review comments not enumerated (issue comments only)

* fix(channels): address review round 3 findings

- #9: buildMetadata derives web URL from baseUrl (GHE support)
- #12: sendThreadMessage throws on invalid threadId format
- #19: mention lookbehind matches cc:@bot and "@bot" patterns
- #23: cursor file name uses sha256 hash to prevent collision
- #26: test verifies cursor persistence to disk
- #31: postErrorComment double-failure logs to stderr
- #45: tests use mkdtempSync isolation instead of real QWEN_HOME

* fix(channels): pass threadId through pairing flow + sendResponseMessage test

- #13+16: onPairingRequired receives envelope.threadId and passes it
  to sendThreadMessage, so pairing codes are delivered on threaded
  channels (GitHub) instead of throwing
- #6: add test verifying sendResponseMessage resolves threadId from
  router.getTarget and passes it to sendThreadMessage

* fix(channels): pass proxy to Octokit for daemon-worker environments

- #44: read this.proxy from ChannelBaseOptions and pass
  HttpsProxyAgent to Octokit request.agent, matching the
  Telegram adapter pattern

* fix(channels): address review findings — immutable senderId, comment time window, validateCursor, retry wrapper

- senderId uses immutable user.id; allowedUsers resolved to IDs at connect
- Comment filter upper bound: updated_at <= maxUpdatedAt (batch window)
- Per-notification errors use continue (best-effort), not break
- validateCursor() virtual hook for subclass cursor shape validation
- sendThreadMessage/postErrorComment wrapped in githubApi() retry
- webOrigin handles default api.github.com → github.com
- Docs: classic PAT only, markNotificationsAsRead, dedup claims removed
- Tests: threadId priority, metadata consumption, defaultSessionScope,
  QWEN_HOME isolation, persistent mock rejection

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

* fix(channels): mark notifications read before processing to prevent duplicate replies

Bot's own replies bump notification updated_at past the pre-captured
maxUpdatedAt, so markNotificationsAsRead(maxUpdatedAt) failed to mark
them read — the next poll re-fetched the same comments and replied
again.

Move markNotificationsAsRead + cursor advance before the processing
loop (best-effort delivery). This is safe because bot's own comments
do not flip notifications back to unread. Update docs to reflect the
new poll cycle order and best-effort semantics.

* fix(channels): update sender gate after allowedUser ID resolution and harden tests

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

* fix(channels): cursor-based comment window to prevent duplicate replies

PUT /notifications is async (202) with a last_read_at cutoff — the
bot's reply bumps updated_at past the cutoff before the server
processes the mark, so the notification is never marked read and gets
re-fetched on the next poll, causing duplicate replies.

Use the cursor value before advancement as an exclusive lower bound
for the comment enumeration window: (windowSince, maxUpdatedAt].
Comments already eligible in a previous poll are excluded regardless
of whether the mark succeeded. Zero new persistent state.

* fix(channels): cursor-based comment window to prevent duplicate replies

PUT /notifications is async (202) with a last_read_at cutoff — the
bot's reply bumps updated_at past the cutoff before the server
processes the mark, so the notification is never marked read and gets
re-fetched on the next poll, causing duplicate replies.

Use the cursor value before advancement as an exclusive lower bound
for the comment enumeration window, with per-notification last_read_at
as the preferred lower bound when available (server-side per-thread
watermark). Comments already eligible in a previous poll are excluded
regardless of whether the mark succeeded. Zero new persistent state.

* fix(channels): address review findings — null guard, cursor validation, metadata dedup, abortable sleep, docs

- Guard against null notification.subject.url in pollOnce
- Validate lastProcessedAt is a parseable date in validateCursor
- Add metadata: undefined to second collect-mode drain path
- Refactor abortableSleep as protected method on PollingChannelBase
- Fix docs: requireMention is nested under groups.*
- Add tests: chat_thread shared session, dispatchedBodies eviction,
  cursor enumeration window, last_read_at in mention tests

* docs(channels): sync docs with implementation — cursor shape, error handling, GitHub adapter tables, first-contact

- Design doc: update Cursor to { lastProcessedAt, dispatchedBodies? }, add
  validateCursor date check, abortableSleep protected method, break-on-error
  semantics, subject.url null guard
- Developer docs: add GitHub to adapter table and adapter matrix
- User guide: add first-contact step to How It Works, clarify mark-before-process

* fix(channels): address review round 2 — error dedup, abortable retry, backoff reset, window test

- Record dispatchedBody on first-contact handleInbound failure to prevent
  duplicate error comments when mark-read async hasn't taken effect
- Use abortableSleep instead of raw setTimeout in githubApi retry so
  disconnect() can interrupt rate-limit cooldowns
- Reset consecutiveErrors in startPollLoop so stop/restart cycles don't
  inherit stale elevated backoff
- Add test for cursor window client-side lower-bound exclusion filter

* fix(channels): address review round 3 — cursor validation, error dedup, sender gate, bot-self body

- validateCursor: normalize falsy non-array dispatchedBodies (false/0/""/null)
  to [] instead of passing them through to .includes() which throws TypeError
- Set dispatchedMention after postErrorComment to prevent first-contact from
  posting a duplicate error comment on the same thread
- Only set dispatchedMention when the sender passes the sender gate, so a
  disallowed commenter's mention no longer suppresses a valid first-contact
  body from an allowed issue author
- Skip bot-authored issue bodies in tryFirstContactBody to prevent
  self-response loops under open sender policy

* fix(channels): address review suggestions — test coverage, cursor filename, assertion precision

- Pairing flow: add threadId pass-through regression test
- pollInterval: add table-driven edge cases (0, -1, NaN, Infinity, string)
- Add null-URL notification followed by valid notification batch test
- Fix comment window test to assert paginate call 3 (listComments) not call 2
- Truncate cursor filename encoded prefix to 200 chars (filesystem 255 limit)
- Assert mark-read uses batch maxUpdatedAt, not just { read: true }
- Assert real GitHub plugin declares defaultSessionScope chat_thread
- Add invocationCallOrder assertion for mark-before-process ordering

* fix(channels): address review round 4 — allowedUsers throw on resolve failure, crash table fix, mark-read failure test

* fix(channels): address review round 5 — created_at filter, retry-after NaN guard, retry/sendThreadMessage tests, docs fixes

* fix(channels): address ci-bot review 4778587403 — reconnect idempotency, github type enumerations, retry/webOrigin tests

* chore(channels): align channel-github version to 0.21.0 after upstream merge

* chore(channels): update package-lock.json for channel-github 0.21.0

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

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: OrbitZore <orbitzore@users.noreply.github.com>
2026-07-25 09:31:50 +00:00
jinye
c4859627a7
feat(serve): Hot-reload workspace trust changes (#7268)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(serve): hot-reload workspace trust changes

Rebuild workspace runtime generations when trust policy changes, fail closed across daemon routes, and expose reconciliation status to SDK and Web Shell clients.

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

* codex: address PR review feedback (#7268)

Document the trust hot-reload capability and reuse the daemon environment fallback so the serve process environment guard remains satisfied.

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

* fix(cli): cache workspace trust status snapshots

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

* fix: address trust reload race regressions

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

* fix(serve): avoid repeated runtime containment

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

* fix(serve): harden workspace generation boundaries

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

* fix(serve): restore stale session owner fallback

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

* fix(serve): preserve workspace metadata across trust reloads

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

* fix(serve): align hot-reload trust semantics

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

* fix(serve): stop git-state watcher on dispose only, fix git chip test (#7268)

beginDrain stopped the git-state watcher but cancelDrain had no way to
restart it, leaving the watcher disposed until the next lazy poll.
disposeRuntime already stops git-state when the drain is committed, so
the beginDrain stop was redundant — remove it.

Also fix the WorkspaceSection git chip test that broke when the trigger
changed from <button> to <span role="button"> inside DropdownMenuTrigger:
use closest('[role="button"]') and interact with the dropdown menu item.

* fix(serve): address review feedback on trust polling and setValue assertion (#7268)

* fix(cli): correct daemon trust policy settings precedence and drain continuation (#7268)

* fix(serve): address review feedback on fork cleanup, persist simplification, sync guard, and a11y (#7268)

* fix(serve): assert before mutate in setValue, add pre-mutation guard, trust-before-generation ordering (#7268)

* fix(serve): honor system defaults in trust policy

Apply the documented settings precedence to daemon folder trust evaluation and keep workspaces outside configured trust rules fail-closed.

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

* fix(serve): preserve managed scratch trust during reloads

Keep daemon-created scratch workspaces trusted across policy reloads while retaining controlled-root validation, and reject trust mutations that cannot apply to these fixed-trust runtimes.

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

* fix(serve): guard auth provider persistence by generation

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

* refactor(serve): remove Web Shell trust UI

Keep this PR focused on daemon and SDK trust reconciliation; the Web Shell integration can follow separately.

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

* fix(sdk): restore workspace trust bundle budget

Preserve the merge-only browser bundle allowance required by the additive workspace trust v2 SDK surface after rebasing.

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

* fix(cli): handle trusted folder write failures

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

* fix(cli): keep capabilities available during trust reload

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

* fix(cli): address review feedback for workspace trust hot reload (#7268)

Drop the closed generation guard before retrying dynamic workspace
runtime creation so the retried runtime starts with a fresh, open guard
instead of inheriting the one closed during the abandoned attempt. Make
the /workspace/reload trust reconcile fire-and-forget with a swallowed
rejection (failures are reported separately), reuse sendGenerationClosedError
for the memory write error path, and assert the subagent deletion commit
boundary once before unlinking so a closed generation fails atomically.
Add coverage for the blocked-entry deep health probe and the /session/:id/cd
generation-close-during-flight path.

* fix(serve): close trust reload cleanup gaps

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

* fix(serve): use fire-and-forget for trust reconcile in workspace-qualified reload (#7268)

* fix(serve): address review feedback on generation guard and trust reconciler (#7268)

* fix(serve): use shared helpers for untrusted/generation-closed responses (#7268)

* fix(serve): continue cleanup after drain commit errors

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

* fix(cli): retry transient trust policy disappearance

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

* fix(serve): align status provider trust default with route-level check (#7268)

* fix(serve): clean up worktree on generation guard abort (#7268)

* fix(cli): guard tool and skill settings commits

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

* test(cli): add discriminating persistent-ENOENT test for trust policy read (#7268)

* fix(serve): close runtime generation gaps

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

* fix(serve): preserve scheduled task cap errors

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

* fix(serve): address review feedback on trust reconciler, settings guard, and route simplification (#7268)

* fix(serve): preserve containment retry semantics

Restore the last verified trust-reconciliation and generation-guard behavior after the automated review fix marked an unconfirmed disposal as contained and removed per-scope commit checks. Defer the remaining late-round suggestions to avoid expanding the PR.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Autofix <qwen-autofix@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
2026-07-25 08:43:07 +00:00
Gaurav
f4e333c580
fix(mcp): harden OAuth callback handling (#7510) 2026-07-25 03:56:27 +00:00
qqqys
541c80d678
feat(serve): expose workspace Channel management API (#7637)
* feat(serve): expose workspace Channel management API

* fix(cli): add workspace boundary guards to stop and remove channel methods (#7637)

* fix(cli): defer channel management runtime loading

* test(serve): cover channel management capability

* fix(serve): scope channel ownership to workspace and address review feedback (#7637)

* fix(serve): scope assertOwnedRuntime to workspace and add no-store to mutation routes (#7637)

* fix(serve): serve channel-types without resolving management service (#7637)

* fix(serve): add validateClient to GET /channels and expand invalid-client test coverage (#7637)

* fix(serve): prevent double 400 response when both name and body validation fail (#7637)

* test(serve): assert workspace reload is blocked

* test(serve): assert workspace reload is blocked (#7637)

* test(serve): expand route test coverage for review suggestions (#7637)

* refactor(cli): compose workspaceCommittedNames from workerFor to remove duplicated predicate (#7637)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-25 01:09:28 +00:00
jinye
055523afab
feat(core): Align GenAI content telemetry fields (#7667)
* feat(core): align GenAI content telemetry fields

Capture provider-final GenAI messages and tool payloads with the shared ARMS/OpenTelemetry field contract, and retire equivalent private content aliases.

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

* fix(telemetry): address content observability review

Keep ACP tool telemetry best-effort, improve diagnostics and sensitive-off coverage, and document finish-reason and tool-description behavior.

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

* docs(telemetry): clarify optional tool parameters

Document that invalid optional parameter schemas are omitted while required tool identities remain ordered and complete.

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

* docs(core): clarify telemetry compatibility behavior

Document the changed semantics of deprecated helpers and the reason the fallback context shadows key operations.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-24 18:42:48 +00:00
jinye
fcd358ae45
feat(core): align GenAI request telemetry fields (#7635)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-24 04:34:00 +00:00
Shaojin Wen
45c8d8f8cc
docs: refresh subagent lifecycle guidance (#7624)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-24 03:03:27 +00:00
BaboBen
bfe8ab4fea
feat(daemon): add explicit channel delivery (#7388)
* docs(channels): define immediate delivery v1

* feat(channels): add proactive delivery boundary

* feat(daemon): route channel delivery to workers

* feat(scheduler): persist channel delivery targets

* feat(daemon): accept reverse channel deliveries

* feat(daemon): deliver prompt and scheduled finals

* feat(daemon): add synchronous channel notify

* fix(channels): classify Feishu delivery network errors

* fix(daemon): harden channel delivery failures

* fix(channels): wrap DingTalk proactive token fetch in typed delivery error

A proactive token fetch failure escaped sendProactiveChunk as a plain Error, bypassing ChannelProactiveDeliveryError classification. Wrap it as a transient typed error (preserving the cause) so downstream classification dispatches on it consistently. Also cover the scheduled-source branch of the bridge channel-delivery ext-method, which previously had no test coverage.

* refactor(cli): rename shutdown drain constant to reflect dual scope (#7388)

* fix(cli): support delivery updates in scheduled-task PATCH (#7388)

* fix(cli): address review feedback on channel delivery (#7388)

- Guard empty text before scheduling channel delivery in both prompt
  and cron paths, avoiding unnecessary IPC round-trips when the model
  emits only tool calls
- Import MAX_CHANNEL_DELIVERY_NAME_LENGTH and
  MAX_CHANNEL_DELIVERY_TARGET_ID_LENGTH from core instead of
  redeclaring them, preventing potential drift between the persistence
  and API validation layers
- Remove dead thought parameter from #collectChannelDeliveryText —
  all call sites pass a single argument

* fix(cli): add cross-reference and invariant comments for channel delivery (#7388)

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

* fix(cli): revert doc corruption and harden delivery event publish (#7388)

* fix(docs): revert prettier-mangled GitHub Action doc and add prettierignore guard (#7388)

* fix(cli): address review feedback on channel delivery (#7388)

- Make deliverChannelMessage error workspace-aware when routing fails
- Discriminate DaemonChannelDeliveryResultData union on status field
- Add old-target rejection assertion to PATCH delivery test
- Add IPC send failure path tests for channel delivery
- Add rehydration → authorization store integration test

* docs(cli): clarify delivery text scope, consume tradeoff, and 503 expectation (#7388)

* fix(cli): address review feedback on channel delivery (#7388)

- Document single-turn invariant on channelDeliveryCollector field
- Note adapter disposition drop in design doc for V1 scope
- Add text length bound to IPC request validator (defense-in-depth)

* docs(daemon): define final-only channel delivery

* fix(daemon): deliver only final response blocks

* test(daemon): cover cancelled scheduled delivery

* fix(daemon): skip delivery for unbound tasks, consolidate error codes (#7388)

* fix(feishu): validate message response codes

* Revert "fix(feishu): validate message response codes"

This reverts commit 85fc0943d69d2f5e7535477f2d46bfcd8c5c3c1e.

* refactor(channels): defer DingTalk error classification

* fix: apply CR suggestions from AI review

* test(channels): cover delivery failure paths

* fix(cli): add required ink FrameCell/ReadonlyFrame properties to selection tests

The branch was behind main where d064bd7dc updated ink types to require
selectable, flowId on FrameCell and boundaries on ReadonlyFrame. Sync
the test fixtures to satisfy the current type definitions.

* Revert "fix(cli): add required ink FrameCell/ReadonlyFrame properties to selection tests"

This reverts commit 103587cfd8d13664abb3a125958ccec9b1c7d9ec.

* fix(cron): preserve armed one-shots across reloads

* fix(channels): preserve safe delivery diagnostics

* fix(cli): avoid global env access in delivery handler

* fix(docs): remove obsolete prettier ignore

* fix(daemon): harden channel delivery boundaries

---------

Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-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 <qwen-code@users.noreply.github.com>
2026-07-23 17:44:23 +00:00