mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
|
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(core): add Agent Team foundation (experimental, flag-gated) First stage of re-porting the Agent Team feature (originally PR #2886) onto current main. The branch had diverged 362 commits behind a parallel rewrite of the agent runtime, so the feature is being re-applied stage by stage rather than merged. This stage lands the self-contained agents/team/ subsystem (TeamManager, mailbox, identity, tasks, leader permission bridge, test-utils) plus the team_create/team_delete and task_create/task_update/task_list tools, with the additive plumbing they need: - Config: TeamManager/TeamContext accessors, cleanupTeamRuntime, and isAgentTeamEnabled (settings or QWEN_CODE_ENABLE_AGENT_TEAM=1). - Tool registry: team/task tools registered lazily, gated on the flag. - Runtime hooks: completeOnIdle for one-shot teammates; args on the agent approval event; teammate-aware tool exclusion sets. - Backend types: TeamAgentHandle, optional getAgent, completeOnIdle. - New experimental.agentTeam setting. Everything is gated behind the experimental flag and inert by default. Build is green; team unit tests and all touched-file regressions pass. * feat(core): add send_message team routing (experimental) Stage 1 of the Agent Team re-port. Extends the send_message tool so it can route to a teammate (or "*" for broadcast) via TeamManager in addition to its existing background-task path, and supports the shutdown_request control message (leader-only). Recipient selection is a oneOf over `to`/`task_id`. Layered on top of main's classifier integration: send_message keeps its 'ask' default permission and forwards the routing fields + message to the AUTO classifier, since the message is an instruction the recipient executes. Re-adds the team-lifecycle E2E test, which now passes end to end (create -> tasks -> messages -> list -> update -> delete). * feat(core): let the Agent tool spawn named teammates (experimental) Stage 3 of the Agent Team re-port. Adds the `name` parameter to the Agent tool: when a team is active and a name is given, the call routes through TeamManager.spawnTeammate instead of launching a one-shot subagent. Without a team the call is rejected up front rather than silently falling back. The tool description advertises team coordination only when the experimental flag is on. Ported onto main's rewritten Agent tool. * feat(cli): render team_result/task_list tool displays (experimental) Stage 5 (partial) of the Agent Team re-port. Teaches the ToolMessage result renderer about the TeamResultDisplay and TaskListResultDisplay shapes so the team/task tools' output is shown via their returnDisplay text instead of a stringified object, and adds a JSON.stringify safeguard for any other non-string display object. The remaining CLI wiring (nonInteractiveCli + useGeminiStream team drivers, permissionController.handleTeammateApproval) is coupled to the turn-loop Teammate handling and will land together with Stage 4. * feat(core): treat teammate messages as top-level turns (experimental) Stage 4 of the Agent Team re-port. Adds SendMessageType.Teammate and includes it in isTopLevelInteraction so that a teammate message delivered to the leader resets the loop detector and opens an interaction span, the same as a user/cron/notification turn. Per the agreed minimal integration, teammate turns deliberately do NOT run the UserQuery/Cron block — they don't bump commit attribution, aren't recorded as user messages, and don't trigger auto-memory prefetch. That keeps the edit to main's restructured turn loop to a single condition. * feat(cli): drive teammates from the headless run loop (experimental) Stage 5 of the Agent Team re-port. Wires the non-interactive/headless run loop to the active team: it subscribes to TeamManager changes, drains teammate messages into the leader's conversation as SendMessageType.Teammate turns, waits for teammate activity when the leader has no pending tool calls, and routes teammate tool-approval requests through the session's permission channel (SDK in stream-json mode; YOLO/cancel fallback otherwise). Adds PermissionController.handleTeammateApproval and exposes it on the ControlService permission facade. Ported onto main's restructured run loop (which added its own cron/notification drain mechanism). * feat(cli): drive teammates from the interactive turn loop (experimental) Final Stage 5 piece of the Agent Team re-port. Wires the interactive (TUI) useGeminiStream hook to the active team: it subscribes to TeamManager, queues teammate messages, and drains them into the conversation as SendMessageType.Teammate turns when idle, guarded against racing the notification drain. Treats teammate turns like user/cron for image-format checks and new-prompt stats. Ported onto main's rewritten hook. * fix(core): declare proper-lockfile dependency for the team subsystem The Agent Team mailbox and task files import proper-lockfile, but the dependency was never declared in package.json, so a clean `npm ci` (as CI runs) failed to resolve the module — cascading into implicit-any and possibly-undefined errors in the same files. It built locally only because the working tree's node_modules already had the package from an earlier install. Adds proper-lockfile to packages/core dependencies and @types/proper-lockfile to root devDependencies (matching the original feature branch), and regenerates the lockfile. Build and typecheck are clean. * fix(team): address review findings on the agent-team subsystem (experimental) Triaged the unresolved review threads from the superseded PR #2886 against the re-ported code and applied the valid fixes: - task_update(status:'deleted') now enforces the same ownership guard as updateTask, so a teammate cannot delete another teammate's task. - Completing a task and adding a blocks edge in the same call no longer leaves the dependent permanently blocked by the just-completed task. - listTasks treats a momentarily-empty (mid-create) task file as a create in flight and skips it instead of quarantining and losing the task. - Fire-and-forget coordination calls (flush, auto-claim, unassign, poll) log rejections instead of surfacing as unhandled rejections. - pollLeaderInbox re-checks the leader callback after the awaited read so a detach during the read cannot throw or drop the batch. - scanIdleAgentsForTasks skips teammates with a pending shutdown. - broadcast uses allSettled so one terminated recipient does not fail the whole broadcast. - Hybrid tool-response+teammate turns reset the loop detector, preventing a false LoopDetected when a polling leader merges teammate messages. - useGeminiStream drains its teammate queue on a manager swap; the join event carries the teammate model for the UI tab label. - Removed dead consumeUnreadByType and an unreachable ENOENT branch. Verified: core unit tests (incl. new regressions for the delete guard, the complete+addBlocks re-block, and the empty-file create race) plus live L3 (3-agent) and L4 (4-agent) E2E, both clean. * fix(core): close ownership TOCTOU and lock-ordering hazard in agent-team tasks deleteTask checked ownership against a pre-lock read, so a concurrent claimTask/updateTask could reassign the owner between the check and the unlink — silently destroying another teammate's task. Acquire the lock first, then re-read and re-check ownership inside it before unlinking, mirroring updateTask. Reciprocal edge cleanup now runs after the lock is released (never holding two per-task locks at once) but before the single tasks-updated notification, so no listener observes a phantom blocker. blockTask issued its two updateTask writes via Promise.all; two calls over the same pair in opposite directions could deadlock on per-task locks. Serialize the writes to remove the lock-ordering hazard. * fix(core): harden agent-team message handling and auto-claim - Cap per-agent pending messages (MAX_PENDING_MESSAGES). The queue only drains when its recipient goes IDLE, so an unbounded queue let a single looping teammate balloon a busy teammate's memory; sendMessage now applies backpressure once the cap is reached. - Wrap auto-claimed task content (subject/description, authored by another agent) in a <task_content> envelope with a defensive instruction so it is treated as data, not as instructions to obey. - Surface fire-and-forget coordination failures (flush, auto-claim, unassign) to the leader's conversation. They were only logged via a namespaced debug logger, i.e. invisible in production, despite mapping to silent stuck-teammate / stuck-task symptoms. * fix(core): require approval for agent-team task_create/task_update A task's subject/description becomes the prompt an idle teammate auto-claims and executes with full tool access — the same privileged-sink shape as send_message. Both tools inherited the base default 'allow', which short-circuits the classifier in AUTO mode. Override getDefaultPermission to 'ask' so that injection path stays under the classifier / human-in-the-loop, matching send_message. * docs(core): correct completeOnIdle JSDoc for team teammates The JSDoc cited team teammates as the use-case for completeOnIdle:true, but teammates set it to false so they settle to IDLE (not COMPLETED) and stay alive for follow-up messages and auto-claim. Document the actual semantics and the invariant the leader's wait loop relies on. * fix(core): harden agent-team leader callback and task envelope - fireAndForget: wrap leaderMessageCallback in try/catch so a throwing callback cannot re-introduce the unhandled rejection the wrapper exists to prevent (enforces the documented 'must not throw from this catch'). - tryAutoClaimTask: nonce-tag the <task_content> envelope with the per-session envelopeNonce (same pattern as formatLeaderEnvelope) so a teammate-authored description cannot forge the closing tag and break out of the protected zone via a </task_content> payload. * fix(core): make deleteTask edge cleanup resilient to partial failure Use Promise.allSettled (was Promise.all) for post-unlink edge cleanup so a single failing dependent (corrupt JSON, EACCES, lock exhaustion) no longer skips notifyTasksUpdated for the dependents that were cleaned. Without this their blockedBy is cleared but scanIdleAgentsForTasks never re-runs, leaving them stuck idle with no recovery (the task file is already unlinked, so a retry returns false). Per-failure warnings are logged. * fix(cli): mount useTeamInProcess so teammate tabs render The hook bridging team TEAMMATE_JOINED events to agent-tab registration (useTeamInProcess) was authored but never mounted in AgentViewProvider — only useArenaInProcess was. As a result teammate tabs never registered and the teammate tab bar never appeared during in-process team runs. Mount useTeamInProcess alongside useArenaInProcess, and label teammate tabs by name rather than model (teammates inherit the leader's model, so a model label collapses to a generic "teammate" and is identical across the team). Add a regression test asserting the provider mounts the team bridge. * test(terminal-capture): add agent-team feature demo + capture fixes Add a standalone streaming demo of the agent-team feature that captures the full lifecycle and the teammate tab navigation into a single GIF (scenarios/agent-team-demo.ts). Supporting engine fixes: - capture(): scroll the xterm viewport to the live bottom before screenshotting, so a capture taken after an idle period shows the current state instead of stale top-of-buffer scrollback. - scenario-runner: skip scenarios/*.ts files with no default export (driver scripts that guard their own entrypoint), so batch runs don't choke. * fix(core): serialize in-process mailbox writers to fix Windows lock flakiness The concurrent-write test fired 10 writeMessage() calls at one inbox, each contending for the same proper-lockfile lock with a fixed, non-randomized backoff. On Windows, slower fs syscalls let the tail writers exhaust the retry budget before winning the lock, throwing ELOCKED ("Lock file is already being held") — a flaky failure that alternated pass/fail across CI runs. Add a per-inbox in-process Mutex (async-mutex, the pattern already used in jsonl-utils and writeContextFile) so same-process writers serialize in memory and only one reaches for the file lock at a time. The proper-lockfile lock stays inside the mutex to preserve cross-process safety between agent processes. Also randomize the lock backoff to de-synchronize genuine cross-process contenders. * feat(team): render teammate reports as a compact notification line A teammate's report was injected into the leader's conversation as a raw <teammate_message_<nonce>> envelope and rendered verbatim as a user bubble — a large, scaffolding-heavy block on screen for what is often the biggest payload in the feature. Adopt the two-text split the notification queue already uses: the full nonce-tagged envelope still goes to the leader's model, but the user now sees a compact "● <name> reported back" line in its place. The verbatim USER bubble is suppressed for SendMessageType.Teammate exactly as it is for Cron, and coordination-error notices get the same treatment. The leader callback now delivers both the model text and a display string built in TeamManager (where the structured sender/summary live), so the UI never parses the envelope. Headless is unchanged — it ignores the extra arg. * fix(terminal-capture): widen agent-team-demo Phase C budget so the GIF doesn't cut off The leader sits idle (no Main-view output) while teammates read their files, so Phase C captures no frames until a report lands — making maxPolls the real wall-clock budget. At 80 polls (~112s) a slow second scout could exhaust it before reporting, ending the GIF mid-run. Bump to 200 polls (~5min) so the capture outlasts the slowest scout plus the combined summary and delete; the loop still exits early on `deleted` and idle polls capture no frames, so the GIF doesn't bloat. * fix(core): separate task-content nonce; forward send_message summary Address two review findings on the agent-team messaging path: - The <task_content_…> envelope reused envelopeNonce — the per-session nonce the leader trusts to authenticate <teammate_message_…> blocks. Because the task-content prompt is delivered to the claiming teammate, a teammate could learn the nonce and forge a leader-trusted envelope. Use a dedicated taskContentNonce so the leader-trust nonce stays secret from teammates. - The SendMessage 'summary' param was dropped between the tool and the mailbox, so the leader UI always showed the '{name} reported back' fallback. Thread summary through sendMessage → writeMessage so it reaches formatLeaderDisplay. Adds regression tests for both. * fix(core,cli): harden agent-team messaging per review round 4 - task-content envelope uses a fresh per-claim nonce instead of a shared per-session one, so a teammate that learns one task's nonce can't forge a later task's closing tag to inject the next claimant. - team_delete wraps manager.cleanup() in try/catch and always resets the Config team state, so a cleanup failure no longer permanently wedges team_create for the rest of the session. - unassignTeammateTasks uses Promise.allSettled so one corrupt/locked task file no longer strands the remaining tasks on a terminated teammate; the caller's re-scan still fires. - non-interactive teammate-approval responses .catch() rejections to avoid an unhandledRejection if the teammate terminates mid-approval. - setupEventBridge warns when the backend can't provide an agent handle or event emitter instead of returning silently. * fix(core): don't let a failed dependent unblock abort task completion unblockDependents used Promise.all, so a single dependent failing (corrupt JSON, EACCES, lock exhaustion) rejected out of updateTask before the completed status was persisted — the task stayed in_progress on disk while already-processed dependents were unblocked, leaving the dependency graph inconsistent. Switch to Promise.allSettled with a debug warning per failure, mirroring the best-effort edge cleanup in deleteTask and unassignTeammateTasks. * fix(core): quarantine corrupt teammate inboxes; skip task scan when no agent is idle Review round 7. A corrupt teammate inbox previously made every writeMessage/consumeUnread re-throw on the same file, so the teammate could never receive another message (including shutdown requests) — while the leader inbox already self-healed via quarantine. readInboxRaw now renames the corrupt file to .corrupt-{ts} and continues on a fresh inbox; the leader-side offset clamps to 0 if the inbox shrank behind the poller so messages are re-surfaced rather than silently skipped. scanIdleAgentsForTasks now checks for idle members before reading the task board, avoiding a full tasks-directory scan on every task update while all agents are busy. Also document the restrictsOwnership field enumeration hazard and the intentional metadata/activeForm exclusion. * fix(core): re-check task ownership under the lock when unassigning a terminated teammate Review round 8. unassignTeammateTasks snapshotted in_progress tasks and then blind-wrote {status: pending, owner: null} per task, so a leader reassignment (or the dying teammate's final completion) landing between the snapshot and the per-task lock was silently reverted. Releases now go through an in-lock compare-and-set that skips the task when its owner or status no longer matches the snapshot. Also isolate task-update listeners (one throwing listener no longer starves the rest) and drop the lone const enum for subsystem consistency. * fix(core): harden team task file layer against partial writes and transient I/O - createTask claims the ID with an empty O_EXCL placeholder and fills it via temp-file + rename, so concurrent readers never see partial JSON (which the quarantine would have destroyed mid-create) - listTasks quarantines only on parse failures; transient read errors (EMFILE/EIO/EACCES) skip the file for one round instead of renaming a healthy task away, and the read fan-out is capped at 16 - updateTask / claimTask / releaseOwnedTask guard the in-lock readFile against ENOENT (resetTaskList and the quarantine rename run without per-task locks), mirroring deleteTask - cover releaseOwnedTask's three defensive branches with tests * fix(core): drain messages enqueued during the IDLE transition; settle abort on idle agents - a message enqueued from inside the synchronous IDLE STATUS_CHANGE emit (TeamManager's flush) landed after the run loop's final empty check while `processing` was still true — enqueueMessage would not restart the loop and the message stranded in a dead queue; the loop now re-checks the queue after `processing` flips false - abort() on an idle/initializing agent only set the signal: no loop was running to observe it, so the agent never reached a terminal status and allTeammatesTerminated()-style gates never fired; abort now settles CANCELLED directly when no loop is in flight - regression tests drive the real AgentInteractive (stub model, real loop) through send-during-idle-emit and abort-while-idle * test(core): align FakeAgent queue and abort semantics with AgentInteractive FakeAgent modeled a friendlier runtime than the one that ships: enqueueMessage processed inline (no queue, no processing flag, resurrecting terminal agents via unconditional RUNNING), which is exactly what masked the flush-into-dead-queue bug. It now queues while a round is in flight, drains before settling IDLE, drops messages after abort()/shutdown() like the real drained queue, and never resurrects a terminal agent. * fix(core): surface spawn failures, handle shutdown_rejected, envelope peer messages - spawnTeammate now checks the agent's status after spawnAgent resolves: start() reports chat-creation failure via FAILED without throwing, so the leader was told the teammate joined while sends were accepted into a queue that could never flush; a failed spawn now rolls back and surfaces the reason (with a terminal-status replay in setupEventBridge for the attach race) - shutdown_rejected now clears _shutdownPending: a teammate that declined once stayed excluded from auto-claim and kill-armed on any later "shutdown_approved" mention - peer-to-peer deliveries get a fresh-nonce envelope like leader deliveries, closing inline leader impersonation between teammates (deliberately not the leader-trust nonce, which must never reach teammate context) * fix(core): exclude workflow tool from teammates The teammate ALS identity propagates into anything a teammate spawns, so prepareTools() keeps choosing the teammate exclusion set for nested agents — without WORKFLOW in it, a teammate-launched workflow re-arms the O(k^n) recursive fan-out the subagent exclusion set prevents. * fix(core): make task tools visible to permission review; reject dependency cycles - task_create / task_update now project their content (subject, description, status, owner, edges) to the AUTO classifier — the base '' sentinel projected to an empty object, so the classifier ruled on task_create({}) and the 'ask' override was blind; the interactive confirmation now shows the description (truncated), since that text is what a claiming teammate executes - task_update rejects self-edges and dependency cycles instead of silently persisting a graph that auto-claim can never unblock - regression tests pin the 'ask' default and a non-empty classifier projection for both tools * fix(core): reclaim stale teams on team_create instead of wedging the name Nothing deletes team dirs on normal exit (only an explicit team_delete does), so every Ctrl+C, completed headless run, or crash permanently wedged the team name behind createTeamFile's wx-exclusive create, with manual rm -rf as the default recovery. team_create now records the owner identity (leadSessionId + leadPid) and, on EEXIST, reclaims the team when the recorded lead process is gone (or is this process); only a live concurrent owner keeps the name refused. * fix(cli): pass teammate envelopes straight to the model, skipping shell/@/slash preprocessing Teammate envelopes are model-authored text already rendered as a notification line by the teammate drain, but they still flowed through the user-input preprocessing: with shell mode active a teammate report was EXECUTED as a shell command, and a leading / or an @path was reinterpreted against the leader's session. They now early-return like Notification. * fix(core): exclude Teammate from UserPromptSubmit hooks and record it in chat history Teammate envelopes are machine-driven re-entries like Cron and Notification: user-authored UserPromptSubmit hooks must not fire on (or block) internal coordination traffic. They also never reached any chat-recording path — record them like notifications so a resumed session restores the same compact info line the live UI rendered. * fix(cli): stop teammate-approval rejections from escaping as unhandled rejections The stream-json listener voided handleTeammateApproval's promise while the handler's own error path re-issues a respond() that can reject (teammate terminated mid-request) — an unhandledRejection that can take down an SDK session. The call site now catches like its headless siblings, and the controller's catch-path respond(Cancel) is wrapped so the method never rejects out of its own error path. * test(core): add getSessionId to team-lifecycle mock config |
||
|---|---|---|
| .. | ||
| baselines | ||
| cli | ||
| concurrent-runner | ||
| fixtures | ||
| hook-integration | ||
| interactive | ||
| sdk-typescript | ||
| terminal-bench | ||
| terminal-capture | ||
| channel-plugin.test.ts | ||
| globalSetup.ts | ||
| test-helper.ts | ||
| test-mcp-server.ts | ||
| tsconfig.json | ||
| vitest.config.ts | ||
| vitest.terminal-bench.config.ts | ||