GLM-5.2 ships a 1M context window, and 1M is becoming the norm for newer
GLM releases. The previous `/^glm-5/` rule capped the whole GLM-5 line at
202752, so every new model would need a code change.
Make 1M the forward default for GLM-5.2+, GLM-6.x..9.x and two-digit
majors, while pinning the confirmed 200K families (GLM-5 / 5.0 / 5.1 and
GLM-4.x or older) explicitly. Third-party deploy prefixes (e.g.
`pai/glm-5.3`) are already stripped by normalize(), so they match the same
rules. Non-numeric names (e.g. glm-z1) stay on the conservative fallback.
Previously /goal state lived only in frontend memory — page refresh or
multi-device sessions lost the active goal. Now the CLI emits goal status
updates as structured daemon events (_meta.goalStatus), which flow through
the transcript as status blocks (source: 'goal', data: {...}). The web-shell
rebuilds goal state from transcript blocks on connect, making goal status
survivable across page refreshes and syncable across devices.
- CLI: emitGoalStatus on goal set/clear, pass outputHistoryItems through
nonInteractiveCliCommands, add setAt to goalCommand output
- SDK: widen DaemonUiStatusEvent source/data types, preserve them in
transcript blocks
- webui: normalize _meta.goalStatus in DaemonSessionProvider, replace
sentinel-prefix text encoding with structured data
- web-shell: derive activeGoal from transcript blocks (getLatestActiveGoalFromBlocks),
remove optimistic client-side goal dispatch, parse structured goal data
in GoalStatusMessage/SystemMessage
- Tests: cover emitGoalStatus, outputHistoryItems passthrough, transcript
block serialization, DaemonSessionProvider event conversion
- Also: harden McpDialog restart result type check with isRestartEntriesResult
Co-authored-by: ytahdn <ytahdn@gmail.com>
The review-pr job only checked qwen's exit code, the tee status, the
timeout sentinel, and an empty log. When the model connection drops
mid-review, qwen still exits 0 and emits a terminal stream-json result
event with subtype=success / is_error=false whose text carries the
inlined '[API Error: ...]'. All existing checks pass, so the job goes
green without ever posting a review comment.
Inspect the terminal result event explicitly: fail when it is missing,
when is_error is true or subtype is not success, or when the result text
contains an inlined API error. A failed check now triggers the existing
fallback-comment step instead of a silent green success.
Refs #5052
* feat(web-shell): make input shortcuts discoverable and clickable
- Add an always-on, clickable hint row below the input: ↑ previous / ↓ next history, ctrl+r search, / commands, @ files — each click runs the matching action.
- Make the status bar "? for shortcuts" a persistent clickable button shown next to the mode indicator.
- Dismiss the shortcuts panel by clicking outside it or pressing Esc (no dedicated close button).
- Cancel reverse-i-search by clicking outside the panel (same as Esc), restoring the original draft.
- Distinguish ctrl+r (search history) from up/down (cycle history) in the shortcuts panel.
- Auto-prepend a space for mid-word @-mentions, and make the / and @ triggers idempotent so a second click re-opens the menu instead of producing "//" or "@ @".
* fix(web-shell): address PR review — touch dismissal, primary-button guard, @ idempotency
- Outside-press dismissal for the shortcuts panel and reverse-i-search now also listens for touchstart, ignores non-primary (middle/right) buttons, and respects defaultPrevented — matching the Settings/Mode inline panels.
- insertText('@') no longer inserts a duplicate '@' when the cursor sits directly before an existing '@'; it steps over the existing one and opens the menu.
* fix(web-shell): close shortcuts panel idempotently on outside-press
Touch fires touchstart plus a synthesized mousedown; a toggle onClose would reopen the panel right after closing. Use a dedicated close (set false) for the outside-press / Escape dismissal, matching the other inline panels.
* fix(web-shell): address /review — disabled guard, mid-line slash, focus, dedup
- Hide the hint row while the editor is disabled and bail the history/search callbacks on disabledRef, so the buttons can't bypass the disabled guard. insertText stays usable for App-driven injection; the now-hidden row is the only internal path that passes '/' or '@'.
- Clicking the / hint on non-empty, non-slash text replaces the content with '/' so the command menu actually opens, instead of leaving a stray mid-line '/'.
- Outside-click search dismissal no longer steals focus from the clicked target (closeSearch keepFocus=false).
- Extract a shared hintProps() helper for the five hint buttons.
* fix(web-shell): address /qreview — capture-phase Escape, hint aria-haspopup
- The shortcuts panel's Escape now wins over the App-level global Escape (capture phase + preventDefault/stopPropagation), so Esc closes the panel instead of being swallowed (clearing queued prompts / cancelling the stream) while it's open.
- Add aria-haspopup to the popup-opening hint buttons (dialog for ctrl+r, listbox for / and @), matching the sibling toolbar buttons.
* fix(web-shell): theme the slash-completion popup scrollbar
The CodeMirror autocomplete list and info panel used the browser-default (light) scrollbar, clashing with the dark theme. Apply the repo's scrollbar convention (thin + var(--border-color) thumb over a transparent track, with the -webkit fallback) so it matches in both themes.
* fix(web-shell): don't destroy the draft when clicking / on non-empty input
Replacing the document with '/' (the previous /review fix) silently wiped a typed draft. Instead, no-op when the line isn't already a command and the editor is non-empty — the slash menu needs a line-leading '/', which can't be added without either a stray mid-line '/' or clobbering the draft. Empty input still inserts '/' and opens the menu.
* fix(web-shell): guard hint history nav on multi-line input; hide hints behind dialogs
- navigatePrev/NextHistory now early-return on doc.lines > 1, matching the ArrowUp/ArrowDown keymap, so clicking the up/down hints no longer replaces a multi-line draft with a single history entry.
- showShortcutHints also requires !dialogOpen, matching the Ctrl+R keymap guard, so the hint buttons aren't interactive while a dialog is open.
* feat(web-shell): undo click-inserted / or @ on cancel; align hint nav with keymap
- Clicking the / or @ hint then pressing Escape (without typing past the inserted char) now removes the trigger too — it was clicked in, not typed. Editing past it cancels this.
- navigatePrev/NextHistory move the completion selection when the menu is open, matching the ArrowUp/ArrowDown keymap.
- The / hint no-op (non-empty draft) no longer fires startCompletion, which would pop an empty menu; a line-leading / still re-opens it.
* fix(web-shell): undo click-inserted trigger on any completion dismissal, not just Escape
Only the Escape keymap undid the click-inserted / or @. Clicking away or blurring also closes the menu but left a stray trigger behind. Watch the completion status (active -> closed) via an updateListener instead, so every dismissal path (Escape, click-away, blur) removes an untouched click-inserted trigger; the per-key special-case in the Escape handler and restarter is removed.
* feat(web-shell): grey out history hint arrows when there's nowhere to go
useInputHistory now exposes a nav { canUp, canDown } state, kept in sync on push/navigate/reset. The up/down hint buttons are disabled when there's no older entry to recall, or when not currently browsing history. Keyboard and mouse share the same state, so the affordance stays accurate however history is navigated.
* feat(core): add Kind.Agent for Agent tool to improve UI categorization
The Agent tool was using Kind.Other as a catch-all, causing WebUI
permission dialogs to show generic titles and descriptions. Adding a
dedicated Kind.Agent value enables agent-specific UI rendering in
permission drawers, tool labels, and export normalization.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* test(webui): lock agent label mapping in labelUtils test
Address wenshao review on PR #5085 — add assertion that
getToolDisplayLabel({ kind: 'agent' }) returns 'Agent', matching the
sibling task/skill cases.
* test(cli): cover agent kind in export normalization + document acp-sdk cast
Address wenshao review on PR #5085:
- Add normalize.test.ts case asserting an agent-kind tool call is
preserved as 'agent' through normalizeSessionData.
- Add TODO(acp-sdk) comment on the KIND_MAP 'agent' cast explaining why
we emit 'agent' (webui SSE consumer) rather than mapping to 'other'.
* fix(acp): map Kind.Agent to 'other' on the wire; drop unusable 'agent' kind
wenshao's daemon-level A/B verification showed emitting kind:'agent' over
ACP is a regression: the daemon's ClientSideConnection Zod-validates every
session/update + session/request_permission from the qwen --acp child before
SSE fan-out, and @agentclientprotocol/sdk has no 'agent' ToolKind (verified
through 0.25.1), so the frame is rejected (invalid_union) and dropped — the
agent tool_call + permission dialog that previously reached SSE clients as
kind:'other' now never arrive.
Shrink to a no-regression safe version:
- ToolCallEmitter maps the internal Kind.Agent to 'other' on the wire (drops
the 'agent' as ToolKind cast that only fooled tsc, not the runtime schema).
- Revert the wire/UI/protocol 'agent' additions that depended on a value the
protocol can't carry: PermissionDrawer/labelUtils kind branches, web-shell
DaemonMessageToolKind + inferToolKind, Java SDK schema/enum, export allowlist.
Kind.Agent stays in core as the internal tool category. The dedicated agent
permission dialog will be delivered via _meta.toolName (which already rides the
validated wire) in a follow-up PR, not via a protocol kind.
* test(cli): add compactOldItems idempotency regression tests
Cover the scenario fixed in commit 595701096 where already-compacted
tool groups (resultDisplay === UI_COMPACT_CLEARED_MESSAGE) were
incorrectly counted as having real output, causing over-compaction.
Three new test cases:
- Already-compacted groups are not re-compacted; second call is a no-op
- All tool groups already compacted → no-op
- Mixed tool group (some tools real, some cleared) → only groups with
real output are compacted
* fix(cli,core): enable explicit GC and disable debug log by default
- enableExplicitGC defaults to true, --expose-gc added to start/dev scripts
- isDebugLogFileEnabled() defaults to false (opt-in via QWEN_DEBUG_LOG_FILE=1)
- Add safety tests: trigger_gc only in critical tier, global.gc() only in
memoryPressureMonitor.ts trigger_gc case
* fix: address R1 review comments for memory pressure monitor
- Replace brittle source-parsing test with behavioral tests for global.gc()
- Export UI_COMPACT_CLEARED_MESSAGE constant and use in tests
- Remove redundant NODE_OPTIONS override from start script
- Add production bin wrapper with --expose-gc for OOM protection
- Remove unused path import from memoryPressureMonitor.test.ts
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix: forward --expose-gc to all deployment modes
Standalone package shims and daemon-spawned sessions (AcpBridge,
httpAcpBridge) were missing --expose-gc, causing explicit GC to
silently fail under critical memory pressure.
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix: forward child process signal in cli-entry wrapper
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(cli,channels): filter --inspect flags when forwarding execArgv to daemon children
* fix: make cli-entry.js executable (mode 100755)
* fix(core): reject whitespace-only QWEN_DEBUG_LOG_FILE and add QWEN_MEMORY_ENABLE_GC=0 opt-out
* fix(scripts): include cli-entry.js wrapper in dist package for npm publish
* fix(acp-bridge): forward --expose-gc and filter --inspect in spawnChannel
- Add --expose-gc to getAcpMemoryArgs() so daemon-spawned ACP children
have global.gc() available for critical memory pressure cleanup
- Filter --inspect/-brk flags from process.execArgv to prevent port
conflicts in multi-session daemon mode
- Update spawnChannel.test.ts for new getAcpMemoryArgs() return shape
This change was previously in httpAcpBridge.ts but lost during the
daemon refactor merge (#4490) that moved spawn logic to acp-bridge.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* feat(web-shell): reveal full tool detail and auto-collapse finished tools
Long tool descriptions were hard-capped at 120 characters and finished
tools (shell/edit/write) stayed expanded indefinitely, so commands were
unreadable and the transcript filled with stale output.
- Lift the 120-char description cap so the full command/path reaches the
DOM; collapsed rows ellipsise via CSS (adapts to width) and a click
reflows the full text into a wrapped block below the header.
- Add a leading disclosure chevron; any row with detail output or a long
description is now expandable.
- Auto-collapse a tool to its one-line summary once it completes
successfully. Running tools stay expanded (live output) and failures
stay expanded (error visible); agents keep their own manual expand state.
* fix(web-shell): preserve manual expand on completion; correct auto-expand comment
Review feedback on #5088:
- shouldAutoExpand: rewrite the comment to match the code. Only the verbose
kinds (shell/edit/write/ask) auto-expand and stay expanded on failure; other
kinds are collapsed by default (their summary line shows the outcome and they
stay click-to-expand). Force-expanding every failed tool was rejected because
tools without an expanded-detail renderer would then hide the summary line
and show an empty body — i.e. hide the error.
- Auto-collapse-on-completion no longer overrides an explicit user toggle: a
userToggledRef latch (set on header click, reset on tool-identity change)
guards the collapse effect, so a row the user expanded/collapsed keeps its
state when the tool finishes.
* test(web-shell): assert tool-detail relocation via DOM, not textContent
Review feedback (#5088): the expand test asserted container.textContent
contains the command before and after the click, which passes regardless of
whether the description is relocated from the header span to the wrapped
block (textContent concatenates the whole subtree). Assert the DOM move
instead — the command is in a leaf <span> while collapsed and in none while
expanded — so a regression dropping the relocation now fails the test.
Migrate the built-in Computer Use tool surface from open-computer-use
(npm/npx) to cua-driver-rs (native Rust driver, trycua/cua).
- Replace the 9-tool ocu surface with the full 35-tool cua-driver surface
(page/CDP, cursor, session, recording, config, app lifecycle, …),
generated from the live `cua-driver mcp` tools/list — pinned to v0.5.2.
- Per-platform signed + notarized binary distribution: download into
~/.qwen/computer-use/ with SHA-256 integrity verification, a three-tier
Windows unzip fallback, headers/idle timeouts, and a bounded retry loop.
- macOS TCC permission flow via CuaDriver.app (com.trycua.driver), polled
one-at-a-time through the no-gate status daemon.
- Mirror cua-driver assets on the qwen-code-assets OSS bucket with a
push-to-main sync workflow guarded by checksums.
- Gate high-risk tools (kill_app, launch_app, start_recording, set_config,
replay_trajectory, page JS execution) to an explicit confirmation type so
AUTO_EDIT cannot silently auto-approve them; AUTO defers to the classifier,
YOLO auto-approves.
Under StrictMode (dev default), DaemonWorkspaceProvider's useEffect
cleanup called client.dispose() synchronously, destroying the memoized
DaemonClient that the second effect invocation reused. This left the
transport closed before the session provider could attach, surfacing as
"Transport connection closed" and a permanent "Loading..." / disconnected
state in the web-shell.
Defer disposal by one microtask. StrictMode's synchronous re-mount
cancels the pending disposal before the microtask fires, preserving the
shared client. Real unmounts and client replacements still dispose
normally since no cancellation occurs in those paths.
* fix(cli): add OSC 52 clipboard fallback for SSH environments
- Add writeOsc52() helper in commandUtils.ts and vim.ts
- Fall back to OSC 52 escape sequence when xclip/xsel/wl-copy unavailable
- Fixes /copy command and vim yank (yy, yw, etc.) over SSH without X11
* refactor(cli): extract writeOsc52 to shared clipboardUtils
- Add writeOsc52() export in clipboardUtils.ts with TTY check, error handling,
and boolean return for success/failure
- Remove duplicate writeOsc52 from commandUtils.ts and vim.ts
- Update both to import from clipboardUtils.ts
- Add 6 tests verifying OSC 52 escape sequence output to stdout/stderr,
TTY detection, special chars, empty string, and error handling
- Fix commandUtils test to assert OSC 52 fallback instead of throwing
Fixes code duplication and inconsistent error handling noted in PR review.
* test(clipboard): fix spawn timeout test hanging with fake timers
Remove vi.useFakeTimers() which is incompatible with process.nextTick
in the mock pattern. Use real timers with 10s test timeout instead.
* fix(cli): check writeOsc52 return value in callers
Warn when OSC 52 clipboard write fails (no TTY available) instead of
silently ignoring the failure.
* fix(test): clear wl-paste image type cache before BMP-to-PNG tests
BMP-to-PNG test block was missing its own beforeEach to reset the
cachedWlPasteImageTypes cache, causing the 'prefer PNG over BMP' test
to fail intermittently in CI when a previous test had populated the cache.
* fix(test): use dynamic import instead of vi.resetModules() to fix test pollution
* fix(test): correct spawn call assertion in BMP-to-PNG clipboard test
* fix(test): clean up /tmp/test before BMP-to-PNG clipboard test to fix flaky assertion
* fix(test): restore vi.resetModules() with dynamic imports for test isolation
Replace stale top-level imports of clipboardUtils with describe-level
variables populated by dynamic import() after vi.resetModules() in
beforeEach. This ensures every test gets a fresh module instance,
eliminating cross-test state pollution from cachedWlPasteImageTypes
and linuxClipboardTool.
* fix(cli): throw error when all clipboard methods fail and restore vi.resetModules for test isolation
* fix(cli): try OSC 52 before throwing when xclip/xsel fail
Ensures consistent behavior between /copy and vim yank commands
in SSH environments where xclip/xsel are installed but fail
due to missing X display.
* test(cli): add OSC 52 fallback tests for xclip/xsel failure scenario
Adds two tests to verify OSC 52 behavior when clipboard tools
exist but fail (e.g., SSH without X forwarding):
- Verifies OSC 52 is attempted and fails gracefully when no TTY
- Verifies OSC 52 succeeds when TTY is available
* test(cli): mock isTTY in OSC 52 no-TTY fallback test to prevent flakiness
* fix(cli): wrap OSC 52 sequence with wrapForMultiplexer for tmux/screen support
* fix(cli): try OSC 52 fallback in vim writeClipboard when cached tool fails at runtime
* fix(cli): prefer stderr over stdout for OSC 52 to avoid Ink rendering pipeline interference
* fix(cli): mention OSC 52 attempt in error message when xclip/xsel and OSC 52 both fail
* fix(cli): harden OSC 52 clipboard with size limit and async error handling
- Cap OSC 52 payload at 75KB (~100KB base64) to prevent terminal crashes/hangs
from oversized escape sequences (iTerm2 ~100KB, xterm ~8KB limits)
- Add write callback to capture async failures on stdout/stderr streams
- Apply same hardening to duplicate implementation in AuthenticateStep.tsx
- Strengthen tests: stub TMUX/STY for determinism, add tmux/screen DCS wrap tests
* feat(core): Workflow P3 — agent({schema, agentType, model, isolation:'worktree'}) (#4721)
Adds the P3 dispatch options to the workflow runtime, completing the
contract qwen-code's workflow tool matches against upstream Claude Code
2.1.168. P1/P2 stubs (workflow-sandbox.ts:508-527) are replaced with
production paths routed through `SubagentManager.createAgentHeadless` so
per-call model overrides go through `buildRuntimeContentGeneratorView`
(provider routing), per-agent MCP servers / hooks get isolated
lifecycles, and worktree-isolated subagents run against a rebound Config.
- agent({agentType: 'X'}) resolves against the declarative-agents
registry (#4842 + #4996) via findSubagentByName; unresolved names throw
"agent({agentType}): agent type 'X' not found" verbatim from upstream.
- agent({model: 'qwen3-max'}) is threaded into SubagentConfig.model so
the runtime view sees it (modelConfigOverrides alone would only swap
the model name within the existing provider's view).
- Workflow's disallowed-tool floor [SendMessage, ExitPlanMode] is unioned
with the agentType's own disallowedTools so a permissive agentType
cannot re-enable them for a workflow subagent.
- agent({isolation: 'worktree'}) provisions a fresh worktree via
GitWorktreeService.createUserWorktree (slug agent-<7hex>, mirrors
AgentTool 1849-1963), rebinds cwd/getTargetDir/getFileService/
getWorkspaceContext on a prototype-chained Config override, and on
completion auto-removes the worktree if clean or preserves the path +
branch (appended to the result string) when the subagent left changes.
Parent-dirty trees are refused with a clear error to avoid silently
running the subagent against a stale HEAD.
- agent({isolation: 'remote'}) throws "agent({isolation:'remote'}) is
not available in this build" verbatim (upstream 2.1.168 parity).
- agent({schema: S}) injects a per-call SyntheticOutputTool (existing
tools/syntheticOutput.ts, AJV-backed) into a fresh per-subagent
ToolRegistry built via rebuildToolRegistryOnOverride, then watches
AgentEventEmitter TOOL_CALL/TOOL_RESULT events for `structured_output`
invocations. A successful call's args are captured as the dispatch
return value (object, not string); after two failed attempts the
third failure aborts the dispatch and throws "subagent completed
without calling StructuredOutput (after 2 in-conversation nudges)"
verbatim. No agent-core.ts changes — the entire 2-nudge counter
lives in the dispatch layer so the shared subagent loop is unaffected.
The sandbox's agent() wrapper now revives per-call object returns into
the vm realm (JSON round-trip inside the vm runInContext block), closing
the same T1/T8/T14 host-prototype-escape vector that P2's per-element
revival closed for parallel/pipeline. Two new sandbox security tests
(constructor-chain probe + non-JSON-serializable collapse) regress this.
WorkflowAgentResult widens from `string` to `string | object`; the
fast-path (no agentType/model/isolation/schema) is preserved byte-for-byte
to keep P1/P2 zero-overhead.
Tests: 159 workflow-suite tests + 217 adjacent (subagents / syntheticOutput /
agent-override) all green. Real-LLM E2E follow-up planned (mirroring P2's
13/13 qwen3-max validation).
Related #4721 (parent design — multi-phase, not closed by this PR)
Related #4732 (P1 merged) #4947 (P2 merged) #4842#4996 (declarative agents)
* chore(core): P3 self-review R1 — align worktree suffix wording + 6 test gaps
R1 of pre-push adversarial self-review on PR #5034 surfaced 6 confirmed
findings across 6 diverse lenses (correctness / security / reuse-altitude
/ self-invariant / consumer-breakage / test-gaps). Each finding faced 2
independent skeptics defaulting to refuted=true; 6 survived majority
challenge.
Source code:
- Worktree-preserved suffix wording now matches AgentTool's
formatWorktreeSuffix (agent.ts:1700-1719) verbatim, including the
`git worktree add <path> <branch>` recovery hint for the directory-
removed-but-branch-preserved race.
Test gaps closed:
- schema-mode success after 1 nudge (round-2 args captured)
- schema-mode success after 2 nudges (round-3 args captured)
- schema-mode + agentType together — floor disallowedTools still unioned
- schema-mode caller-abort takes priority over the StructuredOutput
terminal error (signal.aborted check at workflow-orchestrator.ts:489-490)
- override path dispose() runs in finally on the success path
- override path dispose() runs in finally on the terminate-mode-error path
Declined R1 finding: negative tests for invalid opt types (schema/model/
agentType passed null/number/empty-string). Adding upfront type
validation is scope creep — upstream does not, P1/P2 do not, and the
workflow tool is model-authored where these inputs are extremely
unlikely. Existing AJV / SubagentManager downstream errors are descriptive
enough. Will revisit if R2 makes a stronger case.
166/166 tests pass (workflow suite + adjacent + workflow-orchestrator).
typecheck + lint clean across packages/core, packages/cli,
integration-tests, sdk, webui.
* chore(core): P3 self-review R2 — vm-realm opts revive + error-msg sanitize + 12 tests
R2 of pre-push adversarial self-review on PR #5034. 6 diverse-lens
finders (60 agents, ~2.5M tokens, 24 min) over the R1-fix-applied
code, with 2 independent skeptics defaulting to refuted=true.
12 confirmed survivors after adversarial verify; decisions below.
Security (FIX):
- agent() wrapper in workflow-sandbox.ts now JSON-revives agentOpts
inside the vm runInContext block BEFORE passing them to the host
dispatch. Closes a Proxy/inherited-getter escape that P3 introduced
along with the user-supplied schema object: a script could have
wrapped agentOpts.schema in a Proxy whose getter ran host-side code
during SyntheticOutputTool construction / AJV compile. Same
mechanism as args / parallel-result revival.
- runOverridePath now sanitizes opts.agentType through
sanitizeForErrorMessage() (control chars → space) before
interpolation into the "agent type 'X' not found" error message.
Prevents a model-authored agentType containing CRLF / NUL from
fragmenting a single-line error across log records / OTLP fields.
Reuse-altitude (FIX):
- Added JSDoc block to WorkflowWorktreeIsolation interface
documenting each field's role for cleanup.
Test gaps (FIX, 12 new tests):
- agentType control-char sanitization regression
- dispose() runs in finally when subagent.execute throws
- isolation:'worktree' provision error branches (5):
nested parent / git unavailable / not a git repo / parent dirty /
createUserWorktree returns failure
- isolation:'worktree' cleanup branches (3):
removeUserWorktree fails / branchPreserved race / removeUserWorktree
throws — each preserves the worktree (or branch) with the right
user-facing suffix
- combinations (2): model + isolation:'worktree' threads model AND
provisions worktree; schema + isolation:'worktree' returns
structured payload verbatim (preserved suffix only on string return)
Test infrastructure: vi.mock'd GitWorktreeService at the module level
(partial mock; preserves the existing exports the unrelated
worktreeCleanup.ts depends on) with a per-test beforeEach reset.
Declined R2 findings (kept the R1 line):
- [major] Schema parameter upfront validation: same scope-creep
decline as R1. Upstream doesn't do it; AJV's downstream error is
descriptive enough.
- [major] Worktree provision extracted to shared util with AgentTool:
agreed in principle but out of P3 scope. A separate refactor PR
should land that with AgentTool maintainers in the loop.
178/178 tests pass (workflow + adjacent suites). typecheck + lint
clean across packages/core, packages/cli, integration-tests, sdk,
webui.
* fix(core): address wenshao R1+R2 review on Workflow P3 (PR #5034)
Round 1 (15:41) + Round 2 (17:24) review from wenshao surfaced 7 inline
findings across schema-mode dispatch correctness, worktree cleanup
coverage, and error attribution. Each fix is paired with a regression
test that was RED before the change landed.
T0 [Critical] Worktree leak when schema setup throws after provision
workflow-orchestrator.ts: outer try MOVED to start immediately after
provisionWorkflowWorktree. Previously the try opened only after
createSchemaConfigOverride / createSchemaModeState / signal listener
attachment — so any throw in those three (broken MCP server during
the per-call ToolRegistry rebuild was the trigger wenshao cited)
orphaned the just-provisioned worktree under .qwen/worktrees/.
Test: "isolation:'worktree' + schema setup throws → worktree is
still cleaned up" — simulates createToolRegistry failure during
createSchemaConfigOverride; asserts removeUserWorktree was called.
T1 [Critical] / T4 [H1] agentType + schema silently dead-ended
workflow-orchestrator.ts: schema-mode augmented config now (a)
appends ToolNames.STRUCTURED_OUTPUT to baseConfig.tools when the
allowlist is restricted (no '*' and doesn't already contain it), so
prepareTools / getFunctionDeclarationsFiltered doesn't filter
structured_output out of the subagent's surface; (b) preserves the
resolved agentType's persona by APPENDING the schema-contract
instruction block instead of replacing the systemPrompt outright.
Replace remains only on the ephemeral no-agentType path where
baseConfig.systemPrompt IS WORKFLOW_SUBAGENT_SYSTEM_PROMPT (schema
variant is its strict superset; avoids two near-identical prompts).
Tests: structured_output appears in the allowlist alongside the
agentType's existing tools; persona prompt is contained in the
effective systemPrompt.
T2 [Suggestion] / T5 [M1] Parent-abort listener leaked per schema call
workflow-orchestrator.ts: named listener stored at outer scope,
removed in the outer finally regardless of how the dispatch ended.
Previous `{ once: true }` only auto-removed on actual parent abort;
the happy-path schema dispatch — success capture / 3-failure abort
fires the CHILD controller without the parent ever aborting — left
the listener stuck on the per-run signal. With N schema calls per
workflow N listeners + N child-controller closures accumulated.
Test: 5 sequential schema dispatches over the same parent signal
end with zero live listeners.
T6 [M2] Terminate mode misdiagnosed as nudge exhaustion
workflow-orchestrator.ts: schema path now distinguishes
terminateMode before attributing failure to schema mode. TIMEOUT /
MAX_TURNS / ERROR throw the existing "did not complete (terminate
mode: X)" message that the non-schema path uses. Only the actual
schema-failure cases produce schema wording, and those are split:
attempts > 2 keeps the upstream-verbatim "(after 2 in-conversation
nudges)" wording; attempts === 0 throws an accurate "no validation
attempt — model produced plain-text content" instead of misleadingly
citing nudges that never happened. (The existing 0-call test was
updated to match the new accurate message; the 3-failure test
retains the verbatim wording.)
Tests: parametric over TIMEOUT/MAX_TURNS/ERROR asserting "did not
complete"; companion test pinning the verbatim wording to the
3-failure path.
T3 [Suggestion] Schema-mode JSON revival sentinel — clarified
workflow-sandbox.ts: added a block comment documenting that the
JSON-round-trip + null-on-throw is a SECURITY backstop (errors-as-data
convention from parallel/pipeline) rather than a contract path —
unreachable in production schema mode because the host return is
LLM tool_call args, always JSON-serializable. No behavior change.
Tests: 75/75 orchestrator + 111/111 sandbox/tool/limiter green.
typecheck + lint clean across packages/core and packages/cli.
R1+R2 self-review commits (e1c5ec79c / 62624a994) precede this commit
on the same branch — they predate wenshao's review and address
distinct findings; reviewer L1 (worktree-lifecycle unit coverage) is
already closed by R2's 11 worktree tests.
The message-time-on-hover feature (#5079) wraps each transcript message
with MessageTimestamp, but two sub-agent surfaces were left out and so
looked inconsistent with the rest of the transcript:
- The "Parallel agents · N/N done" box (ParallelAgentsGroup) renders
directly in MessageList, bypassing MessageItem/MessageTimestamp, so it
showed no time. Carry the first grouped launch's timestamp onto the
parallel_agents display item and wrap the box in MessageTimestamp.
- Each sub-tool row inside a SubAgentPanel's Tools list showed no time.
Wrap each row in a scoped hover tooltip (.toolTimeRow/.toolTimeTip,
kept separate from MessageTimestamp's .row/.tip so the nested tooltip
stays independent of the enclosing message's) keyed off the tool's
startTime.
Both reuse formatTimestamp for an identical HH:mm:ss (or dated) format
revealed on hover in the top-right corner, matching the main transcript.
Persist /loop tasks per-project under ~/.qwen/tmp/<project-hash>/ so they survive restarts; the default stays session-only. Missed one-shots are surfaced at startup confirm-first; overdue recurring jobs catch up once then resume. A per-project lock elects a single firing session across concurrent sessions, with takeover on owner exit. Recurring jobs expire after 7 days (final fire), and never-matching cron expressions are rejected at creation. Durable storage lives in the user runtime dir, not the working tree, so it is never committed or shared via the repo.
* 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>
* 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).
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.
* 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".
* 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>
* 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>
* 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.
* 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.
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.
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.
* 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.
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).
* 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>
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.