mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-22 15:34:06 +00:00
111 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0ef3a76bda
|
feat(web-shell): add artifact right panel (#6591)
* feat(web-shell): add artifact right panel * fix(web-shell): address artifact panel review feedback * fix(web-shell): handle artifact panel review edge cases * fix(web-shell): tighten scheduled task parsing * fix(web-shell): address artifact panel review followups * fix(web-shell): guard large file diff stats * fix(web-shell): address review panel suggestions * test(webui): stabilize heartbeat prompt cleanup test * fix(web-shell): address artifact review refresh issues * test(web-shell): stabilize ChatPane artifact hook mock * fix(web-shell): clear stale session artifacts while loading * fix(web-shell): preserve artifact tabs during refresh * fix(web-shell): address artifact review followups * fix(web-shell): respect workspace cwd for artifact outputs * fix(web-shell): scope artifact panel actions to pane * fix(web-shell): resolve split pane merge conflict * fix(web-shell): clear stale artifact panel state * fix(web-shell): preserve leading turn outputs * fix(web-shell): tighten turn output selectors * fix(web-shell): harden artifact preview sanitizer * fix(web-shell): address artifact panel review regressions * fix(web-shell): reconcile split pane artifact snapshots * fix(web-shell): clear pane artifacts on session switch * fix(web-shell): clear stale right panel snapshots * fix(web-shell): repair scheduled task hint string --------- Co-authored-by: ytahdn <ytahdn@gmail.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
65393d0377
|
feat(daemon): record & query sub-session parentSessionId; drop isolated scheduled-task mode (#6676)
Some checks are pending
* feat(daemon): record sub-session parentSessionId; drop isolated scheduled-task mode
Two changes bundled from this session (recovered after an external
merge-test harness force-reset the worktree):
1. Remove the scheduled-task "isolated" run mode and its precondition
machinery. All tasks now run in their bound session (shared). The
create_sub_session tool is retained so users can request isolation
from a prompt. Drops runMode/condition/withheld across the persistence
type, scheduler, Session dispatch, REST route, Web Shell UI, i18n, and
the daemon/SDK contract, with tests updated.
2. Add an immutable parentSessionId to sub-sessions created via
create_sub_session, wired end to end: BridgeSpawnRequest -> SessionEntry
-> session summary, persisted into the child transcript (new
parent_session record + qwen/control/session/parent ext-method) and
rehydrated by listSessions on daemon restart, surfaced on
BridgeSessionSummary / DaemonSessionSummary.
* test(daemon): cover sub-session parentSessionId end to end
- chatRecordingService: recordParentSession writes a parent_session record
- sessionService: listSessions reads parentSessionId back from the transcript,
including the head-window fallback past the 64KB tail
- bridge: spawnOrAttach threads parentSessionId to the session summary and
dispatches the sessionParent ext-method (and not when there is no parent)
- create-sub-session launcher: passes callerSessionId as parentSessionId
* feat(daemon): add parentSessionId filter to the session list API
GET /workspace/:id/sessions accepts a `parentSessionId` query param that
returns only the sessions spawned by that parent (via create_sub_session).
The default path gathers the whole workspace (persisted + live) and filters
before paginating, so a page is never short of matches; results are sorted
newest-activity-first with an opaque activity cursor. Rejected (400) when
combined with view=organized. Threaded through the SDK
(DaemonSessionListPageOptions.parentSessionId / listWorkspaceSessions).
* test(daemon): cover the parentSessionId session-list filter
Response-builder cases: filters to a parent's children, empty when none,
paginates the filtered set completely (no dupes/gaps). Route cases: the
query param filters GET /workspace/:id/sessions, 400s with view=organized
(invalid_parent_session_filter) and on an empty value
(invalid_parent_session_id).
* fix(daemon): address review — fail closed on removed scheduled-task fields; scope & persist parentSessionId
Review follow-ups on the isolated-removal + parentSessionId work:
- scheduled-tasks REST: reject a POST/PATCH that still carries `runMode` or
`condition` (400 unsupported_field) instead of silently ignoring them, so a
stale SDK / cached Web Shell fails closed rather than getting a plain
unconditional task in place of the guarded one it asked for.
- cron scheduler: a legacy on-disk task that still carries a `condition`
precondition is skipped (left on disk), not run inline unconditionally —
a removed safety gate must not silently become "always run". One-time
operator breadcrumb points at remediation.
- bridge: await + verify the sessionParent persistence instead of
fire-and-forget, so a dropped transcript write is surfaced loudly rather
than silently degrading the parent link to live-only after a restart. The
child is not rolled back on failure.
- session list: the ?parentSessionId= page cursor now binds its
parentSessionId + archiveState and rejects a cursor replayed against a
different parent / archive scope, matching the organized cursor contract.
Tests added for all four.
* fix(daemon): address review round 2 — legacy tasks fail closed on all paths; surface parentSessionId persistence outcome
- cron: remove obsolete callback-level "guarded task" tests (headless +
interactive) — the guard moved to the scheduler load (fail-closed), so
those callbacks never receive a guarded job. Fixes the CI failures.
- scheduled-tasks REST: a legacy on-disk task that still carries a
`condition` is now failed closed on EVERY path, not just the scheduler
tick — GET reports it `enabled:false` with no `nextRunAt`, and POST /run
rejects it (409 task_legacy_unsupported). Shared `taskHasLegacyCondition`
helper (exported from core) is the single source of truth.
- bridge: the sub-session parent-lineage write is now timeout-bounded
(withTimeout, like the other init round-trips) and retried, and its
outcome is surfaced to the caller via BridgeSession.parentSessionPersisted
(threaded through the launcher → ext-method → spawner → tool) instead of
only stderr. create_sub_session warns the model when the link is
live-only. Requires persisted===true for success.
Tests added for all of the above.
* fix(daemon): reject enabling a legacy guarded task via PATCH
Follow-up: toView reports a legacy `condition` task as disabled, so the
only PATCH the Web Shell sends for it is the Enable toggle. Accepting that
(200) then reading back disabled again is an Enable control that can never
succeed with no error. PATCH `{enabled:true}` on such a task now returns
409 task_legacy_unsupported with the recreate remediation. Test added.
* fix(daemon): address review round 3 — warn on legacy isolated tasks; idempotent parent record; perf + dedup
Addressing ytahdn's review:
- cron: a bare `runMode: 'isolated'` task (no precondition) has no safety
gate, so it still fires — but it now accumulates history in its bound
session instead of a fresh per-run one. It runs, with a one-time
operator warning (the condition subset still fails closed). New shared
`taskHasLegacyRunMode` helper.
- chatRecordingService: `recordParentSession` is now idempotent (skips a
repeat append for the same immutable lineage) — matters because the
bridge write is now retried.
- sessionService: read `parentSessionId` from the records already loaded
for the listing instead of a second per-file open (the record is written
at creation, within the scan window) — removes the extra I/O on the hot
listing path.
- session-list: extract the persisted+live merge into one
`mergeLiveSessionSummary` helper (was duplicated across the default,
organized and by-parent paths).
Tests: legacy-runMode fires+warns-once, recordParentSession idempotency,
and the acpAgent sessionParent ext-method handler (valid / no-recording /
invalid-params).
* fix(daemon): address deep review — lineage on fork/restore/SDK; durable parent write; legacy tasks fail closed everywhere
GPT-5 review round + a two-pass reverse audit:
- forkSession no longer copies the source's `parent_session` record, so a
fork is a fresh top-level session, not a phantom child of the original.
- restore/resume now re-seed the persisted `parentSessionId` on the live
entry (both the REST handler and the ACP-HTTP transport — the audit caught
the ACP path being missed), so a restored sub-session still reports its
parent after a daemon restart.
- SDK: the ACP route mapping + `session/list` dispatcher now forward,
filter, and project `parentSessionId`; `WorkspaceDaemonClient` serializes
it too — the filter works over every SDK transport, not just the root HTTP
client.
- recordParentSession is awaited via the strict append path, so
`persisted: true` never claims a write that silently failed; it stays
idempotent so a retry can't double-append.
- bridge parent-write is raced against transport close and treats a timeout
as terminal (not a retry that would overlap an in-flight request), under
one deadline.
- legacy guarded tasks (removed isolated mode + precondition) now fail closed
on ALL consumers — cron_list reports them disabled, keepalive won't bind or
keep their sessions resident — not just the scheduler tick.
- a bare legacy `runMode: 'isolated'` task (no gate) still runs, with a
one-time behavior-change warning.
- restored `withheld` as a read-only compat field so pre-upgrade withheld
run-history is marked "skipped", not shown as a successful run.
Tests added/updated across all of the above.
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
|
||
|
|
38384ae7b9
|
feat(serve): Add cursor-paged transcript replay endpoint (#6525)
* feat(serve): Add cursor-paged transcript replay endpoint Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): Bound transcript replay indexing Limit transcript index builds to bounded snapshots and surface oversized transcript errors as 413 responses. Give transcript status calls a dedicated timeout and update the capabilities integration baseline. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Validate transcript cursors Sign transcript cursors so forged snapshot sizes cannot bypass the index cache, and keep hasMore tied to persisted record availability when replay conversion returns a partial page. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Lazy-init transcript cursor secret Avoid generating the transcript cursor HMAC key while importing the core barrel so unrelated tests with narrow crypto mocks can load core without requiring randomBytes. Keep the VS Code companion crypto mock partial so it only replaces the auth-token UUID behavior it asserts on. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): Address transcript replay review suggestions Mark bounded replay truncation frames as having a transcript endpoint, sanitize paged transcript replay conversion errors, and remove the core reader's incomplete pre-encoded cursor field so cursors are only emitted after replay continuation state is merged. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): Stabilize transcript replay pagination Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Avoid quadratic transcript line scanning Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Mark transcript history gaps Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Address transcript reader review comments Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): Address transcript replay review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): align transcript cursor preflight errors Return transcript snapshot conflicts for cursor pagination when the active JSONL can no longer be found during route preflight. Add route-level and integration coverage for full transcript paging, and document the boolean fullTranscriptAvailable SDK contract. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): Cover paged dangling tool call replay Add a HistoryReplayer.replayPage regression test that carries a dangling tool call through pendingToolCalls and finalizes it on a later page. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Bound transcript index cache bytes Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: Address transcript replay review follow-ups Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): Preserve pending tool calls on transcript replay errors Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6525) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix CI failure on PR #6525 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): warm transcript-replay tools leniently The read-only transcript-replay Config sets skipSkillManager, but Config.initialize() still runs toolRegistry.warmAll({ strict: true }), which constructs SkillTool whose constructor throws when no SkillManager exists. The throw escaped the replay try/catch and surfaced as JSON-RPC -32603, so GET /session/:id/transcript returned HTTP 500 for every persisted session. Add a lenientToolWarmup initialize option and set it for the replay Config so tools that cannot construct under the deliberately-skipped subsystems are logged and skipped instead of aborting initialize(). Replay only needs optional tool_call metadata and ToolCallEmitter already falls back to the recorded tool name, so buildable tools keep full title/kind. This supersedes the narrower excludeTools:[Skill] guard, which is removed. * fix(core): invalidate transcript index cache on in-place rewrites An in-place transcript rewrite that keeps the inode and byte length (e.g. rsync --inplace or a redaction pass) reused a stale cached index, because makeCacheKey() keyed only on path:dev:ino:size. readSegmentRecords then found each recorded offset parsing to a different uuid and dropped it, so GET /session/:id/transcript answered 200 with an empty events array instead of the documented 409. Include the file mtime in the index cache key so a fresh read after a same-size rewrite rebuilds the index, and raise SessionTranscriptSnapshotUnavailableError (-> 409) on a uuid mismatch or missing fragment instead of silently returning a short/empty transcript. Also make the qwen-serve docs explicit that at the default --channel-idle-timeout-ms 0 each page rebuilds the index (O(snapshotSize)). * codex: address PR review feedback (#6525) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * qwen: fix CI failure on PR #6525 The Run ESLint step failed on vitest/valid-expect in packages/acp-bridge/src/bridge.test.ts: the getSessionTranscriptPage timeout test stores expect(request).rejects.toBeInstanceOf(BridgeTimeoutError) and awaits it only after advancing the fake timers (a deliberate deferred await so the pending timeout rejection has a handler before it fires). Auto-fixing would add an inline await and deadlock the test, so scope-disable the rule on that assignment with a rationale. lint:ci and the affected test pass. * qwen: address PR review feedback (#6525) Withhold nextCursor on a mid-page transcript replay error. When collectHistoryReplayUpdatesPage catches a replayError partway through a page, records after the failed one are dropped and pendingToolCalls reflect partial state; still emitting nextCursor advanced the client past the dropped records and carried corrupted pendingToolCalls forward (phantom in-progress tool calls on later pages). Now nextCursor is withheld whenever replay.replayError is set — the page is already flagged partial + replayError, so the client stops instead of paginating with corrupted cursor state. Update the handler test to assert no cursor is issued on a replay error. * qwen: address PR review feedback (#6525) Log when parseTranscriptReplayState drops malformed pending tool calls from a replay cursor. Previously rawPending.filter(isPendingReplayToolCall) silently discarded entries that no longer matched the shape (e.g. a cursor from a newer daemon or corrupted in transit), turning a version-mismatch/corruption into a hard-to-diagnose 'tool never completed' artifact on later pages. Now emit a debug warning with the dropped/total counts; behavior is otherwise unchanged. * fix(serve): address transcript review feedback Dispose superseded replay configs, preserve structured resolution errors, sanitize multi-workspace failures, and expand transcript replay coverage across unit and real-daemon integration paths. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * qwen: address transcript review feedback (#6525) - [Critical] Map a missing transcript session to HTTP 404: the child throws a raw resourceNotFound (ENOENT without a cursor) that fell through sendBridgeError to 500. bridge.getSessionTranscriptPage now translates it to SessionNotFoundError, mirroring the load/resume path, with a bridge test. - Dedup the untrusted-session-owner 403 onto the shared sendUntrustedWorkspaceResponse so the response format/message stay consistent across session routes (route logging + context preserved). - Add coverage for parseTranscriptReplayState's non-object replay branch (cursor replay=garbage) -> empty pendingToolCalls + default cumulativeUsage. - Document that cursorHmacKeys are cached for the daemon lifetime (external key rotation requires a restart). * qwen: adopt transcript review suggestions (#6525) - Add a handler test that a mid-page replay error preserves already-emitted events (events>=1) alongside partial+replayError and withholds the cursor. - Add a two-call handler test for the cross-page cumulativeUsage round-trip: page 1 folds the bumped usage into the encoded cursor; page 2 decodes and propagates it into the replay context. - Log (not silently drop) a superseded structured error in the multi-workspace transcript resolution fallback. * qwen: clean up transcript test fixtures to fix no-AK CI flake (#6525) The transcript-paging integration suite wrote ~6 persisted chats/*.jsonl sessions into the daemon's project dir and never removed them. Because vitest runs a file's suites sequentially, those leftover sessions widened a pre-existing race in the later 'PATCH /session/:id/metadata > updates displayName' test (a freshly-created session can exist on disk but not yet appear in the listWorkspaceSessions page), making it fail deterministically in the no-AK smoke run. Add an afterAll to the transcript suite that removes the project chats/ dir, restoring a clean session list for subsequent suites. Verified: full no-AK suite now passes 43/43 across repeated runs. * qwen: harden transcript reader test timestamps + assert page fields (#6525) The record() helper derived the ISO timestamp seconds from text.length, producing invalid values (e.g. 00:00:013) once a record's text reached 10+ chars — harmless today only because no test asserted startTime. Replace it with a monotonic base+offset timestamp (always valid, strictly increasing). Also assert the previously-unchecked required SessionTranscriptRecordPage fields (sessionId, filePath, startTime, lastUpdated); the strict-ISO checks on startTime/lastUpdated guard against the timestamp-helper class of bug. --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
2523a36b52
|
feat(web-shell): workspace management sidebar with dynamic registration (daemon multi-workspace phase 4) (#6625)
* feat(web-shell): add workspace picker for new sessions (issue #6378 phase 4) Multi-workspace daemons now show a new-session workspace picker in the sidebar (default primary, untrusted disabled); the chosen workspace cwd is sent on POST /session so the session spawns in that workspace. daemon-react-sdk createSession gains an optional per-call workspaceCwd override covering both the detached and active-session paths; omitting it preserves the previous primary behavior. * feat(web-shell): workspace management with dynamic registration Replace the new-session workspace picker with a full workspace management sidebar. Registered workspaces render as a parallel, collapsible list (folder icon per workspace), each with its own sessions nested underneath, and a "+" entry registers an existing directory as a new workspace at runtime with no daemon restart. Backend: WorkspaceRegistry becomes mutable (add()/onChange()); a new POST /workspaces route validates the directory (exists, not a duplicate, not nested) and registers it; run-qwen-serve exposes a runtime factory that builds a complete workspace runtime (bridge, fs factory, channel factory, workspace service) on demand. The SDK DaemonClient and daemon-react-sdk gain addWorkspace(). * fix(web-shell): show newly registered workspace without a reload Registering a workspace via the sidebar "+" left the list unchanged until a full page reload. handleAddWorkspace called workspace.getCapabilities(), which returns a cached promise and only feeds setCapabilities from the mount effect, so the refresh was a no-op. Add DaemonWorkspaceProvider.refreshCapabilities(): it bypasses the promise cache, issues a fresh /capabilities fetch, and pushes the result into state so consumers re-render. handleAddWorkspace now awaits it (best-effort, so a refresh failure never masks a successful registration). * fix(web-shell): address review feedback for workspace management - registry: list() returns a frozen snapshot so callers can't mutate the internal runtimes array (restores the push()-throws invariant) - POST /workspaces: reject relative paths on the raw input, canonicalize via realpath so symlink aliases can't bypass the duplicate/nesting checks, and serialize concurrent registrations to close a TOCTOU race that leaked bridge/channel infrastructure - sidebar: restore a compact single-workspace project header (name, search toggle, collapse) so single-workspace users keep those affordances and searchOpen/projectExpanded are no longer dead - daemon session: include the target workspace in the create-session failure message - tests: rework WebShellSidebar tests for the WorkspaceSection UI (add the useWorkspace mock, query workspace buttons, cover primary->undefined), use the canonical DaemonWorkspaceCapability type, and add a createSession workspaceCwd forwarding test * fix(cli): harden dynamic workspace registration per review - POST /workspaces: bound cwd by MAX_WORKSPACE_PATH_LENGTH before any filesystem work, and return a generic 500 (log the full error to stderr) so responses can't leak internal filesystem paths - createDynamicWorkspaceRuntime: log a stderr warning when a workspace's settings can't be read, matching the startup secondary-workspace path * qwen: address PR review feedback (#6625) Dynamic workspace reloadDaemonEnv now mirrors the startup secondary path: after reloadEnvironment() it rebuilds the runtime env via buildRuntimeEnvironment(), calls wsEnv.replace(), and updates the env metadata (envFileReadFailed / envFileReadFailures / overlayKeys / envFilePaths). Without this, .env changes on a dynamically registered workspace never propagated to that workspace's spawned child processes. * qwen: address PR review feedback (#6625) Harden POST /workspaces and the workspace registry per review: - canonicalize with realpathSync.native (matches startup) so the same physical dir on a case-insensitive FS can't register twice - nesting guard now also checks in-flight registrations, closing a concurrent parent/child registration race - error responses no longer echo resolved/other-workspace paths - registry add() isolates onChange listener throws so a bad listener can't abort a caller after the workspace is already committed * qwen: address PR review feedback (#6625) - POST /workspaces: cap total registered workspaces (startup + dynamic) to guard against unbounded registration exhausting resources - createDynamicWorkspaceRuntime: register shutdown-cleanup arrays only after the runtime is fully built, so a throw during workspace-service construction can't orphan the bridge/channel - web-shell App: reset selectedWorkspaceCwd after session creation so the workspace picker is one-shot (next new chat defaults to primary) * qwen: address human review suggestions (batch 1) - WorkspaceSection: add console.warn on session-poll failure (was silent) - WorkspaceSection: add aria-expanded for screen readers - AddWorkspaceDialog: associate label/input (htmlFor/id), i18n the absolute-path error, accept Windows drive-letter paths - i18n: remove unused workspaceUntrustedHint key, add addWorkspaceAbsError * qwen: address human review suggestions (batch 2) - Remove dead CSS (.workspacePickerSelect, .workspaceItem* classes from the old select-based picker, replaced by WorkspaceSection) - Add title tooltip to single-workspace project name (shows full path) - WorkspaceSection: sync expanded state on workspace.primary change * qwen: address human review suggestions (batch 3) - DaemonWorkspaceProvider: refreshCapabilities now clears error on success and sets error+status on failure (was incomplete vs mount) - Remove unused onChange/WorkspaceRegistryEvent from workspace registry per simplicity-first (no consumer exists; defers API surface until a real subscriber like SSE push is needed) * qwen: add workspace-management route test coverage Tests cover: 501 (no factory), 400 (missing/empty/relative/long/ nonexistent cwd), 409 (duplicate canonical path), 201 (success), and verifying error messages are generic (no path leak). * qwen: fix CI build failure — add explicit types in route test The CLI's tsconfig includes test files in tsc --build, so all noImplicitAny violations in tests cause build failures. Add explicit type annotations to mock parameters. * qwen: add type/title to single-workspace add-button --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
f06e932260
|
fix(web-shell): polyfill Range layout APIs in tests (#6677)
* fix(web-shell): polyfill range layout in tests * test(webui): stabilize heartbeat prompt cleanup |
||
|
|
dd62c3a8e5
|
feat(scheduled-tasks): gate an isolated run behind a precondition (#6619)
* feat(scheduled-tasks): gate an isolated run behind a precondition
An isolated scheduled task now takes an optional `condition` alongside its
prompt. On every fire the task's bound session evaluates the condition as an
ordinary cron turn, and only dispatches the prompt into a fresh sub-session
when that turn's verdict is YES.
The check deliberately runs in the bound session rather than a throwaway
sub-session:
- It has exactly the semantics of a `shared` fire — same tools, same
workspace approval mode — so it introduces no new permission surface.
- The bound session of an isolated task is otherwise empty, so its
transcript becomes the task's decision log: the record of why a fire did
or did not happen.
- No session is minted for a run that never occurs.
Everything that is not a YES skips the fire: NO, an unparseable answer, a
tool-loop error, a cancelled or timed-out turn. A precondition exists to
withhold an unattended run, so an ambiguous answer must withhold it too.
A `missed` (late-delivered) fire is judged the same way and then keeps its
existing in-session path — a precondition changes whether a fire runs, never
how. The Web Shell's "Run now" evaluates the condition before relaying the
dispatch through the model, so a manual run reproduces a scheduled one and
doubles as the way to test that a condition is written correctly.
The field is isolated-only. `POST`/`PATCH /scheduled-tasks` reject a condition
on a shared task, judging the combined post-patch state so a condition can be
stranded from neither side; the check is gated on the request actually
touching `condition` or `runMode`, so a hand-edited stranded task stays
editable. `isValidTask` requires a non-empty string, since the fire path gates
on truthiness and an empty condition would silently un-guard the task.
* fix(scheduled-tasks): harden the precondition against four review findings
Never judge a `missed` fire. That job is the scheduler's synthetic carrier:
one batched notification covering every one-shot missed in this load, built
from a spread of the first task, whose prompt is a notice ("these were missed
— ask the user before running them") rather than any task's command. Gating it
on the first task's precondition let a `NO` silently suppress the notice for
its siblings, which `removeMissedFromDisk` has already deleted. The scheduler
now strips per-task guard state from the carrier, and the session refuses to
judge a missed fire — the same contract enforced from both ends.
Distinguish a truncated turn from a clean one. A permission cancel or a
detected tool loop returns mid-tool-loop without aborting the turn's signal or
recording an error, so it reached `onComplete` as `'ok'`. A model that emits
`DECISION: YES` as text in the same streaming round as its tool call would then
release the fire on a verdict it never got to revise. Add an `'incomplete'`
outcome, set at both early returns.
Require the verdict to be the whole line. `\b` after the verdict accepted
`DECISION: YES, but I could not verify it` as a YES. The prompt asks for a line
that is exactly one of the two; a hedged answer is not a decision, and a
precondition must fail closed on an answer it cannot trust. Closing markdown
and terminal punctuation are still tolerated.
Require a bound session for a condition. The check is evaluated in the task's
own session — that is what makes its transcript a decision log. A task with no
`sessionId` (tool-created, or created with no bridge to bind one) fires through
the shared per-project durable owner, so its check would be injected into
whichever session holds that lock. Both create and update now reject a
condition on such a task instead of quietly relocating the check.
Also log the decision point: a non-ok outcome reaches stderr, since the
scheduler has already booked the run and `debugLogger` writes nothing unless a
debug log session is active.
* fix(scheduled-tasks): close four more precondition gaps from review
Read the verdict off the final non-empty line, not from anywhere in the text.
The prompt asks the model to *end* its reply with the verdict, so
`DECISION: YES\n\nBut I could not verify it` is not a decision — scanning the
whole reply took the conclusion off the wrong line and released the fire.
Mark a cut-short tool loop at its choke point. `loopDetected` was flagged, but
its sibling `repeatedDuplicateProviderToolCall` takes the quiet exit: it makes
`#buildNextMessageAfterToolRun` return null, ending the turn with no error and
no abort. A model that streamed `DECISION: YES` in that same round then
released the fire on an investigation it never finished. Both cases (and any
future one) are now marked where the follow-up message comes back null, rather
than by enumerating flags — enumerating them is how the sibling was missed.
Fail closed in the two consumers that cannot evaluate a precondition. The
headless and TUI `onFire` callbacks read only `prompt`/`cronExpr`/`missed`, so
a guarded task fired there with its guard ignored — the exact outcome the
precondition exists to prevent. Both now skip such a fire; only the ACP/daemon
session, which owns the sub-session dispatch the verdict gates, runs it.
Distinguish a withheld fire in the run history. The scheduler books the run the
moment it fires, before any verdict exists, so a task that deliberately did
nothing reported "ran at 02:00". `CronTaskRun.withheld` is stamped afterwards
by the evaluating session, addressed by the fire's own minute (the scheduler
writes `runs[].at` from the very `lastFiredAt` it hands to `onFire`), and the
Web Shell tags the entry. Best-effort and never awaited: losing a cosmetic
marker must not affect a fire that has already been decided.
* feat(scheduled-tasks): make the precondition readable, and translate it
The bound session of a guarded task is the feature's decision log, but it read
like a debug dump: every fire echoed the whole instruction wrapper the model
receives — five paragraphs of "end your reply with a final line that is exactly
one of…" — and nothing in it was translatable.
Echo a compact label instead. `CronQueueItem` gains an optional `echoText`: the
text the client shows when the text sent to the model is not fit to read. A
precondition turn now shows "⏰ Precondition check" and the user's own condition,
whitespace-collapsed and capped at 280 characters (surrogate-safe). The model
still receives the full wrapper.
Say what the check decided. The model's answer explains its reasoning but cannot
state the consequence, so the scheduler adds one line: the run was skipped
(precondition not met, or the check was cancelled / interrupted / failed), or it
is running — with a `qwen-session://` link to the sub-session that is doing the
work. Without that link the bound session of an isolated task shows nothing at
all for a fire that DID run: the work happens in a sibling the user cannot
reach from here.
The status line opens with a blank line. It is an `agent_message_chunk`, which
the client appends to the assistant message already on screen, and that message
ends on the verdict with no trailing newline — without the break the transcript
renders `DECISION: NO⏰ Precondition not met…`. A screenshot caught that; the
assertions did not, so there is now a test for it.
All seven strings go through `t()` and are translated in en/zh/zh-TW (the three
locales `check-i18n` holds to strict key parity). Session.ts had no i18n import
before this; `t()` is initialized on the ACP path by `gemini.tsx`.
Not addressed: the ACP cron path persists no user record at all, so the echo and
the status lines are live-only and a reload shows the model's answers with no
question above them. That is pre-existing — `client.ts` records a cron prompt via
`recordCronPrompt(message, displayText)` only on the core send path, which the
ACP session does not use.
* fix(web-shell): make qwen-session:// links actually clickable
`MarkdownLink` has an interception branch for `qwen-session://<id>` that renders
a button and dispatches `qwen:open-session` so the app shell can navigate. It
has never run.
react-markdown sanitizes every href through `defaultUrlTransform`, which allows
only `http(s)`, `irc(s)`, `mailto` and `xmpp` and rewrites everything else to
`''`. So the scheme was stripped before `components.a` was called: the branch
saw an empty href, fell through, and rendered a plain anchor with no href.
Clicking it did nothing.
Add a `urlTransform` that passes `qwen-session://` through and defers every
other url to the default sanitizer. Letting the scheme through is safe — the
interception branch never puts it in the DOM, it renders `href="#"` and
dispatches the id as an event — and the per-component `isSafeHref` /
`isSafeImageSrc` guards are unchanged.
Dead since #6535 (
|
||
|
|
ac2f371c44
|
feat(scheduled-tasks): add isolated run mode via create_sub_session tool (#6535)
* feat(scheduled-tasks): add isolated run mode via create_sub_session tool
Introduce a new `create_sub_session` tool (daemon-only) that spawns a
fresh top-level sub-session with its own clean context and transcript.
Wire it into the cron scheduler as an `isolated` run mode so each
scheduled fire dispatches its prompt into a fresh sub-session instead
of accumulating in one shared transcript.
- Add `create_sub_session` tool with `first-turn` and `sent` completion modes
- Add `SubSessionLauncher` in cli/serve with concurrency cap, timeout, and truncation
- Extend ACP bridge `extMethod` dispatch for child→daemon sub-session requests
- Add `runMode` field (`shared`|`isolated`) to DurableCronTask, CronJob, and API types
- Add run-mode radio picker to ScheduledTasksDialog UI
- Fix AuthMessage hardcoded placeholder to use i18n key
* fix(scheduled-tasks): dispatch isolated fires daemon-side, not via the model
An `isolated` fire was relayed through the model: the fired prompt was
wrapped with an instruction to call `create_sub_session`. That tool's
default permission is `'ask'`, so under `ApprovalMode.DEFAULT` an
unattended fire reached `client.requestPermission`, found no SSE
subscriber, and was cancelled by the daemon's 5-minute permission
timeout. The task never ran, and the cancel was booked as a successful
run — the headline use case of a scheduled task was broken.
Route isolated fires straight to the daemon instead: the cron `onFire`
handler in `Session` calls the sub-session spawner directly, with no
model relay and no tool-permission gate. The prompt was already approved
when the task was created; laundering it back through the model only
re-opened that gate. `create_sub_session` keeps `'ask'` for
model-initiated calls, and the attended "Run now" button keeps its relay
(a user is present to answer the prompt).
Also fix orphan-session cleanup in the launcher. `closeSession` was
guarded only by `.catch()`, which covers an async rejection but not a
synchronous throw; because the call sits inside the launcher's own
`catch (err)` block, a sync throw escaped and replaced the real launch
error. Guard both shapes.
Tests:
- Cover isolated routing: dispatch, in-session fallback with no spawner,
missed one-shot, dispatch failure (dropped, never run inline), and
shared mode.
- Cover the orphan close, including a `closeSession` that throws.
- Replace the sent-mode concurrency test, which only asserted the slot
was eventually released (moving the release to the drain's *start*
kept it green) with one that asserts the slot is HELD while the drain
runs, plus one that asserts it is released at `turn_complete`.
* fix(scheduled-tasks): honor the caller's AbortSignal and harden the spawn boundary
Four findings from review, all in the model-initiated `create_sub_session`
path (the scheduled `isolated` dispatch reaches none of them).
`execute()` took no parameters, so it silently dropped the parent turn's
`AbortSignal`. `Session.ts` awaits `invocation.execute(signal)` without
racing the abort itself, so cancelling a turn with a `first-turn`
sub-session in flight pinned the caller's tool loop until the daemon's
5-minute ceiling. Accept the signal and return as soon as it fires.
The sub-session is deliberately NOT cancelled and deliberately KEEPS its
concurrency slot: `sendPrompt` has no abort seam, so the sub-session runs
on. Releasing its slot on cancel — as the review suggested — would let the
caller over-admit against sub-sessions that are still consuming a bridge
session and model quota.
`handleCreateSubSession` trusted the child-supplied `callerSessionId`
verbatim, and that id keys the launcher's per-caller concurrency bucket: a
fabricated id starts a fresh bucket at zero (cap evasion) and a victim's id
burns their slots (DoS). Validate it with the connection's existing
`ownsSession` seam.
Every daemon session wires a spawner, sub-sessions included, and each gets
its own cap-sized bucket — so one prompt could fan out 5ⁿ sub-sessions until
`maxSessions` ran dry. Gate nesting at one level: the launcher remembers the
sessions it spawned and refuses to spawn from them. With `callerSessionId`
now authenticated, the gate cannot be sidestepped.
Cap the prompt at 100,000 chars (matching the scheduled-task REST route) and
the display name at 200, both at the bridge trust boundary and, for the
prompt, in the tool's own validation so the model gets an actionable error.
Not changed: `create_sub_session` stays in `PermissionManager.CORE_TOOLS`.
Membership there SUBJECTS a tool to the `coreTools` allowlist; it does not
exempt it. Removing it — as the review suggested — is what would let the
tool bypass a user's allowlist, the way `agent` and `send_message` do today.
* fix(core): do not spawn a sub-session for an already-cancelled turn
`raceCancellation(spawner({…}), signal)` evaluated the spawner as an
argument, so the spawn started before the abort was ever checked. A turn
cancelled before `execute()` ran still created a sub-session on the daemon
— and it kept a concurrency slot — while the tool reported itself
cancelled.
Take a thunk instead, so the pre-abort check happens before any daemon work
is started. Track whether the spawn actually began, and say so: "cancelled
before it started, no sub-session was created" is a different fact from
"a sub-session may already have been created and is not cancelled".
Regression test asserts the spawner is never called for a pre-aborted
signal; it fails against the eager-argument form.
* fix(serve): require callerSessionId and stop misreporting an early stream close
Two findings from review.
`awaitFirstTurn`'s `'incomplete'` stopReason was unreachable. The cleanup
`finally` calls `ac.abort()` unconditionally to tear the subscription down,
so by the time the stopReason ternary read `ac.signal.aborted` it was always
true. An event stream that closed before the turn finished (bridge teardown,
WS drop) was reported as a 5-minute wall-clock `'timeout'` — indistinguishable
from a real one. Track the timer firing in its own flag.
`callerSessionId` was validated only when present. Omitting it handed the
launcher `undefined`, which minted an `anon:<uuid>` bucket — a fresh
concurrency bucket per call, so no cap — and skipped the depth-1 nesting gate
(`info.callerSessionId !== undefined && …`). Authenticating the id closed
forgery but not omission. It is now required at the bridge boundary, and
required in `CreateSubSessionInfo`, so the launcher's anonymous fallback and
the gate's presence check are both gone. Every real caller has a session id —
the tool only ever runs inside a session's turn.
* fix(serve): surface dropped fires and drain timeouts; bound sub-sessions per workspace
Three findings from review.
A dropped `isolated` scheduled fire left no trace. `debugLogger.warn` writes
nothing unless a debug log session is active, and the scheduler persists the
fire as a run before dispatch — so a nightly task could fail forever while its
history claimed it ran. It now also writes to stderr, which the daemon forwards
from the child.
A sent-mode drain that hit its 30-minute ceiling was equally silent: the catch
saw `drainAc.signal.aborted` and skipped logging, the `finally` freed the
concurrency slot, and the sub-session — which the abort does not cancel — kept
burning a bridge session and model quota. The timer now records its own firing
(the controller cannot: `finally` aborts it on every exit path) and the timeout
is written to stderr. The drain ceiling is injectable for tests, mirroring
`firstTurnTimeoutMs`.
The per-caller concurrency cap trusts `callerSessionId`, and the bridge can only
authenticate that id as "a session on this channel". Every session of a
workspace shares one child process, so nothing at the transport can prove which
of them issued the call — and a per-session secret would be readable by the
whole process anyway. Rather than pretend otherwise, add a workspace-wide
ceiling on concurrent sub-sessions that holds no matter which bucket a launch is
charged to.
|
||
|
|
e64010c116
|
Fix workspace skills for disabled extensions and ACP preheat (#6534)
* fix(cli): keep workspace skills in sync with extensions * fix(cli): address workspace skills review feedback * test(cli): cover synthesized inactive extension skills * fix(cli): address workspace skills review issues * fix(cli): address workspace skills review followups --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
1420566620
|
feat(serve): Bound replay snapshot history (#6482)
* feat(serve): Bound replay snapshot history Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6482) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review suggestions (#6482) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(acp-bridge): fix replay truncation assertion access Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): keep replay cap validation out of fast path runtime Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(acp-bridge): reset replay window on bulk seed Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6482) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix CI failure on PR #6482 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk): expose bounded replay status types 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-bot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
40340ef505
|
fix(serve): classify interrupted model stream errors (#6422)
* fix(serve): classify interrupted model streams * fix(serve): address interrupted stream review * test(webui): cover legacy terminated turn error fallback * fix(web-shell): preserve error message data shape * test(daemon): cover turn error fallback boundaries * fix(web-shell): preserve classified error data --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
001d20ff26
|
feat(scheduled-tasks): run each task in its own dedicated, named session (#6389)
* feat(scheduled-tasks): run each task in its own dedicated, named session Scheduled tasks created through the Web Shell management page were never firing in the daemon-only case: the durable-cron tick runs inside an active agent session, and the Web Shell creates a session only lazily on the first prompt, so a task created on the management page (with no chat open) had nothing ticking it. This binds every management-page task to a dedicated session, minted at create time and named "⏰ <task>". The task fires ONLY inside that session — its transcript is the task's run history — instead of via the shared per-project durable owner. A daemon-side keepalive heartbeats those sessions so the idle reaper doesn't stop them, and a boot-time rehydration reloads them after a restart. Archiving, deleting, or unarchiving the session disables, removes, or re-enables the bound task (covered on both the REST and ACP surfaces). Also adds task editing, a live next-run countdown, run history, a one-per-row card layout, and a "run now" that executes in the task's bound session and updates the last-run time. All resident-session management is opt-in and enabled only by the real daemon (runQwenServe), so createServeApp embeds/tests are unaffected. * fix(scheduled-tasks): address code review on per-session task feature Review fixes for #6389: - Distinguish archive-disabled from user-disabled tasks: disableTasksForSessions now marks disabledByArchive; enableTasksForSessions only re-enables tasks carrying that flag, so a task the user deliberately disabled stays disabled across an archive/unarchive cycle. [Critical] - Rehydrate task sessions concurrently with a per-session 30s timeout so one hung loadSession can't stall the boot sweep or leave healthy tasks dormant. [Critical] - Await runScheduledTask + reload before executing the prompt in handleRunNow, so a record failure surfaces and the card's "last run" reflects the trigger. [Critical] - Log keepalive/rehydrate read + heartbeat failures at debug instead of swallowing them silently, so a persistently-failing keepalive is diagnosable. [Critical] - Add integration tests: deleteDaemonSessions -> removeTasksForSessions and unarchiveDaemonSessions -> enableTasksForSessions (guard the coupling). [Critical] - DELETE route: single atomic updateCronTasks that captures the bound session and removes the task in one cycle, closing the read-then-remove TOCTOU. - Stop the keepalive timer during shutdown (matters for embedders that don't process.exit) so it can't fire against a disposed bridge. - Deduplicate DEFAULT_BUILDER: export it once from scheduledTasksSchedule and drop the dialog's copy so the create form and cron-reversal can't drift. - Reject empty-string sessionId in isValidTask: a bound task with "" would silently run unbound under the scheduler's truthy guard. * fix(scheduled-tasks): isolate task sessions, fix catch-up/jitter/revive Second review round (#6389): - Force `sessionScope: 'thread'` when minting a task's session. The daemon's default scope is 'single', which attaches to (reuses) the shared workspace session — so a second task, or a task alongside an open chat, would bind to the same session, rename it, land runs in the wrong transcript, and close it on delete. Thread scope guarantees each task an isolated session. [Critical] - Re-seat a recurring task's schedule anchor to now when a PATCH changes its cron (or flips one-shot→recurring), not just on re-enable. A bound task's catch-up runs on every file-watch reload, so a bare cron edit to an expression with an already-past slot would fire immediately on save. [Critical] - Revive a non-resident bound session from the keepalive when its heartbeat fails (reaper let it go while disabled/archived, now re-enabled). Covers the unarchive and PATCH false→true paths uniformly and retries each interval, so a re-enabled task actually resumes instead of showing a live countdown that never fires. Best-effort, timeout-bounded, non-blocking. [Critical] - Report `nextRunAt` using the scheduler's jittered fire time (`nextDurableFireMs`) instead of the bare cron boundary, so the UI countdown lines up with the real fire (the tick offsets each fire by up to the jitter window) rather than expiring early and advancing prematurely. All four are mutation-verified. The cross-daemon double-fire on bound tasks (same session live in two schedulers) is a separate, architecturally-invasive fix (claim-then-fire on the durable file) tracked as a follow-up. * fix(scheduled-tasks): sync bound session name on task rename Create names a task's session after the task (`⏰ <name>`), but a later PATCH that renamed the task (or edited the prompt of an unnamed task) left the session's display name stale. The PATCH route now re-applies `updateSessionMetadata` with the task's effective label whenever that label actually changes — a bare cron/enabled edit does not touch the session. Best-effort: a metadata failure doesn't fail the committed schedule change. Mutation-verified. * fix(scheduled-tasks): mirror run sessionId on client type; clarify server wiring Review follow-up (#6389, qqqys): - [Medium] `DaemonScheduledTaskRun` now mirrors the daemon's `CronTaskRun` `sessionId?: string`, so run-attribution the wire already sends isn't silently dropped by the client type (not surfaced in the UI yet; passthrough cast means no mapping change needed). - [Nit] Comment the `app.locals.stopScheduledTaskKeepalive` set site, noting it follows the same convention as `fsFactory`/`boundWorkspace`/`acpHandle` and is read by the run-qwen-serve shutdown path (kept the convention rather than diverge to a one-off return value / declaration merge). - [Nit] Comment the outer `.catch(() => {})` on rehydrate as intentional defense-in-depth (the function already handles read + per-session failures). * fix(scheduled-tasks): couple archive/enable + record manual run only on enqueue Two [Critical] review items (#6389, gpt-5-codex): - PATCH re-enable coupling: reject `enabled: true` on a task disabled BY archiving its session (`disabledByArchive`) with 409 `task_session_archived`. Re-enabling it here would show an enabled task with a countdown while its bound session stays archived and can never fire — the caller must unarchive the session (which clears the marker and reloads it). A user-disabled task (no marker) and non-enable edits are unaffected. - Manual "run now" ordering: record the run only AFTER the prompt is enqueued, not before. `runTaskManually` now returns a promise that resolves on enqueue and rejects if the bound session can't be opened (archived/deleted), is superseded, or times out; the dialog awaits it before writing /scheduled-tasks/:id/run, so a failed session switch no longer leaves a phantom run in history. Runs are serialized (one pending at a time, button disabled) so two quick clicks can't drop a prompt on the single bound-run latch. Added coverage for failed session load and double-click; all new tests mutation-verified. * fix(scheduled-tasks): close dormancy/orphan/overflow gaps from review Five items from GPT-5 /review (#6389): - [Critical] Bind tasks to sessions only when resident management is on: createServeApp now passes the bridge to the scheduled-task routes only when `manageScheduledTaskSessions` is set. Embedders that leave it off get UNBOUND tasks (shared-owner firing) instead of bound tasks nothing keeps resident or reloads (which would silently go dormant). - [Critical] Keep the keepalive/revive loop running whenever task sessions are managed, not only when a reaper is active — archiving closes a task session, so a re-enabled one still needs reviving with the reaper disabled. Size the interval under the reaper window (≤ half of it) so a small idle timeout can't let a session be reaped before its first heartbeat. - [Critical] Record a manual run only after the prompt is admitted: the bound run latch now resolves only if `sendPrompt` admitted the prompt and rejects on cancellation (e.g. onSubmitBefore) / failure, so a cancelled Run now no longer advances lastFiredAt or appends history. - [Critical] Clamp the dialog's reload timer to the 32-bit setTimeout ceiling (~24.8 days) so a months-away schedule can't overflow and spin a reload loop. - [Suggestion] Pre-check the task cap before spawning a session, so an over-cap create never mints an orphan task session it must roll back. New tests (route unbound-when-no-bridge, cap-no-spawn, computeKeepaliveIntervalMs bounds, far-future timer clamp) mutation-verified; full server suite green. * fix(scheduled-tasks): guard catch-up double-fire, run-now hang, /run + cron edits Four items from GPT-5 /review (#6389): - [Critical] Bound-task catch-up could double-fire: detection ran on every file-watch reload and read the stale on-disk lastFiredAt, so a reload racing the async catch-up persist (a foreign write to the tasks file) re-detected and re-fired the same overdue slot. Track ids whose catch-up was DELIVERED but not yet persisted (`deliveredCatchUp`) and skip re-detecting them until the write lands; a merely-buffered-then-dropped catch-up isn't tracked, so it still re-detects from disk (recovery preserved). - [Critical] "Run now" hung the full 30s switch timeout when the bound session was ALREADY the current, loaded one (no dep change → the consuming effect never re-ran). Fire the enqueue directly after loadSidebarSession resolves as well as from the effect; whoever runs first nulls the latch, so it runs once. - [Critical] POST /run recorded a run with no enabled/disabledByArchive guard, unlike PATCH — a direct API caller could write a phantom "ran" record onto a paused/archived task. Return 409 task_disabled for a disabled task. - [Suggestion] Anchor re-seat on cron edit compared the raw string, so a cosmetic change (`0 9 * * *` → `00 9 * * *`) dropped a pending catch-up. Compare the canonical (parsed) schedule instead. (The setTimeout-overflow and keepalive-floor reports were already fixed in 2a12cba.) New tests for the first three + the cosmetic-cron case are mutation-verified; full core scheduler + route suites green. * fix(scheduled-tasks): block disabled-task run in UI; record manual run at admission Two [Critical] review follow-ups (#6389): - A disabled task could still EXECUTE from the Web Shell: the Run button was only gated on `runningTaskId`, so clicking it enqueued the prompt and the server's `/run` `task_disabled` guard merely refused the later history write — a real, unrecorded run. Gate `handleRunNow` and disable the button on `!task.enabled` too, so a disabled task's prompt is never enqueued. - Manual run recorded only after the whole turn: the bound-run latch resolved via sendPrompt, which completes through waitForAcceptedPromptCompletion, so a long/permission-blocked run or a closed tab could execute without ever being recorded. Add an `onAdmitted` callback to sendPrompt (fired when the daemon accepts the prompt, before the turn) and resolve the manual-run latch at admission instead — cancellation before admission still rejects. New dialog test (disabled task → no enqueue) mutation-verified; webui/web-shell typecheck + existing session-action tests green. * fix(scheduled-tasks): guard tick double-fire, cap rehydration, harden lifecycle writes Review follow-ups (#6389): - Extend the fire-persist re-detection guard to ON-TIME tick fires, not just catch-ups (renamed deliveredCatchUp → firePersistPending): a bound task fired by the tick advances lastFiredAt asynchronously, so a reload racing that write (bound detection runs every reload) could re-detect the slot and double-fire. The tick persist now adds its ids to the guard and clears them when the write lands, symmetric to the catch-up persist. - Bound boot-rehydration concurrency (batches of 4): each loadSession forks a child, so loading up to 50 at once spiked the host and risked spawn failures that strand tasks. The keepalive revive path was already sequential. - Archive disable failure is now logged (was fully swallowed) so a broken archive→pause coupling — where the keepalive would revive the just-archived session — is diagnosable. - Unarchive re-enable failure is surfaced in the result `errors` and logged, and enableTasksForSessions also runs for already-active sessions — so a task left stranded ({enabled:false, disabledByArchive:true}) by a prior failed enable is recoverable by re-unarchiving, instead of being permanently stuck. - Create rollback now removes the persisted session (close + removeSession), so the loser of a concurrent create at the cap boundary (passes the pre-check, loses the authoritative write) doesn't leave an orphan named session. New tests (tick-fire guard, bounded rehydration, already-active recovery) mutation-verified; full core scheduler + serve suites green. * fix(scheduled-tasks): one-shot run/edit correctness; tick persist non-regression Review follow-ups (#6389, ci-bot): - [Critical] Manual /run on a ONE-SHOT task now removes it from the store. Its slot is still in the future, so stamping lastFiredAt=now didn't stop the scheduler firing it again at its original time — a double run. A one-shot's manual run IS its single fire, so the task is spent. - [Critical] PATCH recurring:false now re-seats the one-shot's createdAt anchor. The old (long-past) anchor made the scheduler read it as a MISSED one-shot and fire + permanently delete it. Re-seating createdAt points its next fire at the upcoming occurrence. Also covers a cron edit on an existing one-shot. - [Suggestion] The tick persist no longer regresses lastFiredAt: it skips the write when the on-disk stamp is already >= the tick slot (a concurrent manual /run or catch-up may have stamped newer), mirroring the catch-up persist guard. - [Suggestion] Added the missing create-rollback test: a post-spawn commit failure closes AND removes the minted session (no orphan). New tests (one-shot run removal, recurring→one-shot re-seat, rollback teardown) mutation-verified; core scheduler + route suites green. * fix(scheduled-tasks): ref-count fire guard, real rehydration cap, authoritative run check Four [Critical] review follow-ups (#6389): - Ref-count firePersistPending (was a boolean Set): the same task can have two lastFiredAt persists in flight (fired again before the first write landed); clearing on the first settle dropped the guard while the second was still pending, re-opening the double-fire window. The count holds it until the last persist settles. - Rehydration concurrency is now enforced on the REAL loads: loadSession isn't abortable, so a timed-out load kept forking in the background while the next batch started. A bounded worker pool holds each slot until the underlying load actually settles, so in-flight child spawns never exceed the cap. - Unarchive recovery reports failures for the full resume set: it enables both unarchived AND already-active sessions but only logged/returned errors for unarchived, so a failed already-active recovery surfaced errors:[] and left a task stranded. Deduped one list used for the call, log, and errors. - Manual "run now" re-checks server-authoritative state before enqueuing: the dialog snapshot can be stale (another tab/API disabled/deleted the task), so it would execute the prompt and only the /run record would 409. It now refreshes, bails if gone/disabled, and enqueues the FRESH prompt/session. New tests (ref-count, slot-held-past-timeout, stale-disabled re-check) mutation-verified; core scheduler + serve + dialog suites green. * fix(scheduled-tasks): catch-up non-regression, disabled-edit re-seat, run/timer/keepalive hardening Review follow-ups (#6389): - [Critical] Catch-up persist no longer regresses lastFiredAt: use `>=` like the tick persist, so a newer stamp (a cross-process manual /run) landing while the catch-up write is in flight isn't overwritten back to the older minute. - [Critical] The PATCH anchor re-seat now runs for schedule edits even while the task is disabled — editing a disabled one-shot's cron then re-enabling it (two separate requests) no longer leaves a stale anchor that fires + deletes it. - [High] Manual "run now" of a bound ONE-SHOT consumes it server-side (/run, which deletes) BEFORE enqueuing, so a record failure leaves a recoverable "recorded but never ran" instead of a silent double execution at its slot. - [Medium] The dialog reload timer backs off a stuck past-due nextRunAt (fast reloads to catch a just-fired advance, then a slow lane) instead of spinning a 1 Hz GET loop. - [Medium] The manual-run latch bounds the admission phase with a timeout, so a send that wedges before admission degrades to a visible "run failed" instead of freezing the run controls. - [Suggestion] Keepalive: an in-flight guard skips a tick while the previous pass runs (no duplicate concurrent loadSession spawns), and per-session exponential backoff stops retrying a permanently-gone session every interval. New tests mutation-verified. Two deeper items (a task session winning the durable lock and firing unbound tasks; tearing down a consumed one-shot's session) are left open as tracked follow-ups — both need new daemon↔child infrastructure. * fix(scheduled-tasks): one-shot anchor on unarchive, memoize next-fire, sanitize + log Review follow-ups (#6389): - [Critical] enableTasksForSessions now re-seats a ONE-SHOT's createdAt anchor (not just recurring's lastFiredAt) on unarchive — otherwise unarchiving a task that was converted to recurring:false while disabled fires it as a missed one-shot and permanently deletes it. - [Critical] Log the DELETE-path removeTasksForSessions failure (was fully swallowed) like the archive/unarchive paths — the session is already gone, so a silent write failure leaves the still-enabled bound task a permanent ghost. - [Medium] Memoize nextDurableFireMs (deterministic per id/cron/recurring/anchor) — a sparse cron costs hundreds of ms per scan and the route recomputed it per task on every request, stalling the event loop for 50 yearly tasks. - [Nit] The consumed one-shot /run response now nulls nextRunAt (it was advertising a future fire on an entity the next GET omits). - [Suggestion] scheduledTaskSessionName strips terminal control sequences (the bridge title guard rejects them → silently drops the rename) and truncates on a code-point boundary (no lone surrogate broadcast as U+FFFD). - [Critical/doc] Document that firePersistPending is instance-scoped — the narrow cross-instance restart window is an accepted edge. - Added the missing test for editing an enabled one-shot's cron. New tests mutation-adjacent; suites green. Two deeper items (session deleted outside the daemon orphaning a bound task; surfacing bound tasks in cron_list) are left open as tracked follow-ups. * fix(scheduled-tasks): re-seat one-shot anchor on re-enable; guard duplicate revive Two [Critical] review follow-ups (#6389): - Re-enabling a one-shot now re-seats its createdAt anchor (added justReEnabled to the one-shot branch). A one-shot disabled past its slot then re-enabled was otherwise read as a missed one-shot on the next reload — fired immediately and permanently deleted. Updated the prior "leaves anchor untouched" test to the safe behavior (fires at next occurrence). - Keepalive revive no longer spawns a duplicate child: loadSession isn't abortable, so a timed-out revive keeps running; a later tick (past its backoff) would start a SECOND load for the same session. An in-flight `reviving` set (cleared on the load's TRUE settlement, not the timeout) blocks that — without holding the sequential tick, so other sessions' heartbeats aren't delayed. Added a configurable reviveTimeoutMs for the test. Both mutation-verified. (The one-shot /run session teardown raised again is the same item as the open deferral — a synchronous close there would break the run, which executes after /run; it's tracked for the keepalive orphan-sweep.) * fix(scheduled-tasks): strip bidi override/isolate chars from session name The bridge's title guard (hasControlCharacter) only rejects C0/DEL, so Unicode bidi override/embedding/isolate controls (U+202A–202E, U+2066–2069) slip past it and can visually reorder a scheduled-task session name in the session list — a Trojan-Source-style attack (CVE-2021-42574). Strip them alongside the existing terminal-control-sequence pass, matching core's stripDisplayControlChars canonical set. Adds a test built from code points so the test file itself carries no reordering controls. * fix(scheduled-tasks): close review findings — rehydrate deadlock, manual-run recording, shared helpers, tests Addresses the review findings on the per-task-session work: - keepalive rehydrate no longer awaits a non-abortable loadSession after its timeout. A genuinely hung load would pin its worker and, with enough hangs, wedge the whole boot sweep (Promise.all never settles) so later task sessions never rehydrated. The worker now records the timeout as failed and pulls the next queued session; the background load is left to settle. Rewrote the test that pinned the old "hold the slot" behavior into a no-wedge regression guard. - web-shell manual run drops its pre-admission timeout. sendPrompt isn't abortable, so rejecting on the timer while the send was still in flight let a LATE admission execute an UNRECORDED run the user could retry into a duplicate. The run is now tied to admission (accepted prompts are always recorded); the "session never becomes active" phase stays bounded by the switch timeout in runTaskManually. - extract collectBoundSessionIds() shared by the heartbeat + rehydrate passes (was duplicated) and isBoundTask() in the lifecycle module (was the lone `sessionId !== undefined` check vs. the strict one used everywhere else). - spell the nextDurableFireMs cache-key separator as `\x00` rather than a literal NUL byte, so cronScheduler.ts no longer reads as binary to ripgrep. - add App.test coverage for the manual-run orchestration (admission-resolve, cancel/error reject, immediate fire, supersede, switch timeout) and a keepalive test that a disabled task gets no heartbeat and no revive. * fix(web-shell): "create via chat" opens a fresh session in scheduled tasks The scheduled-tasks "Create via chat" button switched to the chat view but stayed on the CURRENT session, piling the task-creation conversation onto whatever the user was already doing. It now starts a new session first (createNewSession) and jumps to it before priming the composer, so task creation gets its own chat. Covered by a new App.test case asserting clearSession() is called. * fix(scheduled-tasks): address follow-up review findings - keepalive rehydrate: guard the onError callback with try/catch. If it threw (e.g. stderr EPIPE during log rotation) the rejection escaped loadOne, failed its worker, and short-circuited Promise.all — stranding every other queued session. - cronScheduler catch-up: use the strict `typeof sessionId === 'string' && length > 0` bound-check instead of `!== undefined`, matching every other "is bound?" site. - server rehydration: log the outer defense-in-depth catch instead of swallowing it, so an unexpected throw isn't a silent "tasks never fire". - session-name sanitizer: also strip the standalone Bidi_Control marks U+061C / U+200E / U+200F, not just the override/isolate ranges. - scheduled-tasks dialog: when a consumed one-shot then fails to deliver, show a specific "deleted but never ran — recreate it" error instead of the generic "run failed" that hid the deletion. Kept the deliberate consume-first ordering. * fix(web-shell): don't prime the composer when "create via chat" can't start a new session onCreateViaChat's deferred composer-priming ran unconditionally: if createNewSession() failed, the task-starter text was dropped into the CURRENT session (only onSessionIdChange was gated on success). Gate all post-create side effects on `created`, matching handleMissingSessionNewSession. Adds an App.test failure-path case (new session fails → composer not primed). --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
be7e874fd1
|
Handle missing web-shell sessions without redirecting (#6357)
* fix(web-shell): handle missing session routes * chore(web-shell): clarify missing session route handling * fix(web-shell): address missing session review follow-up * fix(web-shell): address missing session review issues * test(web-shell): cover missing session status handling * fix(webui): handle heartbeat terminal states * fix(web-shell): preserve missing session state * fix(webui): harden missing session diagnostics * fix(web-shell): stabilize missing session recovery * fix(webui): preserve missing session heartbeat state * fix(webui): stabilize missing session recovery * fix(webui): cover missing session review gaps --------- Co-authored-by: ytahdn <ytahdn@gmail.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
d56bd1d8f4
|
fix(daemon): handle settings reload events outside transcript (#6407)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
350191e101
|
feat(web-shell): add token-usage analytics dashboard to Daemon Status (#6388)
* feat(web-shell): add token-usage analytics dashboard to Daemon Status Add a "统计 / Usage" tab to the Daemon Status page: a Today/7D/30D period toggle over the selected range's token totals and input/output/cache-read breakdown, a 12-month token heatmap (per-day tokens + cache-read tooltip, localized month labels), per-model token share, skill-call counts, and daily token/session charts. Backend: a new read-only GET /usage/dashboard daemon API backed by a core usage-dashboard service that aggregates the durable local usage history (cross-project ~/.qwen), reusing loadUsageHistory + aggregateUsage. Skill counts are threaded through the shared usage pipeline. No new instrumentation — every metric is read from data qwen-code already persists. * fix(web-shell): address usage-dashboard review feedback - cap `aggregateUsage` topSkills at 25 like topTools, so the aggregate and dashboard payload stay bounded - fix a DST drift in the heatmap grid: advance the day/month cursor by calendar day (setDate) instead of a fixed `i * MS_PER_DAY` offset - cache the loaded history once (range-independent) so toggling Today/7D/30D re-aggregates from a single disk read; split a pure `buildUsageDashboard(records, opts)` out of `loadUsageDashboard` - drop the unused per-day streak computation and the dead `daemon.usage.streak` i18n key - add debug logging to the dashboard builder and a direct `aggregateUsage`-skills unit test * fix(usage-dashboard): make the dashboard load read-only + fix cache coalescing - Make the daemon dashboard side-effect free: `loadUsageHistory` gains a `persistRebuild` option, and the route passes `persistRebuild: false`, so serving a GET never writes to `~/.qwen`. The transcript-rebuild fallback previously persisted rebuilt records (including an in-progress session), violating the read-only contract. - Fix cache coalescing on the slow path: a pending history load is now reused regardless of age (the TTL starts at settlement), so a request arriving after the TTL while the load is still pending no longer kicks off a second full load. - Tests: read-only rebuild writes nothing, `metricsToUsageRecord` copies `SessionMetrics.skills`, and a pending load is shared past the TTL. |
||
|
|
9a63c03224
|
feat(web-shell): add a Scheduled Tasks management page (#6348)
* feat(web-shell): add scheduled tasks management page Add a "Scheduled tasks" page to the Web Shell for managing durable cron tasks against the current workspace. - Sidebar entry opens a full-pane page (replaces the chat area, not a modal) listing tasks with enable/disable toggle, delete, run-now, and human-readable schedules. - "New scheduled task" opens a modal with a schedule builder (daily / weekdays / weekly / hourly / every-N-minutes / custom cron) and a live preview. - "Create via chat" returns to the chat and primes the composer so the agent creates the task through its cron_create tool. - Daemon CRUD routes (GET/POST/PATCH/DELETE /scheduled-tasks) read/write the existing per-project scheduled_tasks.json; task firing stays with the session-side scheduler. - Extend DurableCronTask with optional name/enabled (backward compatible); the scheduler skips tasks with enabled:false. - Add /scheduled-tasks to the vite dev-server proxy allowlist so the page works under npm run dev:daemon. * chore(web-shell): address review feedback on scheduled tasks - cron_list: surface name/enabled so the agent can tell a disabled durable task from an active one (a disabled task no longer looks identical to an active one). - core: export only the tasks-file functions the daemon route actually uses (drop unused addCronTask / getCronFilePath / CRON_TASKS_DISPLAY_PATH from the public barrel). - CronScheduler: warn when a durable reload fails and the prior view is kept, since a just-disabled or -deleted task can keep firing until the next successful reload. - Extract the schedule helpers (buildCron / describeCron / parseHhmm / describeLastRun) into a pure module and add unit tests for them. - Add route tests for PATCH cron/prompt/recurring, empty-patch rejection, and POST field-length / boolean-type validation. * chore(web-shell): address second review round on scheduled tasks - Log CRUD errors server-side (writeStderrLine) in each route catch block, matching the other daemon routes. - Share one id generator (generateCronTaskId in cronTasksFile) between the scheduler and the daemon route instead of duplicating it. - describeCron: recognize cron day-of-week 7 as an alternate notation for Sunday. - Reset the builder time to :00 when switching to the hourly frequency (its time picker is hidden, so it no longer silently carries the daily minute). - Tests: cron_list name/disabled output; route Feb-30 impossible-cron and corrupt-file 500 read-failure; describeCron dow=7. * chore(web-shell): address third review round on scheduled tasks - Run now: report sendPrompt rejections via the toast/error path instead of dropping the promise. - Block chat interaction while the full-pane Scheduled Tasks view is open, so the covered composer can't receive keystrokes/Escape. - Guard reload() with a request-sequence id so a slow load can't overwrite a newer list after a mutation. - Re-enabling a task that had genuinely fired resumes from now instead of catching up work paused while it was disabled. - Restrict "every N minutes" to divisors of 60 (a non-divisor */N fires more often than the label claims). - Show a Repeats / Runs once label on each card so tool-created one-shots aren't mistaken for repeating schedules. - Return generic 500 client messages (no internal file path); the detail is logged server-side. - Tests: SDK scheduled-task methods (method/URL/id-encoding/headers/errors); route re-enable behavior both ways. * chore(web-shell): address fourth review round (minor suggestions) - Route error logs interpolate the actual task id instead of the literal ":id". - cron_list returnDisplay includes the task name (matching llmContent) so terminal /cron list shows UI-assigned names. - Truncate the delete-confirm label so an unnamed task's long prompt doesn't blow up the confirm() dialog. - Cap the create-form prompt textarea at MAX_PROMPT_LENGTH and drop the dead typeof-window guard. - Test generateCronTaskId (format + near-uniqueness). * chore(web-shell): address fifth review round on scheduled tasks - Re-enable now resumes any recurring task from now (stamp on every false→true), not only ones that had already fired — a task disabled before its first run no longer catch-up-fires the slot it was paused through. - describeCron applies the same divisor-of-60 check as buildCron, so a hand-edited/persisted */45 falls back to the raw expression instead of a misleading "every 45 minutes". - Strengthen the corrupt-file route test to assert the generic client message and no leaked file path. - Tests: recurring-disabled-before-first-run and one-shot re-enable; describeCron non-divisor fallback. * test(cli): cover legacy scheduled-task normalization on GET Seed a pre-fields task (no name/enabled) directly to disk and assert the GET response normalizes it to name:null / enabled:true, guarding backward compatibility with existing scheduled_tasks.json files. * fix(core): cap durable cron loads against a durable-only budget The daemon route accepts up to MAX_JOBS durable tasks on disk, but the scheduler previously capped durable loads against its combined job map (session-only + durable). A session holding session-only cron jobs could push the map to MAX_JOBS and make loadFileTasks silently skip durable tasks the route had already accepted — a create that returned 201 would then never fire. Cap durable installs against a durable-only count instead, and share one MAX_JOBS constant between the scheduler and the daemon route, so a successful create is always loadable. Adds a scheduler test that 40 session-only jobs no longer crowd out 20 durable loads. |
||
|
|
7a528d078a
|
feat(daemon): Add session organization (#6305)
* feat(daemon): add session organization Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(daemon): address session organization review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(daemon): cover session organization review cases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6305) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6305) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6305) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6305) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6305) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6305) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(web-shell): Address session organization review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Harden session organization review edge cases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Address session organization review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
52a190b5c6
|
feat(web-shell): time-series metrics charts on Daemon Status (#6307)
* feat(web-shell): time-series metrics charts on Daemon Status
Add seven bottleneck-analysis line charts (concurrency, requests, API
latency, prompt latency, event-loop lag, memory, token burn) to the
Daemon Status dashboard, backed by a new server-side metrics ring.
The status endpoint is a point-in-time snapshot, so line charts need a
time series. A bounded ring buffer in the daemon (daemon-metrics-ring.ts)
seals one bucket every 5s (~15min retained) from three seams:
- HTTP request rate/latency via the telemetry middleware
- prompt queue-wait/duration via the bridge telemetry hooks
- per-round token usage sniffed at the bridge session/update fan-in
(new DaemonBridgeTelemetryMetrics.tokenUsage hook)
plus memory / active sessions+prompts / a window-scoped event-loop lag
p99 read as gauges at seal time.
The series rides the existing GET /daemon/status contract
(runtime.metrics.series), threaded through the SDK types (JSON passthrough)
to a dependency-free inline-SVG chart component in web-shell -- no charting
library added to the CSP-strict serve --web bundle.
Tests: metrics-ring math, token-usage sniffing on the real sessionUpdate
path, and SVG chart rendering. Verified end-to-end against a live daemon
(GLM-5.2): requests/latency/memory/event-loop, real token burn and prompt
duration, with the concurrency gauge tracking active prompts.
* feat(web-shell): tabs, chart tooltips, and fullscreen for Daemon Status
Split the now chart-heavy Daemon Status dashboard into Overview / Metrics /
Diagnostics tabs (status badge, refresh, and issues stay global) so
monitoring, configuration, and troubleshooting each get their own space
instead of one long 70vh scroll.
Add an interactive hover cursor to the charts: a vertical time line, a dot on
each series, and a tooltip reading the bucket time plus every series' value at
that point -- previously only the latest value and peak were legible, from the
legend.
Add an opt-in fullscreen toggle to DialogShell (via allowFullscreen, wired for
Daemon Status) that expands the panel to near the full viewport; scrolling is
consolidated into the shell body so the content actually grows with it.
Tests: tab switching + diagnostics-behind-tab, SVG tooltip rendering, and the
DialogShell fullscreen toggle. Verified end-to-end against a live daemon
(GLM-5.2) with real request / token / prompt data.
* feat(web-shell): add CPU, LLM-latency, queue-depth, IPC & connection metrics
Extend the Daemon Status metrics ring with more bottleneck-analysis
dimensions, filling the two biggest gaps — resource cost had only memory
(no CPU), and latency had only client->daemon HTTP (not daemon->model):
- CPU %: process.cpuUsage() delta, core-normalized (memoryPressureMonitor
formula, clamped 0-100), sampled alongside memory.
- LLM API latency p50/p95: the token frame's _meta.durationMs (the
daemon->model round-trip), separating 'model is slow' from 'we are slow'.
- Prompt queue depth: a new bridge.pendingPromptTotal aggregate, folded into
the concurrency chart beside active tasks.
- IPC pipe throughput: daemon<->ACP-child stdio bytes (already measured; now
windowed via metricsRing.recordPipe).
- Connection counts (SSE/WS/ACP) and rate-limit rejections, read lazily in the
sampler from the ACP handle registry and the rate limiter.
The tokenUsage telemetry hook is widened to carry durationMs. Verified
end-to-end against a live daemon (GLM-5.2): LLM p95 28.6s vs HTTP p95 324ms,
queue depth 1, IPC peak 0.3MB, SSE gauge 1 on a live stream.
* feat(web-shell): add ACP child process CPU/memory (self-reported over ACP)
The daemon's own CPU/memory only tell half the story — the real LLM/tool work
runs in the spawned 'qwen --acp' child, which is where the resource cost lives.
Surface it: the child self-reports its rss + cpuPercent to the daemon over a new
read-only ACP extMethod (qwen/status/workspace/resource); the bridge caches the
latest sample on the live channel, and the metrics sampler reads it
synchronously each tick (firing an async refresh for the next, off the hot path).
The child computes cpuPercent as a process.cpuUsage() delta between polls (no
dependency on MemoryPressureMonitor's tool-gated sampling), core-normalized and
clamped. Rendered as a second line on the CPU and Memory charts (daemon vs
child, side by side).
Verified end-to-end (GLM-5.2): child RSS ~300MB vs daemon RSS ~225MB, child CPU
tracking above the daemon's -- the child is the resource hog, now visible.
* test(web-shell): cover Metrics tab, chart rendering, and the recordRequest seam
Address review — the metrics dashboard's rendering and its HTTP data seam had
no tests:
- DaemonStatusDialog: switching to the Metrics tab renders the charts from the
series (one SvgLineChart per card) and hides the Overview panel; an empty
series shows the collecting-metrics placeholder.
- daemonTelemetryMiddleware: recordRequest fires once with (durationMs,
statusCode) on a matched route (real status code; once across finish/close),
is not called for unmatched routes, and is a silent no-op when omitted.
* fix(web-shell): enlarge Daemon Status charts in fullscreen
Fullscreen widened the panel but the charts stayed small — the grid just packed
in more 280px cards at a fixed 52px SVG height, so the extra viewport bought
more small charts, not bigger ones. Now the DialogShell body carries a
`data-dialog-fullscreen` marker; the chart grid switches to wider cards (min
480px → fewer columns) and the SVG grows to 120px, so fullscreen actually
enlarges the plots. Verified: 2 wide columns at 120px vs 3-4 columns at 52px.
* fix(web-shell): resolve chart colors in portal, guard child-resource polling
Address review (real-user + ci-bot):
- [Critical] Chart colors (--primary, --agent-blue-400) resolved to nothing in
the DialogShell portal (createPortal to document.body escapes the app root that
defines them), so ~half the chart lines rendered stroke:none. Add both vars to
DialogShell's own theme scope. Verified: 25/25 path strokes colored (was 5 none).
- [Critical] refreshChildResource had no in-flight guard; requestWorkspaceStatus
waits up to 10s (> the 5s cadence), so a degraded child accumulated concurrent
polls. Add a single-flight guard.
- [Critical] getChildResourceSnapshot returned last-good rss/cpu forever; add a
30s staleness window so a stuck child reads 0 instead of looking healthy.
- Exclude GET /daemon/status (the dashboard's own poll) from the metrics-ring
request rate, so the Requests chart doesn't count itself.
- Fix cpuPercent JSDoc (percent of total capacity across cores, clamped [0,100])
in the ring + SDK mirror; add a keep-in-sync cross-reference on the mirror.
Tests: recordRequest excludes /daemon/status; buildDaemonStatusResponse embeds
runtime.metrics.series when provided and omits it otherwise.
* fix(web-shell): address Daemon Status charts review feedback
Correctness fixes surfaced in review:
- bridgeClient: guard token accounting on a live `entry`. On the
`session/load` path HistoryReplayer re-emits saved usage as live
session/update frames before the session entry is registered, which
otherwise dumped a session's historical token total into the current
metrics window as a phantom burn spike with no model call.
- run-qwen-serve metrics sampler: wrap each tick in try/catch/finally so a
throwing getter can't crash the daemon; reset the event-loop-lag histogram
in finally so a thrown tick can't permanently discard it; skip the CPU
delta (and leave the baseline untouched) when process.cpuUsage() throws;
seed the rate-reject baseline on the first tick instead of reporting the
whole since-start backlog as one spike.
- acpAgent workspaceResource: advance the child-CPU baseline only on a
successful read, avoiding a ~2x phantom spike on the poll after a failure.
- bridge.pendingPromptTotal: count only queued prompts (state === 'queued'),
not the running one, so the "Queued" chart reflects real backpressure and
no longer shadows the "Active tasks" line.
- Make the new Daemon Status bridge hooks optional in AcpSessionBridge and
optional-chain them in the sampler, so a bridge injected via
RunQwenServeDeps.bridge that predates them degrades gracefully.
Robustness / UX:
- daemon-metrics-ring sanitizes non-finite gauges to 0 so a bad reading
never serializes as JSON null and gaps the chart.
- child-resource refresh logs failures at debug for observability.
- formatBytes drops to KB/B for sub-MB pipe traffic (was "0.0 MB").
- SvgLineChart peak label is now i18n'd (daemon.charts.peak).
- Daemon Status tabs get the full WAI-ARIA tabs pattern: aria-controls,
role=tabpanel, and Arrow/Home/End keyboard navigation with roving tabindex.
Tests: replay token guard (no live entry), pipe/gauge/sample-cap defenses,
large-value legend formatting, and tab keyboard navigation.
* fix(web-shell): keep Daemon Status fullscreen + tooltip correct in dialog portal
Two DialogShell-portal theme-scope issues surfaced by a follow-up review:
- Fullscreen was clamped back to 80vh on narrow screens: the
`@media (max-width: 560px)` `.panel` rule has equal specificity and later
source order than the base `.panelFullscreen`, so it won. Add a media-scoped
`.panelFullscreen` override so fullscreen actually expands on mobile.
- SvgLineChart tooltip background used `var(--popover, var(--card))`, neither of
which the portal theme scope defines, so the declaration dropped and the
tooltip rendered transparent over the chart. Fall back to `--background`
(which the dialog scope does define).
* fix(web-shell): flip chart tooltip below cursor near scroll-container top
The Daemon Status charts live inside DialogShell's overflow-y:auto body, so the
topmost chart's upward tooltip (bottom: calc(100% + 4px)) clipped against the
scroll container's top edge, truncating the time header / first series row on
hover. SvgLineChart now resolves its nearest scroll parent and flips the tooltip
below the cursor when the plot sits within ~one tooltip-height of that clip
boundary.
* fix(daemon-status): harden child-resource CPU/memory + sampler lag on failure
Follow-up review fixes:
- acpAgent: prevChildCpu inits to null (not {0,0}) and the workspaceResource
handler gates the delta on a live prevCpu baseline, so an init-time
cpuUsage() failure no longer manufactures a phantom spike on the first poll
— mirrors the daemon sampler's safeCpuUsage null-on-failure contract.
- acpAgent: guard process.memoryUsage() too, reporting 0 rss on failure while
keeping the already-computed cpuPercent instead of throwing the handler.
- bridge: require Number.isFinite() (typeof NaN === 'number' is true) and
clamp cpuPercent to [0,100] when caching the child's self-report.
- run-qwen-serve sampler: gate the 5s child-resource refresh on an active
SSE/WS client (idle staleness already reads 0), and hoist the event-loop
lag read before the try so a thrown tick charts the real accumulated lag
instead of a misleading 0.
* fix(daemon-status): protect artifact path from metrics callback + share CPU delta
Follow-up review fixes:
- bridgeClient: wrap recordLiveTokenUsage in try/catch so a throwing injected
onTokenUsage callback can't skip the critical artifact processing after it —
metrics are optional, artifacts are not.
- Extract computeCpuPercent() into daemon-metrics-ring and share it between the
daemon self-sampler and the ACP child's workspaceResource handler, removing
the duplicated delta/normalize/clamp math and giving it direct unit coverage
(null sample, non-positive window, normalization, phantom-spike + negative
clamps).
- Add a single-flight test for bridge.refreshChildResource (two rapid calls
collapse to one in-flight RPC).
|
||
|
|
54ba259106
|
fix(web-shell): keep skill slash commands after starting a new session (#6319)
* fix(web-shell): keep skill slash commands after starting a new session Starting a new session (the sidebar button, quick action, and the /new, /reset and /clear commands all route through clearSession) ran getConnectionAfterSessionClear, which deleted connection.commands and connection.skills. Nothing repopulated them: the SSE loop returns early on manualSessionClear and the deferred skill-fetch path never re-runs, so before the new session's first prompt the composer fell back to the hardcoded local command list. That list omits skills, so typing "/rev" and pressing Tab would not complete "/review". Preserve the workspace-scoped commands and skills across a clear (skills, custom, MCP-prompt and workflow slash commands all live at the workspace/config level, not the session), and only drop the session-scoped supportedCommands and context snapshots. This keeps skill-backed slash commands autocompleting in the new deferred session before its first prompt — the same guarantee #6153 added for the initial deferred connect — while still forcing the next session to refetch fresh metadata. The next session's available_commands_update refreshes the list once it lands. * fix(web-shell): treat a fulfilled empty command snapshot as authoritative Address review feedback on the new-session command fix. Preserving commands across a clear means a later refresh must be able to clear them again when the workspace command list genuinely shrinks to empty, otherwise the preserved entries would keep autocompleting forever. Both refresh paths previously kept the previous list on an empty result (`commands.length > 0 ? commands : current.commands`): - The streamed available_commands_update handler now assigns the mapped commands directly, matching how skills were already handled — the daemon snapshot is authoritative. - The post-attach supported-commands assignment now falls back to the preserved list only when the fetch was skipped or failed (supportedCommands === undefined), not when it returned an empty list. Add tests: an available_commands_update that empties the list clears stale commands; a fulfilled-empty supported-commands fetch after a clear drops the preserved commands; and getConnectionAfterSessionClear is exercised with the commands/skills fields already absent. * test(web-shell): cover supported-commands fetch failure after a clear Add error-path coverage for the post-attach command assignment: when the new session's supportedCommands() rejects, supportedCommands stays undefined and the commands preserved across the clear must survive rather than being wiped. Complements the fulfilled-empty test, which locks that a successful empty snapshot is instead treated as authoritative. |
||
|
|
59e771cef6
|
feat(daemon): Add session export endpoint (#6297)
* feat(daemon): add session export endpoint Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6297) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix PR integration capability baseline (#6297) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address export tool call id review (#6297) 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-bot@users.noreply.github.com> |
||
|
|
c37cb23ccc
|
feat(web-shell): manage sessions from the sidebar (archive, unarchive, delete) (#6293)
Add an Archive quick action and a "..." overflow menu (Rename / Archive / Delete) to each session row in the web-shell sidebar, plus a collapsible "Archived" section that lazily lists archived sessions with Restore / Delete. Thread the daemon's existing archiveState filter and archive/unarchive endpoints through the webui workspace facade and the useDaemonSessions hook; rename stays limited to the current live session. |
||
|
|
9b2fb30cb0
|
feat(web-shell): add a daemon status page backed by GET /daemon/status (#6272)
* feat(web-shell): add a daemon status page backed by GET /daemon/status Surface the consolidated daemon status API (#5174) in the Web Shell as a dashboard dialog opened from a sidebar footer button. - @qwen-code/sdk: DaemonClient.daemonStatus(detail) plus DaemonStatusReport* wire types for the /daemon/status envelope (summary and full detail). - @qwen-code/webui: loadDaemonStatus workspace action and a useDaemonStatusReport hook (exported as useDaemonStatus from daemon-react-sdk). - web-shell: DaemonStatusDialog rendering one dashboard — overall status badge, issues list, daemon/runtime/transport/security/limits/capabilities cards, plus per-session, workspace-diagnostics, and auth sections. The daemon's summary/full cost split is hidden from the operator rather than exposed as a toggle: the cheap summary rides a 5s auto-refresh while the expensive full report (which may spawn the ACP child and aggregate workspace diagnostics) is fetched only on open and on manual refresh, so parking the dialog open never rehits that path. Capabilities are sorted, counted, and height-capped; the long workspace path stays on one line, front-truncated so the tail remains visible. New pulse-icon sidebar entry; EN/zh-CN strings. - vite dev proxy: forward /daemon to the daemon; without it the SPA fallback answered /daemon/status with index.html and the dialog failed JSON parsing under npm run dev:daemon. * fix(web-shell): address review on the daemon status dashboard - Drive the status badge and issues list off the full report when it is available, not the summary. The daemon only rolls workspace/preflight/MCP problems into status+issues for detail=full, so the summary can read "ok" with no issues while a loaded full report is degraded — the dashboard now reflects the full rollup (live counters still come from the summary). - Guard the 5s poll with an in-flight ref so a slow/degraded daemon cannot accumulate overlapping status calls (useDaemonResource discards stale completions but does not abort; the client timeout is 30s). - Fix the public DaemonStatusReport wire type: runtime.channelWorker.channels is string[] (ChannelWorkerSnapshot), not an array of objects; mirror the remaining optional snapshot fields. * fix(web-shell): translate workspace section status badges WorkspaceSectionRow rendered the raw wire status (`ok`/`warning`/`error`/ `unavailable`) while every other badge in the dialog goes through `t()`, so under a Chinese UI these badges showed lowercase English. Route the badge through `t('daemon.level.<status>')` and add the missing `daemon.level.unavailable` key to both dictionaries. * fix(web-shell): scope toolbar error to summary; broaden dashboard test coverage - The toolbar "failed to load" banner now keys on the summary fetch only. A failed full fetch is already surfaced in the diagnostics section, so it no longer makes an otherwise-healthy summary (fresh cards + timestamp) read as broken. - Use the ASCII "..." ellipsis in the diagnostics-loading string to match the rest of the i18n dictionary. - Add tests: summary-healthy/full-failed degraded state, the ACP-disabled transport branch, uptime/memory/duration formatting across unit boundaries (day, GB, sub-second, fractional-second), and sidebar Daemon Status button click (expanded + collapsed) — the feature's only entry point. * fix(web-shell): pause polling on hidden tab; scope dev proxy; fix test mock - Skip the 5s status poll while document.hidden, matching the sidebar poll — a backgrounded tab no longer hits the daemon every 5s. - Narrow the vite dev proxy to the exact /daemon/status route instead of a bare /daemon prefix, mirroring the scoped /voice/stream entry; verified the dashboard still proxies (summary + detail=full) in dev. - Add a message field to the DaemonStatusReport issue mock in the webui provider test so it matches the required DaemonStatusReportIssue shape. * feat(web-shell): surface runtime/channel-worker diagnostics; a11y + polish Address the daemon-status review round: - Render the runtime startup/failure state (runtime.loading / runtime.error) in the Runtime card so the plausible-looking zero counters during startup are not mistaken for a healthy idle daemon. - Surface channel-worker diagnostics (state, exit code/signal, error, restart count) when the worker is enabled — these fields were fetched and typed but never shown, leaving a bare "down" with no context. - Include full.error.message in the diagnostics-failure line (matching the summary error path) so a failed detail fetch is actionable. - Show "N/A" instead of a literal "null" chip for null workspace summary values (the wire type allows null). - Add role="status" + aria-label to the health badge for screen readers. - Rename the public hook alias useDaemonStatus -> useStatusReport, matching the Daemon-prefix-stripping convention of the other re-exports. - Add tests: runtime startup/failure, channel-worker diagnostics, and the empty/disabled placeholders (sessions, rate limit, capabilities, ACP), toolbar-banner-with-data, and pure-loading branches. * fix(web-shell): contain daemon status crashes; workspace empty-state - Wrap the dashboard in a local ErrorBoundary so a malformed/partial daemon response (e.g. an older daemon omitting an additive field like channelWorker) — most likely exactly when the daemon is sick and the dashboard is most needed — shows a contained fallback instead of throwing to the root boundary and white-screening the whole web shell. - Add an empty-state to the Workspace Diagnostics card (parity with the Sessions card) for when full.workspace is empty. - Tests: error-boundary containment on a malformed report, and the workspace empty-state. * fix(web-shell): fix error-boundary recovery; contain detail crashes Address the review round (one Critical): - ErrorBoundary recovery was broken: the comment claimed resetKeys cleared the fallback, but none was passed. Switch to a function-form fallback that surfaces the actual render error (distinct from a network failure) and fix the comment — recovery happens on re-open, since the parent only mounts the dialog while open. - Wrap FullDetail in its own ErrorBoundary so a malformed detail=full payload is contained to the detail region instead of taking down the healthy summary cards with it; add a catch-all branch so a fetch that resolves without a `full` section shows a failed state instead of hanging on "Loading...". - Toolbar failure banner now shows only when the summary errored AND still has data on screen (`summary.error && summary.report`), so it no longer misrepresents a dashboard that is rendering from the full fallback. - SDK type: drop `& Record<string, unknown>` on DaemonStatusReport.daemon and add the typed optional `startup` field, matching the DaemonCapabilities convention the interface JSDoc claims. - Add a real useDaemonStatusReport hook test asserting the `report` alias maps from `data` — the dialog test mocks the whole hook, so nothing else guarded it. * feat(web-shell): surface runtime.activity in the daemon status dashboard PR #6270 added a runtime.activity sub-object to GET /daemon/status (activePrompts, lastActivityAt, idleSinceMs). Type it as an additive optional on the SDK DaemonStatusReport and render it in the Runtime card: active-prompt count and an idle duration ("no activity yet" when the daemon has seen none). Gated on the field's presence so older daemons that omit it still render. Verified end-to-end against a real qwen serve --web that emits the field. * fix(web-shell): daemon status polish — i18n count, negative clamp, coverage Address the review round (all minor): - Move the capabilities count into the i18n string (daemon.capabilities.titleCount with a {count} placeholder) so locales can reorder it. - Clamp negative durations in formatDurationMs (clock-skew defense). - Re-export the hook options type as StatusReportOptions for consumers wrapping useStatusReport. - Tests: use the real rate-limit tier keys (prompt/mutation/read) in the fixture, and cover the session-id display fallback, the channel-worker signal branch, and a healthy workspace section's chip/status rendering. * feat(web-shell): name the failing checks behind a workspace section status A "warning"/"error" workspace-diagnostics section only showed a rollup badge plus count chips, so e.g. a warning preflight was opaque — the operator couldn't tell it was the auth check without curling the API. Extract the individual warning/error cells from the section's raw data (across cells / servers / skills / tools / providers / hooks / extensions) and render each with its label and message (e.g. "auth: No auth method configured."). OK and other non-problem cells stay hidden. Verified end-to-end: a real daemon with no credentials now shows the auth warning inline under preflight. |
||
|
|
b1ec04f4bd
|
fix(web-shell): improve session restore and loading feedback (#6220)
* fix(web-shell): avoid smooth scroll on session restore * fix(web-shell): show skeleton during session load * fix(web-shell): tighten session restore guards * fix(web-shell): address session restore review issues * fix(web-shell): clear session loading on load failure * test(web-shell): cover session restore review cases --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
8dfa7613be
|
fix(serve): respect disabled skill settings (#6223)
* fix(serve): respect disabled skills in status * test(serve): align skill disabled status coverage --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
686a1371c3
|
fix(web-shell): cut mobile session-switch jank (memoized timeline signature, replay-first dispatch) (#6183)
* fix(web-shell): cut mobile session-switch jank (P0) - MessageList: wrap in memo and gate the O(transcript) session-timeline signature/entries computation behind rail visibility (container >= 1160px, never true on mobile), so scroll frames and unrelated App renders no longer rebuild a transcript-sized string - DaemonSessionProvider: dispatch the replay snapshot before the providers/commands/context fetches so the transcript paints one metadata round-trip earlier on session switch; keep catchingUp cleared once the replay is injected Refs #6181 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(webui): cover replay resume catchingUp state * test(webui): assert catchingUp replay state sequence * fix(web-shell): restore timeline observer bootstrap --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
f3b3b99c46
|
fix(web-shell): show skill slash commands (e.g. /review) before first prompt (#6153)
* fix(web-shell): show skill slash commands (e.g. /review) before first prompt Since session creation is deferred until the first prompt (#6066), the deferred connect path reported 'connected' but only fetched workspace providers — it never populated the slash-command list. Before sending a message the composer therefore fell back to the hardcoded local command list, which omits skills, so '/rev' would not autocomplete '/review'. Fetch the session-less /workspace/skills status alongside providers in the deferred connect path and seed connection.commands/skills from it, so skill-backed slash commands autocomplete immediately. The full session-scoped supported-commands snapshot (which also carries custom, MCP-prompt and workflow commands) still replaces this once the first prompt creates a session. * test(web-shell): cover deferred workspace skills fetch failure Add a parallel test to the deferred-connect warn coverage: when client.workspaceSkills() rejects, the connection still reports 'connected' (skills are non-blocking) and the failure is logged via console.warn, mirroring the existing workspaceProviders-failure test. Addresses review feedback on #6153. |
||
|
|
1467ed3100
|
fix(web-shell): defer session creation until first prompt (#6066)
* fix(web-shell): defer session creation until first prompt * fix(web-shell): stabilize deferred session attach * docs(web-shell): document optional session change callback * test(web-shell): cover deferred session setup failures * fix(webui): preserve concurrent session load on clear * fix(web-shell): keep session prep helper in utils * fix(web-shell): harden deferred session lifecycle * fix(web-shell): guard deferred session races * fix(web-shell): simplify controlled session selection * fix(web-shell): tighten empty session edge cases * fix(web-shell): tighten deferred session lifecycle * test(webui): align session action mocks with daemon types * fix(web-shell): notify cleared session ids * fix(webui): guard session cleanup races * fix(web-shell): preserve blocked local commands --------- Co-authored-by: ytahdn <ytahdn@gmail.com> Co-authored-by: 易良 <1204183885@qq.com> |
||
|
|
1609bdaa32
|
feat(web-shell): queue prompts while turns are running (#6005)
* feat(web-shell): queue prompts while turns are running * fix(web-shell): address pending prompt review feedback * fix(web-shell): tighten queued prompt event handling * fix(web-shell): avoid showing active prompt as queued * fix(web-shell): address queued prompt review follow-ups * fix(web-shell): address pending prompt review issues * fix(web-shell): reconcile queued prompt actions from server * fix(daemon): address pending prompt critical review * test(webui): expect submit abort signal forwarding * fix(web-shell): avoid duplicate queued prompt sync * fix(web-shell): keep local slash commands out of queue * fix(webui): avoid aborting prompt admission * fix(daemon): avoid queued prompt cancel cascades * fix(webui): avoid stale client id for queue cleanup * test(webui): update stale session queue cleanup expectation * fix(web-shell): preserve queue reconciliation identity * fix(web-shell): guard queue clear session writes --------- Co-authored-by: ytahdn <ytahdn@gmail.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
8babaa47e0
|
fix(web-shell): improve follow-up suggestion handling (#5996)
Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
c90e6e7ba4
|
feat(channels): Add channel agent bridge abstraction (#5978)
* feat(channels): add channel agent bridge abstraction Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(channels): handle bridge session lifecycle cleanup Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(channels): close bridge lifecycle review gaps Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#5978) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#5978) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: address channel bridge review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#5978) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#5978) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
9601d90b78
|
fix(ui): display output tokens instead of cumulative API throughput for subagents (#5972)
Multiple UI components displayed executionSummary.totalTokens (cumulative sum of total_tokens across all API rounds) as the subagent token count. For a subagent making 87 requests with ~51K prompt each, this inflated to 4.4M — misleading users into thinking it was context window usage. Switch all subagent token displays to use outputTokens (what the model actually generated), aligning with the loading indicator which already correctly shows output tokens only. Closes #5683 |
||
|
|
22389247d5
|
feat(web-shell): add optional session sidebar with navigation (#5931)
Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
ef0c39cf96
|
feat(web-shell): browse MCP server resources in the /mcp dialog (#5879)
* feat(web-shell): browse MCP server resources in the /mcp dialog Port the TUI's MCP resource browser (#5544/#5635) to the Web Shell. The /mcp dialog now shows per-server resource and prompt counts plus an expandable resource browser (URI, MIME type, size, description, and the @server:uri chat reference), reaching parity with the terminal UI. Resources and prompts were never serialized past the in-process core registries, so this wires the data end to end: per-server resourceCount and promptCount ride the existing /workspace/mcp status, and a new qwen/status/workspace/mcp/resources ext-method (mirroring the tools drill-down) carries the resource list through the daemon, SDK, and webui hook into the Web Shell McpDialog. All additions are protocol-additive; older daemons 404 the new route and the client degrades gracefully. * fix(web-shell): make resourcesByServer optional and pluralize 1-byte size Address review on #5879: - SerializedMcpStatusMessage.resourcesByServer is now optional, matching its JSDoc ("older clients omit it") so TypeScript enforces the `?? {}` defensive read at every consumer. - mcp.resource.bytes now pluralizes ("1 byte" vs "2 bytes"), consistent with the mcp.resourceCount / mcp.promptCount strings in this PR. * fix(web-shell): address MCP resources review — remove non-functional @ref, harden fallbacks, add tests Address review on #5879: - [Critical] Remove the "@server:uri" chat-reference UI from the resource browser: the Web Shell submits prompts as a plain text block and the daemon forwards it verbatim (the TUI's atCommandProcessor resolution is TUI-client-side only), so the reference never injected resource content. The browser stays as read-only metadata; @-reference injection is a follow-up that needs the resolver wired into the Web Shell prompt path. - [Critical] reloadServer now isolates the resource refetch in its own try/catch so a failed resource load can't report a successful reconnect/enable as failed. - [Critical] resolveServerMcpResources/Prompts skip a throwing session in the pool-mode fallback so one degraded session can't blank the base /workspace/mcp status (which now carries the counts). - [Suggestion] Resource section renders an "unavailable" state when a server advertises a count but the drill-down list is empty. - [Suggestion] Add SDK client URL-encoding test (workspaceMcpResources), route-table matching test, and an unconfigured-server error-branch test. --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
ac2653e9ce
|
fix(web-shell): stabilize active prompt loading state (#5818)
* fix(web-shell): stabilize active prompt recovery * fix(webui): settle restored prompts on cancellation * fix(web-shell): avoid duplicate approval panels * test(acp-bridge): expect inactive prompt snapshots * fix(webui): preserve restored prompt activity in actions * fix(webui): guard restored prompt activity during settlement --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
83e09f2b5b
|
refactor(web-shell): restructure chat UI (#5775)
Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
8ad0cde994
|
feat(cli): add extension operation polling (#5753)
* feat(cli): add extension operation polling * fix(cli): harden extension operation polling --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
896e6c59e0
|
fix(daemon): Refresh workspace provider defaults (#5638)
* fix(daemon): refresh workspace provider defaults Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): fix acp agent mock exports Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): handle provider config wrapper in workspace providers Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(daemon): address workspace providers review feedback Sanitize provider warning URLs before returning workspace providers status and preserve session context-window fallback from provider catalog models. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(acp): order settings change after model switch Broadcast settings_changed inside the serialized model switch callback so it cannot be reordered behind reconcile's corrective model_switched event. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(daemon): sanitize workspace providers errors Sanitize provider construction error messages before returning them from /workspace/providers so credential-bearing URLs cannot leak through the error path. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(daemon): harden provider URL sanitization Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(daemon): bound invalid provider URL sanitization Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
01d28a1481
|
fix(webui): stop auto-recreating session on user-initiated delete (#5633)
When a user deletes a running session via the session list, the server publishes session_closed with reason client_close on SSE. Previously, the reconnect loop treated this as a passive disconnect and called createOrAttach(), auto-creating a new session and undoing the user's delete. Now the provider detects session_closed with reason client_close, aborts in-flight prompts, clears the session, and exits the reconnect loop. Other reasons (idle_timeout, last_client_detached) and session_died events fall through to the normal reconnect path. Co-authored-by: Qwen Code <noreply@alibaba-inc.com> |
||
|
|
bf70079137
|
fix(cli): Fail dangling replayed tool calls (#5624) | ||
|
|
8e652e1ef7
|
feat(web-shell): support daemon session branching (#5613)
* feat(web-shell): support daemon session branching * fix(webui): keep branch events on new session * fix(web-shell): harden fork session handling * fix(cli): neutralize fork history marker --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
18cc73ce05
|
feat(web-shell): add extension management (#5398)
Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
7cd49e063c
|
refactor(tools): rename TodoWrite tool display name to TodoList (#5319)
* refactor(tools): rename TodoWrite tool display name to TodoList Rename the todo tool's user-facing display name from "TodoWrite" to "TodoList" across every surface that shows it, keeping the wire/schema name `todo_write` unchanged (model tool calls and existing configs are unaffected). - core: ToolDisplayNames.TODO_WRITE -> 'TodoList'; add a ToolDisplayNamesMigration alias so coreTools/excludeTools configs referencing the old 'TodoWrite' display name keep resolving. - cli i18n: rename the toolDisplayName.* locale keys (en/zh/zh-TW) so the localized TUI badge stays correct. - web-shell / webui: map the todo tool to 'TodoList' in their display layers. - sdk daemon normalizer: the ACP plan-update path minted toolName 'TodoWrite'; emit the wire name 'todo_write' instead, so the web-shell's existing wire-name-keyed display + i18n render 'TodoList' (zh 任务清单) for plan-routed todos. desktop keeps 'TodoWrite' as an internal tool identifier (humanized to 'Updating Tasks' / 'Todo List Updated' for users, never shown raw). * refactor(tools): complete TodoList rename in permission rules, importer, export, examples Address review feedback on #5319 — three display surfaces still referenced the old name, plus a few non-user-facing spots. - permissions/rule-parser.ts: add `TodoList` to TOOL_NAME_ALIASES so `allow: ["TodoList"]` resolves (legacy `TodoWrite` kept); rename CANONICAL_TO_RULE_DISPLAY + DISPLAY_NAME_TO_VERB to TodoList. Mirrors the existing Task->Agent handling. Without this a permission rule typed with the new UI label silently did nothing. - webui selectors.ts: also recognize the `todo_write` wire name (the daemon plan path now emits it) so todo detection no longer relies solely on the toolKind fallback. - cli export (collect.ts): exported tool-call title TodoWrite -> TodoList (kind discriminator unchanged). - extension/claude-converter.ts: map Claude's TodoWrite -> qwen TodoList. - example agent templates + webui stories + integration HTML export: update the displayed name. - test: resolveToolName('TodoList') and legacy 'TodoWrite' both -> todo_write. desktop keeps 'TodoWrite' as an internal id (humanized to 'Updating Tasks'/'Todo List Updated' for users); deferred as noted in the PR description. |
||
|
|
9a508773db
|
fix(daemon): centralize mid-turn event constant + recover timed-out drains (#5266)
* fix(daemon): centralize mid-turn event constant + recover timed-out drains Follow-up to #5175 addressing two post-merge /review suggestions. Centralize the `mid_turn_message_injected` SSE event `type`: it was a bare literal in the daemon publisher (acp-bridge), the SDK validator/reducer, and the browser consumer (webui), so a rename in one could silently break browser-side dedup. It now lives once in acp-bridge's dependency-free `daemonEventTypes` module (lightweight like `mcpTimeouts`, so the SDK re-exports it via its build-time devDep without dragging acp-bridge's type graph into the SDK bundle), and bridgeClient / the SDK / webui all import the single binding. Close the drain-timeout message-loss window: the daemon splices + SSE-publishes (browser dedupes) before the ACP child's response lands, so if the child's 2s drain timeout fires first, the late response was discarded — losing the messages from both queues (silent, one-turn loss). The child now recovers that late response and injects it on the next batch instead of dropping it. Tests: drain-timeout recovery (Session), and a rename-safety assertion pinning the shared event constant to the wire literal. * fix(daemon): address review — log drain recovery + rename buildMidTurnParts - Emit a `debugLogger.debug` line when a timed-out drain is recovered (session id + count), guarded on a non-empty payload, so the recovery path is correlatable in production logs. - Rename `#formatMidTurnParts` → `#buildMidTurnParts`: the method records to the chat transcript, so a "format" verb understated its side effect. |
||
|
|
6d17c53313
|
Fix completed prompt lifecycle race (#5192)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
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
* fix(webui): release completed prompts before acceptance returns * fix(webui): address review — comment ordering invariant and add error path test Add a comment explaining why settledPrompts must be checked before activePrompts in waitForAcceptedPromptCompletion (the turn event frees the active slot, so checking activePrompts first would find the next prompt's controller and incorrectly reject). Add a test for turn_error arriving before acceptance returns, verifying that sendPrompt rejects with the expected error from the settled cache. * fix(webui): preserve prompt status across late acceptance * chore(webui): rename prompt settled key helper * fix(webui): guard cancel prompt status cleanup --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
259c933873
|
feat(web-shell): expose transcript event changes (#5193)
* feat(web-shell): expose transcript event changes * fix(web-shell): address transcript callback review --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
24a13632c7
|
feat(daemon): deliver web-shell mid-turn messages into the running turn (#5175)
* feat(daemon): deliver web-shell mid-turn messages into the running turn
Let the web-shell hand a message typed while a turn is running to that turn instead of holding it until the next turn. The daemon now answers the ACP child's `craft/drainMidTurnQueue` ext-method from a per-session queue the browser feeds; previously BridgeClient had no `extMethod`, so the child got -32601 and latched the drain off for the session.
Server: `SessionEntry` gains a mid-turn queue; `bridge.enqueueMidTurnMessage` accepts only while a turn is active and the queue is emptied at the idle boundary; `BridgeClient.extMethod` drains it and publishes a `mid_turn_message_injected` SSE frame. A new `POST /session/:id/mid-turn-message` endpoint plus DaemonClient/DaemonSessionClient methods feed the queue.
Browser: `enqueuePrompt` also pushes text-only messages to the daemon; a sidechannel hook drops the matching entries from the local queue when the injection frame arrives. A message is therefore delivered exactly once — mid-turn when a turn is live, or via the existing next-turn queue otherwise — and never both. Exactly-once rests on the injection frame arriving in order ahead of the turn-complete frame, plus the dedupe effect running before the next-turn drain.
Out of scope: live "sent" rendering of an injected message (it shows on reload); ACP-transport (non-REST) ingestion parity.
* fix(daemon): address mid-turn drain review — exactly-once + hardening
Review follow-ups on the web-shell mid-turn drain.
[Critical] The injected-message sidechannel was single-slot (latest-wins), so two drain frames landing back-to-back — a multi-batch turn, or a backgrounded tab flushing buffered SSE — coalesced: the first batch's messages were never removed from the browser queue and got resent next turn = double delivery, the exact failure this feature prevents. The sidechannel now ACCUMULATES batches; the consumer reconciles every batch and then clears. The queue-dedup is extracted into a pure `removeInjectedFromQueue` helper and unit-tested (the App.tsx path had no test, so this regressed silently).
Hardening and tests:
- Cap mid-turn message length (server, 16 KB) and per-session queue depth (bridge, 20), matching the bounds on the sibling /btw and /prompt; over-cap returns `{accepted:false}` and the browser keeps the message for its next-turn queue.
- Drop the dead try/catch around `EventBus.publish` (never-throws contract — "don't wrap publish()"); check the return value and emit one diagnostic line per non-empty drain.
- Assert the settle-clear: a new test seam exposes the agent-side connection so a test can drive `extMethod('craft/drainMidTurnQueue')` after settle and assert the leftover was cleared (not re-drained next turn), plus the back-to-back FIFO survival case.
* fix(daemon): address Copilot review — trim consistency + doc accuracy
- POST /session/:id/mid-turn-message now length-checks and enqueues the TRIMMED message (it was checking the raw `message.length` while the bridge stores the trimmed value), so whitespace-padded input whose real content fits is no longer rejected.
- Correct the `mid_turn_message_injected` docs (events.ts payload + bridgeClient.ts): it is a transient dedupe signal, not a transcript render — the message reaches the model mid-turn and the persisted transcript shows it on reload.
* fix(daemon): address review — client-ownership gate + per-originator mid-turn dedupe
Addresses the /review main finding and doudouOUC's inline comments on the
web-shell mid-turn drain.
- Authorize the mid-turn endpoint per session (review main finding): the
route now forwards the client id via `parseClientIdHeader` and
`enqueueMidTurnMessage` runs `resolveTrustedClientId` before queuing —
mirrors `/prompt` and `/btw`, so a token-holding client bound to another
session can no longer push into this turn (throws `InvalidClientIdError`).
- Route the drain's SSE echo per originator (doudouOUC #3417739340): the
trusted client id is recorded on each queue entry and the drain publishes
one `mid_turn_message_injected` frame per originator carrying
`originatorClientId`, so a peer on the same session can't dedupe a
coincidentally-equal entry it never queued.
- Wire the web-shell consumer to its own client id: the daemon now stamps
every drained frame, and the web-shell always sends a client id, so
`removeInjectedFromQueue` must filter on it. Plumbed `clientId` onto
`DaemonConnectionState` (set from the bound session) and passed
`connection.clientId` into the dedupe — without this the new filter would
skip every batch, leaving our own messages to be resent next turn (the
exact double-delivery this feature prevents).
- Tests (doudouOUC #3417739347 + coverage for the above): per-originator
publishing, the `published === false` (bus-closed) degradation, the
endpoint ownership gate, end-to-end originator stamping, and the
web-shell originator-filtering matrix (match / peer-skip / anonymous /
mixed / missing-id regression guard).
- Comment-only: point `MAX_MID_TURN_QUEUE_DEPTH` at
`maxPendingPromptsPerSession` as the promotion model (doudouOUC #3417739352).
* fix(daemon): address review round 2 — mid-turn races, route tests, observability
Addresses the qwen3.7-max /review pass on the web-shell mid-turn drain (3
criticals + 6 suggestions).
Criticals
- consume() race (sidechannel): the buffer is read during render but reconciled
in an async effect, so a frame appended in that window was wiped by an
unconditional clear → resent next turn (double delivery). `consume` now does a
compare-and-swap — it only clears if the buffer still holds the exact snapshot
it reconciled; a newly-arrived batch survives to the next reconcile.
- Late-arriving enqueue (web-shell): the fire-and-forget mid-turn POST is now
scoped to a per-turn AbortController, aborted when the turn settles, so a slow
push can't land during a SUBSEQUENT turn and be injected twice. An aborted
push resolves `{ accepted: false }`, so the message just follows its normal
next-turn path.
- HTTP route had zero tests: add a `POST /session/:id/mid-turn-message` suite
(accept, reject, missing/empty/oversized body, unknown session, malformed
client id) plus the fakeBridge wiring it needed.
Suggestions
- Telemetry: register `mid-turn-message` in the daemon route regex so the
endpoint gets a route label / spans / latency like its siblings.
- Single source of truth for `craft/drainMidTurnQueue`: export
`MID_TURN_QUEUE_DRAIN_METHOD` from acp-bridge and import it in both the
answerer (BridgeClient) and the caller (Session.ts), so a rename can't desync
them into a silent -32601 latch.
- Observability: `enqueueMidTurnMessage` now logs idle/empty/full rejects and
the drop-at-settle path (the drain already logged); rejects are low-volume
(the browser only pushes when it believes a turn is live).
- Buffer safety cap (sidechannel): bound the accumulating buffer and evict
oldest so an orphaned consumer can't grow it without limit.
- Tests: depth-cap overflow + trimming (bridge), compare-and-swap + cap
(sidechannel).
* fix(daemon): scope mid-turn consume to reconciled batches; correct originator doc
Addresses the follow-up /qreview pass.
- Cross-session wipe (web-shell): the dedupe reconcile is session-scoped, but
`consume()` cleared the whole accumulating buffer. The buffer is a
cross-session singleton, so a late `mid_turn_message_injected` frame for the
PREVIOUS session (e.g. after an in-place `/resume` switch) was wiped
un-reconciled and lost on switch-back → resent next turn = double delivery.
Replace the blanket clear with identity-removal: `consumeSidechannelMidTurnInjected(handled)`
drops only the batches actually reconciled (the active session's). Batches for
other sessions — and frames that arrived after the render snapshot (the
render→effect race the prior compare-and-swap covered) — are not in `handled`
and stay buffered for their own reconcile. So this subsumes the race fix and
adds multi-session correctness. `consume` is now stable (no per-render churn).
- originatorClientId doc (SDK): the field is declared on
`DaemonMidTurnMessageInjectedData` (the `data` shape) but, unlike the sibling
permission events, this event is not reduced and the daemon never merges the
id into `data` — it rides the SSE envelope (`event.originatorClientId`) and is
lifted into `data` only by the web-shell's own parser. Document that so an SDK
consumer doesn't read an always-undefined `data.originatorClientId` and treat
every batch as anonymous (dedupe-for-all foot-gun).
- Tests: identity-removal across the race, the cross-session leave-behind, and
the already-evicted no-op.
* fix(daemon): mid-turn robustness — fetch timeout, observability, docs, tests
Addresses the latest /review pass (DeepSeek + qwen3.7-max). Stale duplicates of
already-shipped fixes (shared drain-method constant, depth-cap test, consume
cross-session/race) are answered inline; the substantive new items:
- DaemonClient.enqueueMidTurnMessage now routes through `fetchWithTimeout` like
every other method, so a hung daemon can't wedge the void-ed caller in
actions.ts forever. The helper composes the caller's signal with its timeout,
so the turn-settle abort still propagates. (+ propagation and timeout tests.)
- Browser-side observability (mirrors the server-side writeStderrLine added
earlier): the actions catch logs non-abort failures at debug (an abort is the
designed settle cancel, kept silent); the settle-abort and sidechannel buffer
eviction each get a `console.debug`; and a debug warns when stamped batches
arrive but `connection.clientId` is undefined (dedupe would skip them).
- Docs: spell out the `originatorClientId` CONTRACT — a consumer that dedupes
MUST compare it against its own client id (the daemon broadcasts, it does not
route), or it drops another client's coincidentally-equal message.
- Tests: `POST /session/:id/mid-turn-message` InvalidClientIdError → 400; the
SSE event-pump routing of `mid_turn_message_injected` to the sidechannel (not
the transcript); DaemonClient signal propagation + hung-daemon timeout.
|
||
|
|
31281f6a7d
|
feat(acp): dedicated agent permission dialog via _meta.toolName (follow-up to #5085) (#5105)
* feat(acp): carry _meta.toolName on permission frame; agent drawer (vscode) WIP: producer mirrors _meta.toolName onto session/request_permission; webui PermissionDrawer + vscode webview map it to render 'Launch this agent?' for the Agent tool without a protocol kind. Daemon/web-shell surface + tests follow. * feat(web-shell): dedicated agent permission prompt via _meta.toolName Thread the canonical tool name from the permission frame's _meta.toolName through web-shell's PermissionRequest so ToolApproval renders 'Launch this agent?' for the Agent tool, mirroring the vscode PermissionDrawer. Add tests for the toolName extraction and the agent drawer title. * fix(acp): mirror _meta.toolName on second producer path + address review Address @wenshao's review on #5105: - [Critical] SubAgentTracker's approval handler builds its own RequestPermissionRequest (the second producer path, for nested sub-agent tool calls) and was missing `_meta: { toolName }`. Session.ts adds it on the primary path; mirror it here so nested agents (and any future tool relying on _meta.toolName for specialized UI) don't fall back to the generic prompt. Locked with a _meta assertion in the approval test. - Dedupe the three hardcoded 'agent' string matches behind a single shared `AGENT_TOOL_NAME` / `isAgentTool` in @qwen-code/webui (re-exported via daemon-react-sdk, same pattern as DAEMON_APPROVAL_MODES), consumed by PermissionDrawer (webui) and ToolApproval (web-shell). - Move the agent check to the top of PermissionDrawer.getTitle() so it wins over kind-based checks, matching ToolApproval's isAgent-first ordering across the two surfaces. - Extract the _meta.toolName lifting logic in useWebViewMessages into a testable `liftToolNameFromMeta` helper and cover the three cases wenshao flagged: lift onto toolName, preserve a pre-existing toolName, no-op when _meta is absent (plus undefined-toolCall guard). 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) |
||
|
|
9be731ce75
|
feat(cli,web-shell): persist goal status in daemon transcript events (#5098)
Previously /goal state lived only in frontend memory — page refresh or
multi-device sessions lost the active goal. Now the CLI emits goal status
updates as structured daemon events (_meta.goalStatus), which flow through
the transcript as status blocks (source: 'goal', data: {...}). The web-shell
rebuilds goal state from transcript blocks on connect, making goal status
survivable across page refreshes and syncable across devices.
- CLI: emitGoalStatus on goal set/clear, pass outputHistoryItems through
nonInteractiveCliCommands, add setAt to goalCommand output
- SDK: widen DaemonUiStatusEvent source/data types, preserve them in
transcript blocks
- webui: normalize _meta.goalStatus in DaemonSessionProvider, replace
sentinel-prefix text encoding with structured data
- web-shell: derive activeGoal from transcript blocks (getLatestActiveGoalFromBlocks),
remove optimistic client-side goal dispatch, parse structured goal data
in GoalStatusMessage/SystemMessage
- Tests: cover emitGoalStatus, outputHistoryItems passthrough, transcript
block serialization, DaemonSessionProvider event conversion
- Also: harden McpDialog restart result type check with isRestartEntriesResult
Co-authored-by: ytahdn <ytahdn@gmail.com>
|
||
|
|
8472c6fcea
|
fix(webui): defer DaemonClient disposal to survive React StrictMode (#5091)
Under StrictMode (dev default), DaemonWorkspaceProvider's useEffect cleanup called client.dispose() synchronously, destroying the memoized DaemonClient that the second effect invocation reused. This left the transport closed before the session provider could attach, surfacing as "Transport connection closed" and a permanent "Loading..." / disconnected state in the web-shell. Defer disposal by one microtask. StrictMode's synchronous re-mount cancels the pending disposal before the microtask fires, preserving the shared client. Real unmounts and client replacements still dispose normally since no cancellation occurs in those paths. |
||
|
|
ce4b0cf629
|
feat(sdk,serve): DaemonTransport abstraction + ACP standard compliance (#5040)
* feat(sdk): DaemonTransport abstraction — pluggable transport for REST/ACP-HTTP/ACP-WS
- DaemonTransport interface with fetch + subscribeEvents
- RestSseTransport: extract current SSE logic from DaemonClient
- AcpWsTransport: WebSocket multiplexer + URL-to-JSON-RPC mapping
- AcpHttpTransport: POST /acp + session-scoped SSE
- AcpEventDenormalizer: JSON-RPC notification -> DaemonEvent
- AutoReconnectTransport: opt-in reconnect + fallback wrapper
- negotiateTransport(): auto-detect best transport via GET /capabilities
- Provider: DaemonWorkspaceProvider gains transport prop
- Server: GET /capabilities advertises supported transports
- Zero breaking changes: no transport = current REST behavior
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* docs(design): include DaemonTransport design doc in implementation PR
* fix(sdk): address 6 verification findings — bundle size, WS hang, error types, ACP compat
- Remove ACP transport class re-exports from barrel (index.ts) to avoid
~19.7KB browser bundle bloat; keep type-only exports
- Fix WS dial hang: reject connect promise in onerror when not yet
connected (Node WebSocket may only fire error, not close)
- Fix parked generators: maintain _activeGenerators set, abort all on
WS close so generators throw DaemonTransportClosedError
- Forward abort signal through AcpHttpTransport.sendRequest to fetch
- Restore DaemonHttpError in RestSseTransport (was plain Error)
- ACP endpoint compat: extract connectionId from initialize, send
Acp-Connection-Id header, add _qwen/ prefix for vendor methods,
preserve real HTTP status in error mapping, fetch /capabilities
from REST endpoint for correct shape
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk): address 16 review findings + CI bundle size
- CI: move negotiateTransport to separate file, extract DaemonHttpError
to break static import chain from barrel -> DaemonClient. Browser
bundle drops from 136KB to 115KB, under the 116KB budget.
- Route table: extract shared acpRouteTable.ts, used by both transports.
Unify method naming (remove _qwen/ prefix inconsistency).
- Token: move from URL query to Authorization header on WS upgrade
- Error type: DaemonHttpError extracted to DaemonHttpError.ts; import
in RestSseTransport no longer pulls in DaemonClient.
- Init retry: reset failed initPromise so next call retries
- Reconnect mutex: prevent concurrent reconnect storms
- Generator queue: cap at 256, drop-oldest
- WS init timeout: 30s default
- negotiate: clear timer on all paths, catch dispose rejection
- Headers: forward init.headers in ACP transports via mergeHeaders()
- Dead code: remove unused pendingRequests/sseAbort fields
- Provider: dispose client on unmount
- Helpers: extract matchRoute/synthesizeResponse/jsonRpcErrorToHttpStatus/
isRecord/composeAbortSignals to shared acpTransportUtils.ts
- Package exports: add deep import paths for ACP transports
- Tests: add AcpEventDenormalizer unit tests (17 cases)
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk): ESLint array-type rule — ReadonlyArray<T> → readonly T[]
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk): fix 3 ACP wire bugs + bundle size + npm exports
Wire bugs (verified broken against real daemon):
1. AcpHttpTransport: read connectionId from response header + correct JSON path
2. AcpWsTransport: send token via Authorization header, not URL query
3. AcpEventDenormalizer: read params.update.sessionUpdate, not params.type
Bundle: remove negotiateTransport from barrel-reachable imports
Exports: add package.json deep import paths for ACP transports
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(sdk): comprehensive ACP transport test suite (~175 tests)
- RestSseTransport: fetch delegation, SSE subscribe, auth, timeout, signal
- AcpWsTransport: route mapping, token auth, event filtering, queue cap
- AcpHttpTransport: connectionId extraction, header injection, init retry
- AutoReconnectTransport: reconnect mutex, fallback, delegation
- negotiateTransport: capability probing, timeout, fallback
- acpRouteTable: URL→method mapping, param extraction
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk): route table coverage, browser WS auth, header forwarding, capabilities type
- Route table: add file/stat/list/glob/write/edit paths (all DaemonClient URLs)
- Route table: add session diagnostic routes (context, tasks, stats, rewind, language)
- Route table: add bulk sessions/delete
- WS auth: document browser limitation, Node uses headers, browser needs proxy
- Headers: forward X-Qwen-Client-Id via JSON-RPC _meta in WS transport
- DaemonCapabilities: add transports field to SDK type
- Package exports: remove unreachable deep exports, document monorepo usage
- Provider bypass: document limitation for glob/stat/list in workspace actions
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk): add missing detach + hooks routes per QA doc
Cross-referenced with daemon-acp-integration-qa.md route table.
Added POST /session/:id/detach and GET /session/:id/hooks.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(serve,sdk): enforce ACP standard session/new — always isolated session
ACP standard mandates session/new MUST create a new isolated session.
Server-side (dispatch.ts):
- Force sessionScope='thread' on /acp session/new, ignoring client params
- REST POST /session retains 'single' default for backward compat
SDK-side (acpRouteTable.ts):
- Strip sessionScope from session/new params in ACP transports
- Document that ACP follows the standard (no extensions)
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(serve): ACP session/new returns standard models/modes fields
ACP standard NewSessionResponse includes optional `models` and `modes`
top-level fields alongside `configOptions`. Extract model/mode state
from configOptions and surface them as standard-shaped objects:
- models: { currentModelId, availableModels: [{id}] }
- modes: { currentModeId, availableModes: [{id}] }
Also update test to verify sessionScope is always forced to 'thread'
(ACP standard compliance — session/new always creates isolated session).
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(serve): add standard ACP methods session/set_mode, session/set_model, session/fork
Align /acp endpoint with ACP standard protocol:
- session/set_mode: dedicated method for mode changes (standard)
Maps to bridge.setSessionApprovalMode(). Params: {modeId, sessionId}
- session/set_model: dedicated method for model changes (unstable)
Maps to bridge.setSessionModel(). Params: {modelId, sessionId}
- session/fork: create a branched copy of an existing session
Maps to bridge.branchSession(). Response includes configOptions,
models, modes per ACP standard.
- session/load, session/resume: responses now include configOptions,
models, modes (per ACP LoadSessionResponse/ResumeSessionResponse)
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(serve): TS2345 — pass persist: false to setSessionApprovalMode
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(webui): add dispose() to MockDaemonClient in provider tests
DaemonClient now has dispose() (called in provider cleanup effect).
Mock clients in test files need to implement it.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(serve): add sessionId pre-validation + remove type assertion
- session/set_mode, session/set_model: add explicit sessionId empty
check before requireOwned (consistent with session/fork)
- session/set_model: remove `as unknown as` type assertion, pass
proper {modelId, sessionId} matching SetSessionModelRequest
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk,serve): align route table with dispatcher + AcpHttp SSE response correlation
Route table:
- Add _qwen/ prefix to all vendor session/workspace methods
- Split workspace catch-all into granular dispatcher methods
- Fix session/branch → session/fork, model → session/set_model
- Remove routes with no dispatcher handler
AcpHttpTransport:
- Implement conn-scoped SSE stream for response correlation
- POST returns 202 (ack), real response rides SSE stream
- Map<id, {resolve, reject}> for pending request correlation
dispatch.ts:
- Remove session/set_mode, session/set_model from CONN_ROUTED_METHODS
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk): bump browser bundle budget 116KB→118KB for transport abstraction
Main uses 117,753 bytes (99.1% of 116KB budget). The transport
abstraction adds ~1.5KB (DaemonTransport interface + RestSseTransport
default constructor in DaemonClient). Bump to 118KB (120,832 bytes).
Also change RestSseTransport to type-only export from barrel (class
is constructed internally by DaemonClient, not needed as a value
export for consumers).
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk): fix 2 test failures — SSE error message + workspace catch-all route
- RestSseTransport: error message 'SSE response has no body' → 'No SSE body'
(matches existing DaemonClient.test.ts assertion)
- acpRouteTable: re-add GET/POST /workspace/* catch-all after granular routes
(AcpWsTransport.test.ts expects generic workspace path to resolve)
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk): align RestSseTransport test with updated error message
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
||
|
|
c61006b978
|
feat(web-shell): daemon web-shell improvements — token usage, settings, retry, streaming metrics, hidden commands (#5066)
* feat(web-shell): daemon web-shell improvements - Align daemon token usage with structured DaemonTokenUsage type - Optimize settings panel with i18n, theme/language pickers, compact mode - Handle missing session recovery (404/410) with configurable behavior - Restore settings event signal bump for workspace changes - Prevent queued prompt loss on useEffect dependency change - Align streaming loading indicator with CLI metrics logic - Add Ctrl+Y retry for turn_error with daemon support - Hide non-essential UI elements on narrow screens (≤700px) - Prevent loading indicator flicker on page refresh - Hydrate displayName from persisted session title on load * fix(web-shell): harden retry affordance * fix(web-shell): gate retry handling --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |