mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-22 07:04:58 +00:00
6205 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
acb0275ecd
|
fix(serve): Add prompt queue backpressure (#5033)
* fix(serve): add prompt queue backpressure Add per-session prompt admission limits across the bridge, REST and ACP entrypoints, and SDK clients. The server now rejects full prompt queues before returning accepted semantics, advertises the active limit through capabilities, and documents the behavior with focused tests and design artifacts. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(sdk): stabilize pending prompt cleanup Close the mocked SSE stream explicitly in the pending prompt cap test so cleanup does not rely on abort-driven stream cancellation timing in CI. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(sdk): stabilize subscription prompt race Reject accepted subscription prompts if the event stream has already ended, and make the prompt-cap tests wait for the pending registration before closing or injecting SSE frames. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk): map prompt queue full responses Map server-side prompt_queue_full responses to DaemonPendingPromptLimitError for both blocking and non-blocking prompt calls, include the session id in the local limit error, and cross-reference the duplicated default prompt cap constants. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test: keep qwen planning docs ignored Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): address prompt backpressure review Log synchronous prompt queue rejections, document the sync admission contract, clean up SDK prompt-slot release, and cover the reviewed backpressure edge cases. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk): restore daemon bundle budget headroom Reduce the generated daemon client bundle slightly and raise the browser daemon SDK bundle budget to 116 KiB so the PR merge ref has practical headroom. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
84d01e7070
|
feat(web-shell): show message time on hover (#5079)
* fix(acp-bridge): preserve original timestamp when replaying session history History replay re-emits each persisted record with its original epoch-ms time nested in update._meta, but BridgeClient.sessionUpdate published the frame without lifting it to the envelope. EventBus.publish then stamped envelope _meta.serverTimestamp with publish-time Date.now(), which the client's extractServerTimestamp picks up at higher priority than the nested original — so a resumed session rendered every historical message at the resume moment instead of when it was sent. Lift update._meta.timestamp (or serverTimestamp) to the envelope serverTimestamp so EventBus preserves it. Live updates without such a timestamp keep the Date.now() fallback unchanged. * feat(web-shell): show each history message's time on hover Carry each transcript block's wall-clock time (serverTimestamp ?? clientReceivedAt) onto every message and reveal it as a CSS-only hover tooltip in the message list. Same-day messages show HH:mm:ss; older ones show yyyy-MM-dd HH:mm:ss (local time, zero-padded). |
||
|
|
aebf82cd29
|
fix(web-shell): improve slash command panel layering (#5078)
Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
b748ef4b73
|
feat(web-shell): revamp floating todo panel interactions (#5069)
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
The "Current tasks" panel above the composer was a static display:
always expanded, rotate-to-front ordering with jumbled numbering,
no progress summary, and it vanished the instant the last item
completed.
- Collapsible header (persisted in localStorage); collapsed mode is a
single line showing progress + the current in-progress item
- Progress counter (completed/total) in the header
- Natural-order window anchored on the in-progress item replaces the
rotation: one completed context line above, pendings below, with
clickable "N completed" / "N more" summary lines that expand the
full list (and "Show less" to return)
- All-done moment: a finished list stays visible as "All tasks
completed" until the next user prompt instead of disappearing
instantly; historical finished lists stay hidden on session restore
- Locate button scrolls the transcript to the source TodoWrite/plan
message with a flash highlight (new MessageList imperative
scrollToMessage, callId fallback for compact-merged tool groups)
- Visual consistency: in_progress uses the accent color, PlanMessage
adopts the shared icon set, items ellipsize to one line with a
hover tooltip, and the number column scales past 9 items so the
status icons stay aligned
getFloatingTodos moves to utils/todos.ts and now reports
{todos, allCompleted, sourceMessageId, sourceCallId}; panel visibility
is a render-time state machine so the active-to-completed transition
does not unmount the panel for a frame. New i18n keys for en/zh-CN
and 17 new unit tests.
|
||
|
|
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> |
||
|
|
66c69865c7
|
fix(core): preserve background agent launch flags (#5061)
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> |
||
|
|
44627a24be
|
feat(mcp): project .mcp.json + workspace approval gating with aligned scope precedence (#4615) (#4713)
* feat(mcp): project .mcp.json + workspace approval gating with aligned scope precedence (#4615) Adds untrusted-source approval gating for MCP servers and a coherent cross-source precedence model. Sources & precedence (low -> high): user/default settings < project .mcp.json < workspace/system settings < session(ACP/IDE) < --mcp-config - Load project servers from .mcp.json (pure read, never connects), tagged scope:'project'. - Tag workspace/system settings servers with provenance scope at merge time so the winning entry keeps its source; centralize assembly in assembleMcpServers. - Gate checked-in/shareable sources (project + workspace) behind a hash-bound approval store; .mcp.json edits revert approval to pending. system/user/CLI/ extension/session sources are never gated. - .mcp.json now overrides USER settings (Claude parity) but never enterprise 'system' settings. - Route ACP/IDE-injected servers through a top-tier sessionMcpServers param so a repo .mcp.json can't override or gate them. - Startup approval dialog + 'qwen mcp approve|reject' + 'qwen mcp list' cover both gated sources; non-interactive sessions auto-approve (lenient). Co-Authored-By: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): cover mcp scope stamping * fix(mcp): harden approval binding * fix(mcp): harden approval store persistence * fix(mcp): address approval review feedback * refactor(cli): rename gated MCP approval helper * fix(mcp): surface approval metadata in prompts * docs(mcp): clarify pending approval snapshot * fix(mcp): persist prototype-named approval records * test(mcp): cover pending approval guard paths * chore(mcp): refresh approval gating checks * fix(mcp): enforce approval gate outside interactive * fix(cli): label MCP server sources accurately * fix(cli): include project MCP servers in reconnect * docs(cli): correct MCP approval noninteractive note --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
098367a485
|
refactor(web-shell): remove duplicate agents panel, contain SubAgent views (#5059)
* refactor(web-shell): remove duplicate agents panel, contain SubAgent views - Remove ActiveAgentsPanel above the status bar: the SubAgentPanel in the message history is mouse-operable and shows the same data. Drop its focus chain (EditorHandle.blur, onFocusActiveAgents renamed to onFocusFooter) and the activeAgents i18n strings. - Cap completed SubAgent results and the Tools tab in 400px scroll windows (same cap as the expanded live stream) so a panel never grows past one screen; the tools window follows the newest call while running and snaps back to the top on completion. Gated on compactThinking, so hosts without it are unchanged. - Reuse the transcript ToolLine for sub-tools so they collapse and expand with the same detail views (bash output, diffs, file content) instead of a fixed one-line summary. * fix(web-shell): drop duplicate total from parallel-agents header The header rendered "Parallel agents · 9 · 9/9 done" — the standalone total repeats the denominator already shown in the done counter. Keep "Parallel agents · 9/9 done". |
||
|
|
662197e3fb
|
fix: enable fork subagents by default (#4963)
* fix: enable fork subagents by default * fix: respect fork subagent config flag |
||
|
|
f5e512e6e6
|
feat(serve): deliver A2UI surfaces over MCP — bridge extraction and action endpoint (#4961)
* feat(serve): A2UI over MCP — bridge extraction and action endpoint
Deliver Google A2UI (v0.9) surfaces from MCP tool results to web clients,
with zero changes to core / ACP schema / tool registry:
- acp-bridge BridgeClient.sessionUpdate: detect a2ui UI-server tool results
(by _meta.serverId containing "a2ui", tool-name fallback), extract the
leading A2UI command array (core flattens EmbeddedResource to text and
drops the application/a2ui+json mime), publish a separate
sessionUpdate:'a2ui' frame {surfaceId, callId, commands} onto the event
bus (journal/replay included), and sanitize the original tool frame so
raw command JSON never reaches transcripts.
- serve POST /session/:id/a2ui-action: proxy user actions on A2UI surfaces
to the UI MCP server's standard `action` tool (per the official
A2UI-over-MCP guide). UI server discovery prefers the live workspace MCP
status (covers runtime-registered servers) and falls back to workspace
settings; stdio and streamableHTTP transports supported. Continuation
frames are returned synchronously as {commands, fallback}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(serve): address review — robustness, error mapping, and tests for A2UI
Review fixes for #4961:
- connect/callTool now both carry a 15s timeout so an unresponsive UI MCP
server cannot hang the HTTP request.
- callTool results with isError are surfaced as failures (502) instead of a
200 with null commands; the error detail is logged server-side only and the
client receives a generic message (no internal paths/URLs leak).
- transport.close() is called alongside client.close() so a stdio child
spawned by a half-failed connect cannot leak.
- stdio env is merged over process.env (spawn treats env as a full
replacement; a partial env would strip PATH/HOME), matching mcp-client.ts.
- array-valued `context` bodies are rejected instead of forwarded; settings
fallback is async (fs/promises); dead `url` config field removed (legacy
SSE intentionally unsupported); license header aligned.
- multi-surface tool results are now split into one a2ui frame per surface
(first-appearance order) instead of publishing only the first surfaceId;
multiple a2ui+json blocks keep explicit first-wins semantics.
- extraction/detection helpers are exported and covered by unit tests
(22 cases: balanced-array parser edge cases incl. nested arrays/escaped
quotes/unbalanced brackets, detection by serverId/tool name, grouping,
sanitization, endpoint validation/discovery/fallback/error mapping).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(serve): cover A2UI action result extraction
* fix(acp): sanitize unrecognized a2ui updates
* test(serve): cover a2ui action transport lifecycle
* test(core): import mocked session root context
* test(acp-bridge): cover A2UI session update publishing
---------
Co-authored-by: 衍星 <qiuyusheng.qys@alibaba-inc.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8000342667
|
fix(core): remove unused debugResponses array and dead extractUsageFromGeminiClient (#4982) | ||
|
|
92c4a82390
|
fix(memory): avoid stale tool schema recall (#5058)
* fix(memory): avoid stale tool schema recall Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> * fix(memory): seed resumed tool recall context Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> --------- Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> |
||
|
|
2be01a104e
|
fix(daemon): Sanitize logs and type MCP restarts (#5006)
* fix(daemon): Sanitize ACP delete logs and type MCP restarts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(daemon): Cover PR review edge cases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(web-shell): Show MCP restart entry failures Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(daemon): Harden ACP delete log sanitization Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(daemon): Fix ACP log sanitizer lint Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
3a224d1efe
|
feat(skills): support user-invocable frontmatter (#5037) | ||
|
|
d3cded95f7
|
chore: sync package-lock.json with packages/cli ws dependencies (#5023)
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
|
||
|
|
233e8e0caa
|
feat(core): let grep results satisfy prior-read checks (#5043) | ||
|
|
4ae788623e
|
feat(core,cli): bubble background subagent permission prompts to the parent session (#4955)
* feat(core,cli): bubble background subagent permission prompts to the parent session Background subagents auto-deny any tool call that needs interactive confirmation, so a single permission-gated step (a git push, an rm, a network call) silently fails and the work bounces back to the parent turn — defeating the point of backgrounding. This adds an opt-in approvalMode value for subagent definitions, `bubble`: instead of denying, the call is parked on the BackgroundTaskRegistry and surfaced in the Background tasks dialog, where the user answers it through the shared ToolConfirmationMessage; the agent then resumes. - `bubble` is a subagent-only approvalMode (deliberately not a session-level ApprovalMode value); it resolves to `default` run behavior and only flips the background path from deny to surface, in interactive sessions. Headless / non-interactive contexts keep auto-deny. - BackgroundTaskRegistry grows a parked-approval queue (add/resolve/clear), an approval-change callback, and an event bridge (TOOL_WAITING_APPROVAL parks, TOOL_RESULT clears stale prompts). Every terminal transition auto-rejects parked calls so the agent loop never hangs on an unanswerable prompt; cancel() rejects before aborting so respond(Cancel) actually fires ahead of the abort-driven queue clear. Auto-reject failures are caught on the promise, not via try/catch around a voided async call. - The launch path (agent.ts) and the resume path (background-agent-resume.ts) share the same gate, so a resumed agent of the same definition keeps bubbling instead of silently reverting to auto-deny. - TUI: the footer pill shows a "needs approval" marker, dialog list rows are flagged, and the detail view embeds the confirmation prompt. While a prompt is up, left (back) and x (stop agent) remain available as escape hatches so a re-parking agent cannot trap the keyboard. Closes #4928 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(i18n): add zh-TW translations for background approval strings check-i18n requires every zh key to have a zh-TW counterpart; the three strings added for permission bubbling were registered in en/zh only. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): harden background approval edge cases from review - resolvePendingApproval: if respond() rejects, the tool call is still parked in the scheduler, so re-add the approval and re-emit instead of silently clearing the prompt (which left the UI showing nothing pending while the agent hung). Returns false on failure. - reset() and finalizeCancellationIfPending(): reject parked approvals defensively. The /resume and /clear paths already gate on hasBlockingBackgroundWork() so these only run on terminal entries today, but rejecting here means a future caller dropping that guard can't strand an unanswered respond() callback. - resolveSubagentApprovalMode: resolve the subagent-only 'bubble' mode to Default explicitly rather than via approvalModeToPermissionMode's default fall-through, so a future ApprovalMode.BUBBLE enum member can't silently change it. Adds tests for the fail / finalizeCancelled / reset auto-reject paths and the respond()-rejection re-park. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): include args in approval test events * test(core): update nested yaml parser expectations * fix(cli): reuse selected background agent id * fix(core): fail consumed background approval retries * fix(core): prevent persistent bubbled approvals * fix(core): harden bubbled approval handling * fix(core): cover background approval edge cases * fix(cli): isolate bubbled question approval keys * fix(cli): localize background approval labels --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
7cb95bebbd
|
fix(docs): update Coding Plan model list and fix stale references in developer docs (#5054)
* fix(docs): update Coding Plan model list and fix stale references in developer docs - model-providers.md: expand Coding Plan model table from 3 to 9 models (add qwen3.6-plus, glm-5, kimi-k2.5, MiniMax-M2.5, qwen3-coder-next, glm-4.7) to match the provider registry - auth.md: update Coding Plan model list to match all 9 models and use the correct date-stamped model ID (qwen3-max-2026-01-23) - contributing.md: fix Node.js version requirement from 18+ to 22+ to match package.json engines - sdk-typescript.md: fix nonexistent tool name run_terminal_cmd → run_shell_command - integration-tests.md: fix file extension from .test.js to .test.ts to match actual test files - qwen-serve-protocol.md: fix legacy tool name search_file_content → grep_search and remove nonexistent ripgrep tool reference * fix(docs): add missing qwen3.7-plus to Coding Plan model docs MODELSTUDIO_MODELS in alibaba-coding-plan.ts defines 10 models, but the inline list in auth.md and the table in model-providers.md only listed 9, omitting qwen3.7-plus (1M context, thinking enabled). Add it after qwen3.6-plus in both, matching the source order. |
||
|
|
a99b01b020
|
feat(core): persist oversized tool results to disk (#4095 Phase 4) (#5042)
* feat(core): persist oversized tool results to disk (#4095 Phase 4) Large tool outputs (>28K chars) are now saved to disk as tool-results/<callId>.txt and replaced with a 2KB preview stub in the LLM context, preventing OOM and context pollution. Key mechanics: - Triple-skip gate: read_file exempt → already-truncated skip → threshold+3K headroom - Budget: 50MB per-file cap, 500MB per-session cumulative (Buffer.byteLength) - Security: atomicWriteFile with mode 0o600, noFollow, forceMode; path.basename sanitization - Cleanup: 24h expiry on startup and /clear - Error branch: large stderr also persisted - Fallback: budget exhausted → preview-only stub; write failure → legacy truncateAndSaveToFile * fix(core): suppress no-control-regex lint for null-byte sanitization The \x00 regex is intentional security hardening to strip null bytes from callIds before using them as filenames. * fix(core): address wenshao review round 1 - Fix mock Config in coreToolScheduler.test.ts: add getToolResultBytesWritten, trackToolResultBytes, and storage.getToolResultsDir to all 4 mock instances - isAlreadyTruncated: change includes to startsWith for <persisted-output> to avoid false positives from tool output containing the literal string - Remove dead code: recalcContentLength function and its call site (contentLength is unconditionally overwritten downstream) - buildStub: non-persisted stubs no longer use <persisted-output> tag to avoid misleading model into wasted read_file calls - Add GATE_HEADROOM rationale comment * fix(core): update prompts.test.ts snapshots for persisted-output guidance The new <persisted-output> model guidance added to prompts.ts changed the system prompt output, requiring snapshot updates. |
||
|
|
a283ca0479
|
fix(telemetry): Propagate daemon ACP trace context (#5047)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
a064779e2e
|
feat(daemon): gate direct session shell behind explicit opt-in (#5031)
* feat(cli): gate direct session shell execution * fix(cli): address session shell review feedback * codex: address PR review feedback (#5031) |
||
|
|
e66281590d
|
chore(release): v0.18.0 [skip ci]
* chore(release): v0.18.0 * docs(changelog): sync for v0.18.0 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
c2962eef73
|
fix(release): allow fzfWorker.js in standalone dist allowlist (#5049)
esbuild emits dist/fzfWorker.js as a standalone entry next to cli.js, but create-standalone-package.js's DIST_ALLOWED_ENTRIES did not list it, so 'Build Standalone Archives' failed with 'Unexpected dist asset'. prepare-package.js already whitelists it for the npm tarball; this syncs the standalone packer. |
||
|
|
78f063517a
|
feat(acp): broadcast session title updates to daemon clients (#5035)
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
* feat(acp): broadcast session title updates to daemon clients * test(cli): update session worktree chat recorder mock |
||
|
|
fa684552b0
|
fix(test): unbreak qwen serve integration suites after daemon batch merge (#5041)
Three integration tests have failed every nightly Release and E2E run since the daemon-mode feature batch (#4490) merged, because these suites only run post-merge: - routes: resync the capabilities envelope baseline with the features the batch added (verified against a live daemon), and strip the env toggles that flip conditional tags so the exact-equality assertion is hermetic on dev machines. - baseline: the 2xN MCP grandchildren tripwire fired as designed — the workspace MCP pool eliminated the bootstrap/session duplicate discovery. Assert exactly N pooled children and cross-check the pool's per-server accounting against pgrep. - streaming: the permission test could finish with its turn still blocked on a second permission request nobody would ever answer; the abandoned request wedges the shared session's prompt FIFO and the downstream Last-Event-ID resume test times out waiting for a turn_complete that never comes (reproduced empirically). Pin the session to default approval mode (hermetic vs host user settings) and cancel the possibly-in-flight turn before finishing. The daemon-side wedge (abandoned permission request blocks the FIFO until an explicit cancel) is real beyond tests and tracked separately. |
||
|
|
e07d069720
|
fix(stats): dedup usage records by sessionId and skip in-progress writes (#4995)
* fix(stats): dedup usage records by sessionId and skip in-progress writes (#4994) Opening /stats during the first-ever turn followed by /clear (or exit) used to write the same sessionId twice into ~/.qwen/usage_record.jsonl, permanently inflating every aggregate (sessions / tokens / durations / tools / heatmap / projects) 2x for that session. Closes #4994. Defense in depth: - Read side: loadUsageHistory dedups records by sessionId (last-wins), so any duplicates already on disk from this bug stop inflating future aggregates. - Write side: rebuildFromSessionJsonl skips the in-progress session when its sessionId is passed in; statsDataService threads currentSession.sessionId through. New duplicates are no longer created at the source. Regression coverage in usageHistoryService.test.ts mirrors the exact bug sequence (open /stats during first turn -> /clear -> re-open /stats) and asserts sessionCount=1, totalTokens=1600 for a single ~1.6k-token session. * fix(stats): address PR review — log dedup count and rename rebuild-skip param Why: - dedupBySessionId silently dropped duplicate records, making it impossible to observe how many users were affected by the #4994 bug. Now logs the removed count at debug level. - The new loadUsageHistory parameter was named currentSessionId but only controls the write-side skip during rebuildFromSessionJsonl — the read path ignores it. Renaming to skipSessionInRebuild makes the limited scope explicit; statsDataService still strips/re-pushes the live current session for defense-in-depth. PR review feedback on #4995. |
||
|
|
9895decdbe
|
test(i18n): raise timeout for slow must-translate locale suites (#5024)
The must-translate locale coverage tests switch locales and build the full built-in command tree (loadCommands runs twice per strict-parity locale), which triggers dynamic locale imports plus command construction. On cold Windows CI runners this intermittently exceeds vitest's default 5s per-test budget and times out (zh-TW / zh-CN strict-parity cases), while ubuntu and macOS pass. The test logic is unchanged. Give the three locale-iterating it.each blocks an explicit 20000ms timeout, matching the convention already used by the sibling i18n suite (index.test.ts). |
||
|
|
bc2a5cfbb7
|
fix(core): support .toml command files in extension command discovery (#5017)
* fix(core): support .toml command files in extension command discovery
loadCommandsFromDir only globbed for **/*.md, causing extensions like
caveman that ship .toml commands to have their commands silently ignored
during installation and loading. The CLI-layer FileCommandLoader already
supports both formats, but the core discovery function did not.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(extension): address CR comments on .toml command discovery
- Merge two separate glob calls into single **/*.{md,toml} pattern
- Fix Windows path separator regression: use /[/\\]/ instead of path.sep
- Remove Set dedup in loadCommandsFromDir so consent UI shows true count
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(extension): add colon sanitization test for command names
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(extension): add ENOENT branch coverage for missing commands directory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
4363d58758
|
fix(core): serialize team task claims per agent and add mailbox lock parity (#4981)
The auto-claim busy-check had a TOCTOU: claimTask read isAgentBusy before taking the per-task lock, and the racing claims (scanIdleAgentsForTasks vs a message flush, for the same idle agent) held different task locks — so moving the check inside the per-task lock couldn't close it. Both passed the stale read and the agent ended up owning two in_progress tasks, breaking the one-task-per-agent invariant the UI and auto-claim rely on. Add a per-agent claim mutex (keyed by agentId) around the busy-check + claim so the second claim observes the first's committed write and bails. Distinct agents never block each other; the loser refuses on its next iteration. Separately, bring tasks.ts to parity with mailbox.ts's locking: an in-process per-file mutex (withTaskFileLock) wraps every task-file lock site so same-process writers queue in memory instead of stampeding the OS lockfile (the Windows ELOCKED cause — most acute when up to MAX_TEAMMATES claimants race the same first-pending task), plus randomize:true jitter on the retry backoff. Acquisition order is always agent-mutex → file-mutex → OS lock; only claimTask nests the two, and dependency cycles (which the reverse order would need) are rejected, so no deadlock. Also give the reciprocal edge-mirror writes a RECIPROCAL_CALLER sentinel instead of an empty callerName, so the intentional ownership-guard bypass is greppable; it can't collide with a sanitized [a-z0-9-] agent name. Regression tests cover the per-agent serialization (mutation-verified: fails with double-ownership when reverted), cross-agent same-task contention, and the sentinel bypass. |
||
|
|
5854e2832b
|
fix(tui): Tighten message and tool spacing (#4595)
* fix(tui): Tighten message and tool spacing * docs(tui): Add spacing density evidence * docs(tui): Use upstream spacing evidence references * fix(tui): tighten inter-block spacing and add composer separator - Set marginTop=0 for user and gemini message types to eliminate blank lines between Q&A turns and tool calls - Add half-block (▄) separator line above Composer input area, replacing the full blank row with a subtle blended color line - Add color-utils helpers: interpolateColor, supportsTrueColor - Add HalfLinePaddedBox component (reusable, currently used by Composer only) - Add design doc for TUI spacing density PR2 Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): use dimColor for user message half-line separators Replace the interpolated purple band color with terminal-native dimColor rendering for ▄/▀ half-line characters. This avoids theme-dependent color mismatches while preserving the visual spacing effect. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): refine spacing and add subtle user message band - User message: three-layer seamless band using subtleBandColor (6% brightness shift, no hue change) with ▄/content-bg/▀ - gemini type: restore marginTop=1 for thinking→output gap - Thinking text: trimEnd() to avoid double blank lines - Composer: restore marginTop=1, remove separator line - Add subtleBandColor() helper to color-utils Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): address review findings for spacing PR - Add useIsScreenReaderEnabled() guard in UserMessage to skip decorative half-block characters for screen reader users - Add width <= 0 guard to prevent RangeError on narrow terminals - Remove unused HalfLinePaddedBox component (dead code) - Cache supportsTrueColor() result at module scope - Add unit tests for interpolateColor, subtleBandColor, supportsTrueColor Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): add marginTop fallback for non-truecolor user messages When the half-line band is unavailable (non-truecolor terminal, screen reader, invalid width), restore marginTop=1 on the plain PrefixedTextMessage so user messages don't become adjacent to preceding content. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): respect NO_COLOR for user message band Skip half-line band rendering when theme.background.primary is empty (NoColorTheme sets it to ''), preventing decorative characters from appearing in NO_COLOR environments. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore: remove accidentally committed local data files Remove .dataworks/ and .qwen/skills/data-consistency-analysis/ that were accidentally included in the merge commit via git add -A. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): update design doc and add bright color variants - Update PR2 design doc to match implementation: 6% brightness shift (not 15% accent blend), remove Composer separator section - Add 8 bright Ink color variants to INK_NAME_TO_HEX for completeness Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(tui): always use marginTop=1 on UserMessage fallback path When subtleBandColor() fails to compute a color, the fallback PrefixedTextMessage now always gets marginTop=1 regardless of the useBand flag, preventing user messages from becoming flush against preceding content. 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> |
||
|
|
b794d64fee
|
fix(cli): join previous line when Ctrl+U pressed at column 0 (#5011)
* fix(cli): join previous line when Ctrl+U pressed at column 0 When cursor is at column 0 and not on the first line, kill_line_left (Ctrl+U) now joins the current line with the previous line instead of doing nothing. This mirrors the behavior of kill_line_right at end of line and backspace at column 0, matching Claude Code's behavior. Fixes #4985 * test(cli): add undo test for kill_line_left join operation Add test verifying that undo after a kill_line_left join restores the original two-line state with correct cursorRow, cursorCol, and preferredCol. Co-authored-by: qwen-code-ci-bot --------- Co-authored-by: 俊良 <zzj542558@alibaba-inc.com> |
||
|
|
04412163ab
|
fix(desktop): allow unsigned Windows auto-updates (#5028) | ||
|
|
5121c6563e
|
perf(desktop): add --cli-only flag to skip non-CLI packages during vendor build (#5025)
The desktop vendor step only needs the CLI bundle, but was building all 14 workspaces including webui, sdk, web-shell, and vscode-ide-companion. This wasted ~30-40% of build time and triggered TS type errors in vscode-ide-companion on newer Node.js versions. Add a --cli-only flag to scripts/build.js that truncates the build order after the CLI package. vendor-qwen-code.ts now passes this flag when building from a local source checkout, so both local brand builds and CI desktop-release (source_branch mode) benefit automatically. |
||
|
|
3c55295f63
|
docs(desktop): use main for brand builder skill (#5021) | ||
|
|
c491f33928
|
feat(core): add enter_plan_mode tool and Plan Approval Gate (#4853)
* feat(core): add enter_plan_mode tool and Plan Approval Gate
Allow the model to proactively enter plan mode when tasks are complex or
under-specified, and add a 3-agent design review gate for AUTO/YOLO modes
so autonomous plan exit goes through a structured checkpoint before
restoring execution privileges.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): address code review findings for plan gate
- P0: populate EvidenceBundle.originalRequest and researchSummary from
new exit_plan_mode params (originalRequest, researchSummary)
- P1: extract cap-escalation option labels to shared CAP_ESCALATION_LABELS
constants used by both the gate orchestrator and AskUserQuestion
- P1: check signal.aborted between gate agent retries to bail early on
user cancellation
- P2: Session.ts mode notification now compares before/after approval mode
instead of assuming any non-error result means the mode changed
- P2: add parseGateAgentResult + formatEvidence unit tests (11 cases)
- P3: explicitly exempt enter_plan_mode in isPlanModeBlocked instead of
relying on the default 'info' confirmation type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(core): simplify Plan Approval Gate to single-agent review
Replace the 3 parallel review agents (request_fit, system_fit,
execution_readiness) with a single comprehensive reviewer that covers
all three dimensions in one prompt. This significantly reduces
complexity while preserving the gate's severity/cap/escalation logic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): address inline review comments on plan gate PR
- Add applyPlanGateMetadata unit tests (5 cases: continue → uncapped,
approve → user_override, free-text → user_takeover, needs_user →
reset reviewCount, no metadata → no mutation)
- Remove userAdditions from EvidenceBundle — it had no way to be
populated through the exit_plan_mode params
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): fix TS4111 index signature access in askUserQuestion test
Move getPlanGateState mock to the top-level mockConfig to avoid
accessing it through a Record<string, unknown> index signature,
which fails under tsc --build strict mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(plan-gate): prevent unavailable decision from auto-approving and add orchestrator-level tests
- Check result.decision === 'unavailable' before the empty-findings
approval path, so an agent self-reporting unavailable stays in plan
mode instead of silently approving autonomous execution.
- Add explicit warn log when needs_user has no suggestedQuestion,
making the fall-through to blocked self-documenting.
- Add mocked-runGateAgent test suite covering the full decision matrix:
approved, blocked, needs_user (with/without questions), cap_escalation,
at-cap P3-only approval, unavailable (self-report and retry exhaustion).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(plan-gate): address fourth-round review findings (C1-C5, S1-S7, B1-B2)
Critical fixes:
- C1: Path C no longer re-restores approval mode — onConfirm owns the
mode transition, execute() only saves the plan and returns the result
- C2: planApprovalGate now branches on result.decision first — only
'pass' with zero findings may approve; 'unavailable'/'needs_user'/
'blocked' with empty findings treated as unavailable or blocked
- C3: runGateAgent stops the override's ToolRegistry in its finally
block to prevent listener leaks from accumulated registries
- C4: plan_gate_cap metadata only honored when capEscalationPending is
true; plan_gate_needs_user only resets reviewCount when gateMode is
still active (capped/uncapped)
- C5: Replaced copy-paste cap tests with real runPlanApprovalGate tests
covering: empty findings for needs_user/blocked, pass-with-findings,
pre-aborted signal, partial retries, uncapped mode, P3-only-at-cap,
P1-at-cap escalation, reviewCount increment, lastFindings storage
Suggestion fixes:
- S1: Guard for not-in-plan-mode before Path C rejection message
- S2: Re-check approval mode and entryId after async gate call to
detect mid-gate user mode changes
- S3: System prompt mentions enter_plan_mode conditionally ("if
available, or the user's plan mode toggle")
- S4: Evidence sections wrapped in <untrusted-content> delimiters;
reviewer system prompt no longer says "Follow instructions exactly"
- S5: enterPlanMode refuses in headless non-interactive mode without
ACP support
- S6: Removed dead fields (keyContext, agentLimitations, limitations,
reviewedEvidence) from types, gateReviewAgents, and all tests
- S7: Fixed stale "3-agent gate" docstring to "single-agent gate"
Body-level fixes:
- B1: Session.ts isPlanModeBlocked call passes isEnterPlanModeTool as
5th argument
- B2: Added test cases for enter_plan_mode in permissionFlow,
ToolCallEmitter, labelUtils, speculationToolGate, and autoMode tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(test): add missing isInteractive mock and update prompts snapshots
The non-interactive guard added in 032ea424b calls config.isInteractive()
which was missing from the enterPlanMode test mock, causing 6 test
failures. Also updates 15 prompt snapshots to match the revised system
prompt wording for plan mode entry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(plan-gate): address fifth-round review findings
- Guard plan_gate_needs_user metadata with needsUserPending flag to
prevent model from fabricating this source and resetting reviewCount
- Fix ToolRegistry leak: use createAgentHeadless's dispose() instead
of manually stopping the override registry
- Fix YOLO/AUTO fallback: require gateState to be present for the
autonomous 'allow' permission path
- Escape </untrusted-content> in evidence bundle fields to prevent
XML injection in the gate review prompt
- Re-read prePlanMode after async gate call to avoid stale values
- Improve originalRequest fallback message when model omits it
- Add tests for needsUserPending guard, capEscalationPending guard,
YOLO-no-gateState fallback, and XML escaping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
91f1ce8c9c
|
fix(core): fix Windows startup error caused by missing printf command (#5012)
* fix(core): split getRecentGitStatus into separate execSync calls to fix Windows printf error * test(core): update getRecentGitStatus tests for separate execSync calls * fix(core): remove unused gitSnapshot and GIT_STATUS_SEPARATOR |
||
|
|
9b4ba60e7c
|
fix(core): stabilize prompt-cache prefix against MCP/skills churn (#4896)
* fix(core): stabilize prompt-cache prefix against MCP/skills churn The skills listing was embedded in the Skill tool's description, which sits near the front of the tools→system→messages cache prefix. Any skill change (chokidar edits, conditional activation on file reads, MCP-prompt changes) rewrote the description and called setTools(), invalidating the ENTIRE cached prefix — tools + system + messages. This was the primary cause of cache hit rate collapse when MCP servers or skills changed mid-session. This PR decouples skill visibility (what the model sees) from skill validation (what the tool accepts), placing them in cache-appropriate tiers: - (A) Make the Skill tool declaration static — refreshSkills() now only updates in-memory runtime sets (availableSkills, pendingConditionalSkillNames, modelInvocableCommands) without calling setTools(). The description is a session-constant string pointing the model to system-reminder messages. - (A2) Extract shared two-layer helpers into skill-utils.ts — collectAvailableSkillEntries (stateful filter/dedup) and renderAvailableSkillsBlock (pure XML render with stable sort). - (B) Move the skills listing into the stable messages prefix — a session-start <available_skills> snapshot built once via buildAvailableSkillsReminder in getInitialChatHistory, rebuilt only at session boundaries (start/resume/compaction). Subagents opt out via includeAvailableSkillsReminder: false. - (C) Mid-session changes flow only through tail deltas — conditional activation reminders now carry description/whenToUse from collected entries; manual enable/disable and new MCP prompts are announced via per-turn system-reminders with announcedSkillReminderKeys dedup state (seeded from snapshot, pruned on disconnect, per-agent keyed). Net guarantee: toggling/activating a skill refreshes the in-memory sets (cache-neutral by construction — never serialized) but produces zero change to tool declarations, system instruction, or the messages prefix. Only the uncached tail delta changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(core): update agent-headless test for includeAvailableSkillsReminder option The getInitialChatHistory call in agent-core now passes includeAvailableSkillsReminder: false alongside includeDeferredToolsReminder. Update the spy assertion to match. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(core): address code review findings for cache-prefix stabilization PR - Add debugLogger.warn to silent catch blocks in drainSkillAndCommandReminders and buildAvailableSkillsReminder for observability - Reset announcedSkillReminderKeys/skillRemindersInitialized in restoreStartupContextAfterCompaction and refreshStartupContextReminder to prevent stale dedup keys after prelude rebuilds - Fall back to name-only skill entries when collectAvailableSkillEntries throws during path activation (correctness regression fix) - Rename fitSkillEntriesToBudget → trimSkillEntriesTowardsBudget to reflect best-effort semantics - Fix append-only activated set suppressing re-enable announcements by tracking ever-announced keys separately from currently-announced keys - Add 16 new tests covering drainSkillAndCommandReminders state machine, buildAvailableSkillsReminder, and buildAddedSkillsReminder Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(core): use double assertion for mock SkillManager to satisfy tsc --build Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(core): add missing everAnnouncedSkillReminderKeys reset in startChat path The replace_all in the prior commit missed this location due to different indentation (6-space vs 4-space). All three prelude rebuild paths now consistently reset all three dedup fields. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(core): address second-round review findings for cache-prefix PR - Fix import ordering in environmentContext.ts (move skill-utils import above debugLogger const) - Add debugLogger.warn to coreToolScheduler catch block for consistency - Extract resetSkillReminderDedup() helper to DRY the three prelude-rebuild reset sites - Return "No skills available" reminder instead of null when entries are empty - Add tests for cmd: key prefix path, catch fallback in activation path, and resetSkillReminderDedup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(core): address third-round critical review findings for cache-prefix PR - Seed skill-reminder dedup from snapshot entries (not first drain) to prevent late-registered MCP prompts from being swallowed as "already announced" - Remove activatedConditional suppression to fix subagent path-activation polluting parent's suppression via shared SkillManager (duplicate announcements are harmless; omissions are permanent) - Compute includeAvailableSkillsReminder from effective tool surface instead of hardcoding false — subagents with Skill tool now get the snapshot - Apply trimSkillEntriesTowardsBudget to buildAddedSkillsReminder to bound MCP prompt descriptions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(core): address fourth-round review findings for cache-prefix PR 1. First-drain fallback no longer silently swallows late registrations: when seedSkillReminderDedupFromSnapshot was never called (edge-case construction path), the drain now announces all entries instead of marking them as "already announced" without the model ever seeing them. 2. Subagent shared-SkillManager double-announcement fix: coreToolScheduler now records inline-announced skill keys via Config.addInlineAnnouncedSkillKeys, and drainSkillAndCommandReminders consumes them before building the delta reminder, preventing the same skill from being announced both inline on the tool result and again in the per-turn tail reminder. 3. Background agent resume now computes includeAvailableSkillsReminder dynamically from the subagent's tool surface (via subagentWillHaveSkillTool) instead of hardcoding false. Subagents that include the Skill tool now get the <available_skills> snapshot on resume. 4. buildAddedSkillsReminder now caps individual entry descriptions to first line / MAX_TRIMMED_SKILL_DESC_LEN before rendering, guarding against unbounded remote-controlled MCP prompt descriptions. 5. Stale comments swept: references to the removed setTools() re-render mechanism updated in slashCommandProcessor.ts, skill-manager.ts, config.ts, nonInteractiveCliCommands.ts, skill.ts, and skill.test.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): add missing getSkillManager mock in background-agent-resume tests The subagentWillHaveSkillTool() helper calls buildAvailableSkillsReminder which needs config.getSkillManager(). Without this mock, the call throws silently and resumeBackgroundAgent returns undefined. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
161c6ec847
|
fix(core): stabilize truncated tool retry key (#4970) | ||
|
|
546b2758fb
|
fix(docs): correct stale settings keys, wrong defaults, and missing commands (#4969) | ||
|
|
3622cb47d4
|
fix(core): add Tool Fallback rule to system prompt to prevent premature tool abandonment (#4931) | ||
|
|
88fdf41ed7
|
feat(core): Workflow P2 — parallel() + pipeline() concurrent fan-out (#4721) (#4947)
* feat(core): add createConcurrencyLimiter sliding-window util A p-limit-style concurrency limiter that keeps at most `limit` thunks in flight and starts a queued thunk the instant a slot frees — so one instance can be SHARED across several fan-out calls and still hold the total in-flight count under a single cap. The existing in-repo concurrency control (memoryDiscovery.ts) is fixed-size sequential batching, not a sliding window, so this is a new primitive rather than a refactor. API: - run(thunk): schedule one thunk through the shared window; rejections propagate raw. - settleAll(thunks): batch convenience that resolves to a position-aligned Array<T|null> where a rejected thunk becomes null (errors-as-data) and the ONLY rejection is an abort of the limiter's signal — so an aborted run surfaces as a rejection rather than a silent array of nulls. Guards a non-positive-integer limit (mirrors background-tasks.ts), preserves input order, short-circuits empty input, and (with an AbortSignal) refuses to start new queued work once aborted. 13 unit tests cover the window cap, errors-as-data, order, sharing across calls, and the abort paths. * feat(core): Workflow P2 — parallel() + pipeline() + 1000-agent cap Implements the P2 phase of the dynamic-workflow port (#4721): concurrent fan-out primitives on top of P1's sequential agent(). parallel(thunks) - Runs thunks through a per-run shared sliding window (createConcurrencyLimiter, cap = max(1, min(16, cpus-2)) — the max() guards 1–2 core machines). - Resolves to a position-aligned array; a thunk that throws becomes null at its index (errors-as-data). parallel() itself only rejects on abort, so an aborted run surfaces a rejection rather than a silent array of nulls. - Rejects on a non-function element (eager promise instead of a thunk). pipeline(items, ...stages) - Parallel-of-chains: one thunk per item, all sharing the SAME window, so it is staggered (item A can be in stage 3 while item B is in stage 1) with no inter-stage barrier. Stage callbacks receive (prev, item, idx); the first stage's prev is the item itself. A stage that throws OR returns null drops that item to null and skips its remaining stages. 1000-agent-per-run cap - The orchestrator wraps this.dispatch with a counter, so EVERY agent() call — sequential, parallel, or pipeline — funnels through one chokepoint. A fan-out cannot bypass the cap; the 1001st call throws. SECURITY — vm-realm result revival (closes an uncovered escape) - vmAsync's resolve path is verbatim: it does NOT re-wrap resolved values. The host parallel/pipeline impl resolves with a HOST-realm array, so handing it to the script would reopen the T1/T8/T14 escape (out.constructor.constructor('return process')() reaches host process via the host Array.prototype chain). The vm wrapper now revives the array in-realm with JSON.parse(JSON.stringify(...)) — the same mechanism that makes `args` safe — before the script sees it. The pre-P2 escape test only probed the *Promise* (already vm-realm), not the resolved array; new tests probe the resolved array (outer + nested) and were verified to FAIL against a verbatim wrapper. Real impls are injected by the orchestrator; the sandbox keeps its throwing P1-unsupported stubs as the default when parallel/pipeline are not injected, so an un-wired sandbox still gives a clear error. Tool description updated to document the P2 surface. Full workflow suite + new orchestrator/sandbox/tool tests green; tsc + eslint clean on all touched files. * fix(core): Workflow P2 self-review — per-element revival + honest tool description Adversarial self-review (6-dimension finder fan-out) surfaced two genuine defects plus three test gaps. EAD-1 [major] — reviveInRealm did JSON.parse(JSON.stringify(WHOLE array)), so a single slot whose VALUE is non-serializable (a thunk that returns a BigInt or a circular object) threw on the entire array and REJECTED the whole parallel()/pipeline(), destroying every sibling result. That defeats errors-as-data for return values. Revive PER-ELEMENT instead: a bad slot becomes null at its index, siblings survive, and the outer array is still built in-realm so the host-process escape stays closed. Regression test: parallel([() => 'a', () => 1n, () => 'c', () => circular]) => ['a', null, 'c', null]. API-1 [major] — the WorkflowTool top-level description (passed to super()) still read "No parallel, no pipeline" while the param-schema description and the runtime both now support them. Updated to describe the P2 surface (parallel/pipeline, ≤16 in flight, ≤1000 total). Also refreshed the now-stale "scheduled for P2" messages on the un-injected fallback stubs to an accurate "unavailable: sandbox created without an implementation" wording. Test gaps closed: - TST-1: pipeline() now has a concurrency test proving it shares the SAME per-run window as parallel (peak in-flight === cap), so a pipeline impl that bypassed the shared limiter would fail. - TST-2: pipeline() staggering is now tested — item 0 reaches stage 2 long before item 1's slow stage 1 finishes, proving there is no inter-stage barrier (a stage-by-stage barrier impl would fail the <50ms threshold). - TST-3: mid-flight abort through the orchestrator is now tested (the prior test only used a pre-aborted controller), proving parallel() rejects after dispatches start rather than resolving with a silent array of nulls. 150 workflow-suite tests green; tsc + eslint clean on touched files. * feat(core): make Workflow P2 caps env-overridable Mirror the established QWEN_CODE_MAX_BACKGROUND_AGENTS / P1 QWEN_CODE_MAX_WORKFLOW_SECONDS precedent so operators can tune the P2 caps without a code change: - QWEN_CODE_MAX_WORKFLOW_AGENTS — override the per-run 1000-agent cap. - QWEN_CODE_MAX_WORKFLOW_CONCURRENCY — override the cpu-derived min(16, cpus-2) in-flight window with an explicit integer. Both use the house resolver shape (resolveMaxConcurrentBackgroundAgents): a non-integer / <1 value is rejected with a debug warning and the default is used. The agent cap default is renamed DEFAULT_MAX_AGENTS_PER_RUN and the cap message is now built from the resolved value. Resolvers take an injectable env arg for pure unit testing. Adds resolver unit tests (default / valid / invalid) plus an integration test proving QWEN_CODE_MAX_WORKFLOW_AGENTS=3 makes a 4-thunk parallel() yield exactly 3 results + 1 null at run time. 154 workflow-suite tests green. * fix(core): throttle Workflow P2 concurrency at the dispatch layer, not the thunk layer P2 self-review (independent adversarial fan-out) caught a real deadlock the mock-tested suite missed: the concurrency window was applied at the thunk level (parallel()/pipeline() scheduled their thunks through the shared limiter). A nested fan-out — e.g. `pipeline([items], item => parallel([...]))`, the canonical /deep-research shape — would have every outer slot held by an outer thunk awaiting an inner settleAll() whose thunks can never acquire a slot. pump() only re-runs from an in-flight thunk's finally, so the queue never drains: unrecoverable silent hang until the 30-min wall clock. On 1-3 core machines (limit = 1) a SINGLE nested call deadlocks; abort cannot break it because pump() is never re-invoked. Fix: the window throttles AGENT DISPATCHES, not orchestration thunks. The limiter now wraps `this.dispatch` inside countedDispatch, so only leaf agent() calls acquire a slot; parallel()/pipeline() compose promises freely via a plain Promise.allSettled + position-aligned null-map (settleToNullArray) and cannot deadlock when nested. This is also the correct "N agents in flight per run" semantics (the cap is about concurrent model calls, not orchestration depth) and makes abort prompt (dispatch slots free normally). The limiter's unused settleAll() is removed — its errors-as-data null-mapping + abort-reject moved into settleToNullArray in the orchestrator, where the batch semantics belong. The tool description's "≤16" is softened to "16 by default" now that QWEN_CODE_MAX_WORKFLOW_CONCURRENCY can raise it. Adds two RED-verified regression tests (nested parallel-in-pipeline and parallel-of-parallel, forced to concurrency=1) that deadlocked before the fix and now resolve in ms. 150 workflow-suite tests green; tsc + eslint clean. * fix(core): Workflow P2 round-2 self-review — prompt queue abort + AbortError consistency + doc accuracy Round-2 adversarial review of the post-F1 code (fresh finders + skeptics) found no new critical/major behaviour bugs (architectural convergence after the dispatch-layer concurrency fix), but surfaced two real correctness items plus a doc-accuracy cleanup pass. createConcurrencyLimiter — prompt queue abort - The limiter previously rejected queued jobs only lazily, inside pump(), which re-runs from an in-flight thunk's .finally. So if an in-flight thunk never settled (a buggy/hung future dispatcher), queued jobs would hang forever even after abort. Production today never hits this because subagent.execute always settles, but the limiter shouldn't lean on an unenforced invariant. Now an `{once: true}` 'abort' listener drains the queue the moment the signal fires. Adds a RED-confirmed regression test (limit=1, in-flight = `new Promise(()=>{})`, abort → queued must reject within 200ms). settleToNullArray — abort error type consistency - Was throwing `new Error('Workflow run aborted.')`, which `isAbortError()` (utils/errors.ts) does NOT recognise — an aborted parallel/pipeline would surface as a generic run failure. Now throws `new DOMException('Workflow run aborted.', 'AbortError')` to match the limiter, so the whole P2 abort path classifies uniformly. Doc accuracy pass (review caught 5 stale strings) - Tool descriptions accurately state the default cap is `min(16, cpus-2)` (not a flat "16"), document both env-overrides (QWEN_CODE_MAX_WORKFLOW_CONCURRENCY, QWEN_CODE_MAX_WORKFLOW_AGENTS), and note that a thunk resolving to a non-JSON-serializable value also becomes null at its index. - makeParallelImpl docstring updated: parallel() rejects on invalid input OR abort (not "only on abort" — that was contradicted by the array/function validation right above). - WorkflowTool fileoverview no longer claims "P1 sequential only". - Orchestrator.run() comment updated to describe the actual P2 signal flow (per-run limiter derived from abortOnTimeout, not P1's "sandbox-level signal intentionally not exposed"). - Wall-clock rationale loses its stale "P1 sequential" framing. 151 workflow-suite tests pass (was 150 + 1 new lazy-abort regression); tsc + eslint clean. * docs(core): Workflow P2 round-3 self-review — wall-clock honesty + cpu-floor + symmetric pipeline docs Round-3 adversarial review found one confirmed factual error in the round-2 doc rewrite plus three real consistency gaps. No new behaviour bugs; the architecture has converged after the round-1 dispatch-layer fix and the round-2 prompt-queue-abort + AbortError consistency. (1) wall-clock docstring: the round-2 rewrite claimed "even a long pipeline with the 1000-agent cap is bounded well under" 30 min. Arithmetically false: 1000 agents × 10-min subagent cap ÷ default 16-concurrency ≈ 10.5 hours, 20× the wall clock. Rewritten honestly: the wall clock is a 0-token-hang backstop, NOT a precise cost cap; for cost control point operators at the env-overridable per-run cap (QWEN_CODE_MAX_WORKFLOW_AGENTS) and concurrency window (QWEN_CODE_MAX_WORKFLOW_CONCURRENCY). (2) tool descriptions now show the actual default formula `max(1, min(16, cpus-2))`, including the outer max(1, ...) floor — without it, the displayed default would be -1 on a 1-CPU container even though the runtime clamps to 1. (3) tool descriptions now document the non-JSON-serializable→null rule for pipeline() as well as parallel() — they share the same reviveInRealm code path (per-element JSON round-trip), so the asymmetric docs were inaccurate. (4) settleToNullArray's AbortError comment is corrected: the round-2 commit overclaimed "uniform classification via isAbortError() at the WorkflowTool boundary". In reality the DOMException name is preserved at the HOST callsite inside the orchestrator, but vmAsync re-throws the script-visible rejection as a fresh `new Error(msg)` and the outer catch wraps it as WorkflowExecutionError — so isAbortError() at the tool boundary returns false either way. The DOMException is still useful as host-internal consistency, but the script-observability claim was wrong. Declined this round (intentional, documented): - F4/P2-R3-F2 wall-clock is plain Error, not AbortError — wall-clock IS a timeout, not an abort; semantically correct as-is. A unified abort surface is P3+ work. - R2-MIN-1 limiter listener leak — only triggers if signal outlives the limiter and never aborts; production caller is per-run and always aborts. - F6 new symbols not re-exported from index.ts — same internal-API decision as createConcurrencyLimiter. - Various test-vacuity nits — tests already cover real failure modes. 151 workflow-suite tests pass; tsc + eslint clean. * fix(core): Workflow P2 PR review R1 — observability + hard ceilings + SECURITY comment R1 review by @wenshao (4 [Suggestion] threads + 1 review-body comment). The fifth thread, [Critical] nested deadlock on a thunk-level limiter, was already caught and fixed in commit 0401ac88f by the dispatch-layer refactor — that commit was authored before the review posted, so the thread is on the pre-fix code; verified resolved by independent round-1 self-review fan-out that converged on the same finding from a different angle, and by a real-LLM E2E scenario (parallel-in-pipeline at concurrency=1 against qwen3-max). settleToNullArray observability + abort docs (T2 / T3 wenshao): - settled.map now logs the discarded rejection reason at debug level when a thunk rejects. Operators investigating a workflow that returned unexpected nulls can now disambiguate the four indistinguishable null paths (dispatch failure, 1000-cap, pipeline stage exception, non-JSON-serializable result) via the WORKFLOW debug logger; the contract to the script stays opaque. - Docstring now explicitly explains the abort-responsiveness path: the apparent Promise.allSettled "wait for all to complete" is in practice "wait for all to reach an abort-aware rejection" because the dispatch signal is threaded all the way down to subagent.execute, and the limiter's separate abort listener drains the not-yet-started queue instantly. env-override hard ceilings (T4 wenshao): - HARD_MAX_AGENTS_PER_RUN_CEILING = 10000 caps QWEN_CODE_MAX_WORKFLOW_AGENTS. - HARD_MAX_CONCURRENCY_CEILING = 64 caps QWEN_CODE_MAX_WORKFLOW_CONCURRENCY. - Both clamp with a debug warning rather than silently dropping the override. - Two RED-verified tests cover the over-ceiling clamp path (and a just-under value preserved). - Not a security issue (env is operator-controlled), but stops a fat-finger =999999999 from silently uncapping the run. reviveInRealm SECURITY comment (T5 wenshao): - Added a SECURITY block warning future maintainers that the revival function MUST stay inside the vm init runInContext block. JSON / Array / Object here are vm-realm globals; extracting this textually-identical helper into a host-side utility would resolve those names against the host realm and silently reopen the T1/T8/T14 escape that the revival is designed to prevent. The textual identity to a host-side util is exactly the trap. Declined this round (review-body 4 pipeline test-coverage sub-claims): - pipeline abort, parallel+pipeline shared-limiter, pipeline 1000-cap, pipeline E2E. After the dispatch-layer refactor in 0401ac88f, parallel and pipeline mechanically share the SAME countedDispatch.limiter.run path — the parallel-side abort / cap / concurrency tests cover the mechanism. Explicit per-shape sibling tests would not catch a regression that the parallel versions don't already catch. 145/145 workflow-suite tests pass; tsc + eslint clean. The 2 config tests fail locally only because the rebase pulled in #4844 (Agent Team)'s new proper-lockfile dependency which the symlinked node_modules doesn't have — CI resolves on fresh install. * fix(core): Workflow P2 PR review R2 — staggering test + revival logging + pipeline E2E R2 review by @DragonnZhang (re-review after R1 push) and @qwen-code-ci-bot (post-fix review). Three real items addressed; one style/wording inconsistency in the bot's review body declined per the round-weighted bar. T6 [Bug] staggering test deterministically fails on macOS-14 CI (DragonnZhang) - The test asserted item 0 reaches stage 2 within 50ms while item 1's stage 1 takes 120ms. That timing assumption holds only at concurrency ≥ 2. On GitHub's macos-14 runners (3 CPU cores) cpu-derived concurrency = 1, FIFO forces all stage-1 dispatches to settle before any stage-2 starts, and the ~122ms s2-of-0 timestamp blows the threshold. The test passed on my workstation but blocks the macos-latest CI matrix — root cause of the failing `Test (macos-latest, Node 22.x)` check that downgraded the prior APPROVE. - Replaced with a deterministic gate-based assertion that does NOT depend on wall-clock thresholds: force QWEN_CODE_MAX_WORKFLOW_CONCURRENCY=2, have item 1's stage 1 block on a Promise gate that only item 0's stage 2 can release. A staggered impl completes (item 0 advances while item 1 is held); a barrier impl deadlocks (item 0's stage 2 can't start until item 1's stage 1 finishes, which can't finish until item 0 reaches stage 2). Vitest timeout catches the barrier-deadlock case; the assertion `item0ReachedStage2 === true` is timing-free. T7 [Suggestion] reviveInRealm catch silently sets null with no log (qwen-code-ci-bot) - The R1 fix added debugLogger.warn for rejected thunks in settleToNullArray, but a thunk that *resolves* to a non-JSON-serializable value (BigInt / circular object) takes a different path through reviveInRealm's catch in the vm init script. Operators with debug logging on still couldn't distinguish "rejected" from "resolved-but-unserializable" — symmetric observability was missing. The R1 audit should have caught the sibling and didn't. - Added a host-side `logRevivalFailure(idx, reason)` hook to the bridge (debugLogger.warn host-side) and call it from reviveInRealm's catch with the coerced-to-string error message. The bridge contract is preserved: only primitive strings/numbers cross back; reviveInRealm itself stays inside the vm runInContext block per the SECURITY comment. T8 [Suggestion] no pipeline() end-to-end test through WorkflowTool (qwen-code-ci-bot) - This is the SAME finding wenshao raised in his R1 review-body, which I declined on a "parallel/pipeline share mechanism — symmetric tests redundant" basis. The bot's R2 raise provides specific mechanism evidence that breaks that argument: pipeline's vm wrapper uses `callPipeline.apply(null, arguments)` and `[items].concat(stages)` to spread the variadic stage list, a code path structurally distinct from parallel's single-argument call. A regression in the vm-to-host stage forwarding would not be caught by the parallel E2E. My R1 decline was based on incomplete grep — apologies, accepting now. - Added a pipeline E2E test mirroring the parallel E2E shape: full stack drive through WorkflowTool → orchestrator → sandbox revival, asserting the chained stage results `[11, 21]`. Declined this round (review-body): - qwen-code-ci-bot's workflow() stub wording inconsistency ("not supported in P1" vs the new "is unavailable: ..." on parallel/pipeline). R2 style/ nit per the round-weighted bar; no behavioural impact. 146/146 workflow-suite tests pass; tsc + eslint clean. * chore(core): Workflow P2 — dedupe WORKFLOW debugLogger + simplify revival error coercion Post-R2 /simplify pass. Two findings that are pure cleanup of code added in the R2 commit, with no scope drift: (1) The R2 fix added `createDebugLogger('WORKFLOW')` to workflow-sandbox.ts, duplicating the identical call in workflow-orchestrator.ts:21. Export the sandbox-side instance and import it in orchestrator — single source of truth, one fewer logger object retained for process lifetime. Direction is natural (orchestrator already imports from sandbox; the reverse would be circular). (2) The reviveInRealm catch coercion `String((e && e.message != null) ? e.message : e)` collapses to `String(e?.message ?? e)`. The truthy/null distinction the original drew (treating empty-string message different from the toString fallback) was not meaningful for a debug log line. Same behaviour for any realistic error; less noise to read. Deferred per the same self-review discipline that the R2 commit message documented: - `withEnv` helper to dedupe the 6-line env-var save/restore boilerplate (now 3 sites in workflow-orchestrator.test.ts): real ~10 LOC win but touches 2 pre-existing tests, out of R2 scope. - Rename `bridge.logRevivalFailure` to a generic `bridge.warn(category, msg)` for future vmAsync silent-reject logging: speculative; per the altitude analysis, "zero rename cost when the second consumer arrives" means deferring loses nothing. 146/146 workflow-suite tests pass; tsc + eslint clean. |
||
|
|
e25d7eec04
|
feat(core): port declarative-agent mcpServers + hooks (CC 2.1.168 parity follow-up) (#4996)
* fix(core): replace yaml-parser stringify with eemeli/yaml for safe nested round-trip
PR #4870 swapped `parse` over to the `yaml` library so block scalars and
nested structures load correctly, but left the hand-rolled `stringify`
in place. The hand-rolled serializer only walks one level of nesting and
emits `[object Object]` for any value below — so any caller that does
`parse → modify → stringify` (e.g. SubagentManager saving a frontmatter
file, or a Claude-Code-format converter writing back a `.qwen/agents/*.md`)
silently corrupts nested fields like `mcpServers` or `hooks` on disk.
This commit:
- Delegates `stringify` to `yaml.stringify` with `lineWidth: 0`, matching
the parse side's library choice and unlocking arbitrary-depth round-trip.
- Drops the now-unused `formatValue` helper.
- Replaces the byte-exact escape-sequence assertions with property-based
`parse(stringify(x)) === x` round-trip checks — the previous tests
pinned hand-rolled quote output that eemeli/yaml legitimately chooses
differently from. The contract that matters at the public API boundary
is round-trip, not stable bytes.
- Adds two CC-shape nested round-trip tests (mcpServers + hooks) that
would have been impossible to express under the old serializer.
The parse-side safety guards added in PR #4870 (schema 'core', timestamp
/ binary tag filtering, `Object.create(null)`, Date/Uint8Array sanitize,
parseSimple fallback) are preserved untouched.
* feat(core): port declarative-agent mcpServers + hooks end-to-end
Builds on PR #4842 (which deferred these two fields) and PR #4870
(which made `parse` nested-safe) by adding the remaining surface +
runtime wiring so a `.qwen/agents/*.md` with per-agent MCP servers
and hooks works the same way as the equivalent `.claude/agents/*.md`.
## Schema layer
`agent-frontmatter-schema.ts` gains two lenient DL7-parity parsers:
- `parseAgentMcpServers` — keeps a record-of-records shape and drops
per-key entries whose value is a scalar / array / null (mirrors CC's
`gS8` shallow validation; per-spec union is enforced later by the
MCP loader).
- `parseAgentHooks` — keeps a record-of-arrays shape and drops events
whose value isn't an array (mirrors CC's `TKO` / `_u`).
Both return `undefined` when no entries survive shape filtering, so the
caller can omit the field entirely rather than emit an empty object.
## SubagentConfig surface
- `types.ts`: adds optional `mcpServers?: Record<string, unknown>` and
`hooks?: Record<string, unknown>`.
- `subagent-manager.ts` `parseSubagentContent`: extracts both fields
with warn-and-drop on top-level shape failure, matching the existing
posture for `permissionMode` / `maxTurns` / `color`.
- `subagent-manager.ts` `serializeSubagent`: emits both back to YAML.
The previous skip-list carve-out in `claude-converter.ts`
(`NESTED_FIELDS_NOT_ROUND_TRIPPABLE`) is gone — round-trip is safe
now that the YAML stringifier is eemeli/yaml.
- `claude-converter.ts`: emits `mcpServers` (was missing) when
converting from a CC plugin agent file.
## Runtime wiring
- `hookRegistry.ts`: new public `addAgentHooks(hooks, scopeId): () => void`
appends ephemeral entries tagged with `agentScope`, runs them through
the same per-definition validation pipeline as session/user/project
hooks (so a malformed entry is logged + dropped instead of breaking
the spawn), and returns an unregister callback. The duplicate-detection
key includes `agentScope` so identical hooks from different subagents —
or from a subagent and the session — coexist instead of swallowing
one another.
v1 scope limitation: while a subagent's entries live in the registry
they fire for every event of their declared type regardless of which
agent is currently active. Per-agent scope filtering at firing time
is a follow-up; the limitation is documented in the user-facing doc.
- `subagent-manager.ts` `buildSubagentContextOverride`: now takes the
`SubagentConfig` and, when per-agent `mcpServers` are present,
overrides `getMcpServers()` on the subagent's Config wrapper to return
the union of session + agent servers (agent wins on key collision,
matching CC's `scope: 'agent'` semantics). The skip-rebuild
optimization is bypassed in this case — without a fresh
`rebuildToolRegistryOnOverride` anchored on the override Config, the
pre-existing wrapper-owned `McpClientManager` would still see only
the session set and the discovery loop below would silently no-op.
After rebuild, the loop explicitly discovers each per-agent server so
its tools land in the subagent's registry before `AgentHeadless.run`.
- `subagent-manager.ts` `createAgentHeadless`: when `config.hooks` is
set, registers via `HookRegistry.addAgentHooks` with a per-spawn
scope ID (`agent:<name>:<uuid>`) and wraps the caller-provided
`AgentHooks.onStop` so the unregister callback fires after the
caller's handler, in a `finally` block to survive a throwing user
handler. Errors from `AgentHeadless.create` itself unregister the
hooks before re-throwing.
## Tests
- `agent-frontmatter-schema.test.ts`: +8 tests covering shape filtering,
drop-on-bad-top-level, drop-when-empty for both new parsers.
- `subagent-manager.test.ts`: +7 tests covering parse, serialize, and
drop-on-malformed for both fields.
- `subagent-manager-override.test.ts`: +2 tests for the
session+agent MCP merge and the no-override pass-through case.
- `hookRegistry.test.ts`: +4 tests for `addAgentHooks` — scope tag,
no-collision with existing same-identity entries, two concurrent
agents keep their own copies, empty payload no-op.
* docs(core): declarative agents follow-up + yaml-parser audit
- `docs/yaml-parser-replacement.md` is the new audit doc covering the
PR #4870 → eemeli/yaml decision (parse-side), the security probe
results (`maxAliasCount` default, `!!js/function` becomes literal
string + warning, merge keys disabled by default, custom-tag filter
for timestamp/binary), and the stringify-side gap this follow-up
closes.
- `docs/declarative-agents-port.md` status table now marks `mcpServers`
and `hooks` as **shipped (follow-up)** with a one-line note on the
runtime wiring strategy + v1 scope limitation for hooks. The
reverse-engineering record below the table is unchanged and remains
the reference for the still-deferred fields (`effort`, `memory`,
`isolation`, `initialPrompt`, `skills`).
- `docs/users/features/sub-agents.md` adds `mcpServers` + `hooks` rows
to the CC-compatibility table and a full example frontmatter showing
all four shipped fields composed together. The v1 hooks scope
limitation is called out as a blockquote so users picking up
per-agent hooks know to prefer fire-globally-safe handlers (logging)
over behavior-mutating ones until the firing-time scope filter lands.
* fix(core): self-review round 1 — proto-pollution defense + parallel MCP discovery
Audited the three commits adversarially across four lenses (correctness,
security, reuse, test quality). Two real findings, two test gaps.
## Real bugs fixed
### `parseAgentMcpServers` / `parseAgentHooks` could pollute the result's prototype
Both helpers wrote into a plain `{}` while iterating an input object that
yaml-parser hands back as null-prototype. A YAML key of literal
`__proto__` survives that null-prototype guarantee as an own property,
so `Object.entries(record)` walks it, and the assignment
`result['__proto__'] = spec` triggers the inherited setter — silently
re-wiring `result`'s prototype chain to point at the attacker's spec.
Object.prototype itself stays untouched (the setter is per-instance) and
the downstream spread `{ ...result }` only copies own enumerables, so the
pollution doesn't directly leak through current callers. But returning
an object with a hijacked prototype is a latent footgun: a future caller
that uses `for…in`, `result.someKey`, or any property access that misses
the own table would pick up values from the polluted chain.
Switching to `Object.create(null)` makes the assignment a plain own
property — matching the null-prototype invariant the rest of
`yaml-parser.ts` already maintains. Two regression tests pin the
defense (one per parser), each constructing a null-prototype input the
way `yaml.parse` actually produces it (`{ __proto__: … }` in an
object literal triggers the setter at construction and so does NOT
reproduce the attack input shape).
### Per-agent MCP discovery serialised through every server
`buildSubagentContextOverride` called `discoverToolsForServer` in a
sequential `for…of` loop with `await`. A misbehaving server (stdio
command that hangs at startup, remote endpoint that times out at the
MCP layer's default 30s) blocks every following server, so the subagent
spawn paid the sum of every per-server timeout instead of the max.
Switched to `Promise.allSettled` over the server list. Each call still
carries the MCP layer's own per-server timeout (`stdio` default 30s,
remote default 5s, per-spec `discoveryTimeoutMs` override); `allSettled`
only removes the serialisation between siblings. Rejections still
log-and-drop so a single bad server doesn't block its siblings'
tools from landing in the subagent's registry.
## Test gaps closed
### `addAgentHooks` coexistence test only asserted count
The "coexists with session/user hooks of the same identity" test
asserted `getAllHooks()` had length 2 after the add but did NOT verify
which entries were present. A regression that dropped `agentScope`
from the dedup key would still leave two entries by ordering luck and
silently break concurrent-agent isolation. Replaced the bare count
check with explicit assertions for `(source: User, agentScope: undefined)`
+ `(source: Session, agentScope: 'agent:test:def')`.
### Empty-record edge cases for `parseAgentMcpServers` / `parseAgentHooks`
The existing "returns undefined when nothing survives shape filtering"
tests covered the case where every key had a bad shape. The empty
input case `parseAgent…({})` was on the same code path but never
exercised — callers rely on the `undefined → omit field` behaviour for
both. Added one test per parser.
* fix(core): pr #4996 review round 1 — leak fixes via explicit dispose contract
Reviewer flagged two Criticals + one Suggestion + one Nice-to-have. The two
Criticals share a root cause (subagent execute()'s inner try/finally
doesn't fire on every exit path), so they fold into a single API change.
## [Critical] Hook cleanup leak on AgentHeadless.execute() early exits
`wrapAgentHooksForCleanup` relied on `onStop` firing inside execute()'s
inner try/finally. Two early-exit paths bypass that finally:
1. `createChat()` returning null at agent-headless.ts:224-226 — returns
before the outer `try` at 233 is even entered.
2. `prepareTools()` throwing at 234 — propagates through the outer
`finally` at 335, which only calls `abortController.abort()` and
never reaches the inner finally that fires `onStop`.
The pre-fix `catch` block only guarded `AgentHeadless.create()`, not
`execute()`. Leaked HookRegistry entries fire globally for every matching
event in the session, polluting unrelated tool calls.
## [Critical] Per-agent MCP server processes leak after every spawn
`discoverToolsForServer` connects real MCP clients (stdio child
processes, HTTP/SSE sockets) in the force-rebuilt subagent ToolRegistry.
Nothing stopped that registry: `Config.shutdown` only reaches the root's
`this.toolRegistry`, and AgentTool's existing `agentConfig.getToolRegistry()
.stop()` (fg + bg + resume finally blocks) only stops the parent's
registry, not the override's distinct fresh one. Every subagent
invocation that declared `mcpServers` orphaned a child process for the
rest of the host process's lifetime.
## Shared fix — caller-driven `dispose` contract
`SubagentManager.createAgentHeadless` now returns
`{ subagent, dispose }`. Callers MUST invoke `dispose()` in the same
`finally` block that wraps `subagent.execute()`. That `finally` lives
in the caller's scope (AgentTool fg/bg, BackgroundAgentResumeService),
which is reachable on every execute() exit — including the two early-
exit paths the previous `onStop` hook never reached.
`dispose` is a single closure that calls the previously-separate
cleanup callbacks in order:
1. `unregisterAgentHooks` returned from `HookRegistry.addAgentHooks`
(when per-agent hooks were registered).
2. `disposeRegistry` returned alongside the new `buildSubagentContextOverride`
return shape `{ context, disposeRegistry }` (set only when this call
force-rebuilt the registry for `mcpServers`).
Both cleanups are wrapped in `try/finally` that logs and re-arms so an
exception in one path doesn't block the other and doesn't double-fire.
The pre-existing constructor-failure catch in `createAgentHeadless` now
runs the same closure directly — the caller never received the return
value, so it cannot fire `dispose` itself.
The three callers gain one variable + one `void dispose?.().catch()`
inside their existing finally:
- `agent.ts:2039` (foreground) — finally at `agent.ts:2904`
- `agent.ts:2131` (background) — finally at `agent.ts:2502`
- `background-agent-resume.ts:630` (resume) — finally at `:852`
Fork subagents share the parent's lifecycle; their `dispose` stays
undefined and the `?.()` is a no-op.
`wrapAgentHooksForCleanup` is removed — it was load-bearing only for
the happy + inner-reasoning-loop-failure paths and is now obsolete.
## [Suggestion] Repeated guard condition
`config.hooks && Object.keys(config.hooks).length > 0` no longer appears
in both `if` / `else if` arms. Single outer guard + nested branch on
`hookRegistry`.
## [Nice-to-have] claude-converter.ts:290 missing `.trim()`
`stringifyYaml(newFrontmatter).trim()` brings the converter into line
with `subagent-manager.ts:651`. Without trim, eemeli/yaml's trailing
newline produced an extra blank line before the closing `---`
delimiter — cosmetic (both readers tolerate it) but the asymmetry
between the two writers was a real consistency bug.
## Tests
3 new RED-first tests in `subagent-manager.test.ts` pin the dispose
contract:
1. `returns { subagent, dispose }; dispose unregisters per-agent hooks`
2. `dispose unregisters even when execute() never runs (early-exit leak fix)`
— the case where `createChat()` → null or `prepareTools()` throws
3. `dispose is a safe no-op when neither hooks nor mcpServers are declared`
Override test helper updated to destructure the new
`buildSubagentContextOverride` return shape. AgentTool + BackgroundAgent
test mocks updated to return `{ subagent, dispose }`. All 2087 in-scope
tests pass.
* refactor(core): /simplify cleanup pass on the dispose contract
Three cleanups from a multi-angle quality review (reuse / simplification /
altitude lenses, all on commit
|
||
|
|
12bc80c308
|
fix(cli): debounce resize repaint and clear stale scrollback on settle (#4919)
Dragging a terminal window edge during streaming left fragmented content
at mixed widths in the scrollback on macOS Terminal.app / iTerm2.
Root cause (legacy `ui.useTerminalBuffer=false` path, the default):
- A window drag fires dozens of `resize` events with intermediate widths
(useTerminalSize is intentionally undebounced; other consumers want the
live value).
- PR #3967 (
|
||
|
|
adbdff8b1f
|
fix(cli): avoid headless browser open crashes (#4716)
* fix(cli): avoid headless browser open crashes * fix(cli): reuse browser launch guard * test(core): isolate browser launcher env * fix(core): make browser env launches non-blocking * fix(core): handle LSP stdio write failures |
||
|
|
1cd5b81aa3
|
fix(desktop): keep composer sendable after idle escape (#4788)
Co-authored-by: pratyushjaiswal0806-dot <pratyushjaiswal0806@gmail.com> |
||
|
|
246a0a1fc5
|
feat(core): persist file history snapshots for cross-session /rewind (T2.1) (#4897)
* feat(core): persist file history snapshots to JSONL for cross-session /rewind (T2.1) File history snapshots were purely in-memory — lost on process exit, making /rewind unusable after session resume. This adds JSONL persistence so restored sessions can rewind to any pre-resume turn. Key changes: - Serialize/deserialize FileHistorySnapshot to/from JSONL system records - Record each snapshot after makeSnapshot succeeds (incremental per turn) - Re-record surviving snapshots after rewind (full batch on active branch) - Parse file_history_snapshot records in sessionService.loadSession() - Restore snapshot chain in config.getFileHistoryService() on resume - Copy backup files on session fork (hard link with copy fallback) - Add session_resume capability tag (stable alias for unstable_session_resume) - validateRestoredSnapshots with dedup + batched parallel stat 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix: address Copilot review — last-wins dedup + isEnabled guard - Change snapshot dedup from first-wins to last-wins so rewind batch records (which contain the most up-to-date snapshot state) override earlier incremental records for the same promptId. - Guard validateRestoredSnapshots behind isEnabled() to skip I/O when file checkpointing is disabled. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix: address wenshao review — ghost snapshots + test assertion - Session.ts: slice snapshots to targetTurnIndex+1 to exclude turns being discarded (fixes ghost-snapshot persistence) - AppContainer.tsx: only pass snapshots when file restore succeeded (avoids writing un-truncated snapshots for conversation-only rewind) - Session.test.ts: update rewindRecording assertion to expect 3 args 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix: address wenshao R2 — slice snapshots in AppContainer + add deserialize warning - AppContainer.tsx: use .slice(0, targetTurnIndex + 1) to match Session.ts behavior (prevents ghost snapshots for conversation-only rewind) - sessionService.ts: log warning instead of silent continue on malformed file_history_snapshot deserialization 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix: enable daemon file checkpointing + seed TUI promptCount on resume Two fixes from wenshao's local verification: 1. ACP daemon sessions had fileCheckpointingEnabled=false because stdin is a pipe (non-TTY). Add enableFileCheckpointing() to Config and call it in acpAgent.newSessionConfig so daemon /rewind works. 2. TUI prompt counter restarted at 0 on --resume, colliding with restored snapshot promptIds and corrupting the chain via last-wins dedup. Seed promptCount from the resumed conversation's user turn count so new promptIds don't overlap. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix: add makeSnapshot + recordFileHistorySnapshot to ACP prompt path ACP sessions drive the chat through Session.prompt → GeminiChat, bypassing GeminiClient.sendMessageStream where makeSnapshot lives. This meant daemon-created sessions never produced file history snapshots, leaving /rewind non-functional. Add makeSnapshot + recordFileHistorySnapshot at the start of each ACP prompt turn (mirroring client.ts:1488), using the existing sessionId########turn promptId format. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix: add session_resume to integration test capability assertion 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix: add debug logging to ACP makeSnapshot catch blocks 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(acp): move makeSnapshot after slash-command and hook checks Locally handled slash commands (/help, /memory, etc.) previously created file history snapshots even though no model turn was added. This caused the snapshot index to drift from the real user turn count, breaking rewind in web-shell sessions that use slash commands frequently. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address qwen-code-ci-bot review round 5 - Remove stale line number from comment (Session.ts) - Single cast instead of double cast for systemPayload (sessionService.ts) - Guard mkdir in copyFileHistoryBackups to prevent fork failure (sessionService.ts) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address wenshao review — mock, ordering guard, JSDoc placement - Add makeSnapshot/rewind to FileHistoryService mock in Session.test.ts - Invalidate cached FileHistoryService on enableFileCheckpointing() to prevent stale enabled=false if service was lazily created first - Move copyFileHistoryBackups above class JSDoc to fix association Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: truncate in-memory snapshots on ACP rewind + deduplicate recordFileHistorySnapshot - Call restoreFromSnapshots(survivingSnapshots) in ACP rewindToTurn to prevent phantom snapshots from accumulating in the in-memory array after conversation-only rewind - Simplify recordFileHistorySnapshot to delegate to the batch variant Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add restoreFromSnapshots to FileHistoryService mock Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
963fc543d1
|
ci(desktop): mac code-signing + App Store Connect API-key notarization (#5013)
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
* chore(desktop): drop dead NOTARIZE env flag from mac signing paths electron-builder (>=24) auto-notarizes via notarytool whenever APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID are present in the env. The NOTARIZE=true flag set in the release workflow, build-dmg.sh, and scripts/build/darwin.ts was never read by electron-builder, and the build-dmg.sh comment claiming it enabled notarization was misleading. Remove the no-op and document the actual auto-detection behavior. * ci(desktop): notarize via App Store Connect API key instead of Apple ID Switch the macOS desktop release notarization path from the Apple ID + app-specific password method to the App Store Connect API key method, which is more robust (no 2FA, no password expiry) and reuses the notary key already provisioned for the org. The signing step now reads APPLE_NOTARY_API_KEY_P8_BASE64, APPLE_NOTARY_KEY_ID, and APPLE_NOTARY_ISSUER_ID, decodes the .p8 to a temp file, and exports APPLE_API_KEY/APPLE_API_KEY_ID/APPLE_API_ISSUER, which electron-builder (>=24) consumes to notarize via notarytool. Published mac releases now require those notary secrets plus APPLE_TEAM_ID. |
||
|
|
2cfa32f785
|
Add /cd command (#4890)
* feat(cli): add /cd command * fix(cli): stabilize cd command checks * fix(cli): cap pending cd trust confirmations * fix(core): allow resuming migrated cd sessions * test(core): fix cd relocation path expectations * fix(core): accept migrated sessions in all project checks * fix(core): preserve runtime workspace dirs on cd * fix(cli): keep branched relocated sessions loadable --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
f2ebfeaece
|
fix(goal): persist iteration count across resume so MAX_GOAL_ITERATIONS bounds the whole session (#5000)
* fix(goal): persist iteration count across resume so MAX_GOAL_ITERATIONS bounds the whole session
On resume, restoreGoalFromHistory re-arms an unfinished /goal via registerGoalHook,
which always primed the store with iterations: 0. Since findGoalToRestore only
returned the condition, the running count recorded in the transcript was dropped,
so the MAX_GOAL_ITERATIONS safety cap was re-granted in full on every resume —
an unreachable goal could auto-loop another full budget after each /resume.
The count is already persisted (checking goal_status items carry iterations), so
the fix just reads it back:
- findGoalToRestore returns { condition, iterations } from the latest non-terminal
goal_status item (set items restore at 0).
- registerGoalHook accepts an optional initialIterations (default 0, clamped at 0).
- restoreGoalFromHistory threads the restored count through.
Resume re-arm stays passive — continuation timing is unchanged.
Closes #4999
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(goal): persist cumulative checking iterations
* fix(goal): record checking status during continuations
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
||
|
|
5bcd2665da
|
chore(daemon): remove dead code and simplify control flow (#4789)
* chore(daemon): remove dead code and simplify control flow - acpAgent.ts: remove unreachable `if (!bootstrapSkipsMcpDiscovery)` block (32 lines) — the constant was hardcoded `true`, making the MCP-ready wait and failed-server warning permanently dead code. Inline the flag directly into the `config.initialize()` call. - bridge.ts: remove no-op try/catch in `removeRuntimeMcpServer` — every path through the catch block re-threw the error unchanged. Collapse to a direct `await Promise.race(...)`. - bridge.ts: merge `'resolved'` and `'recorded'` switch cases in `respondToSessionPermission` — both returned `true`. - bridge.ts: remove 19-line orphaned deletion-archaeology comments (lines documenting `resolveAnyTrustedClientId` removal and the F3 permission-mediator lift — the functions no longer exist here). - bridgeClient.ts: remove 9-line orphaned deletion-archaeology comment (documents types moved to permissionMediator.ts). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(daemon): preserve provider status metadata * fix(daemon): sanitize provider status urls * fix(daemon): sanitize account info base url * fix(daemon): sanitize provider list base urls --------- Co-authored-by: 衍星 <qiuyusheng.qys@alibaba-inc.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |