* 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
* fix(skills): use full YAML parser for frontmatter to support block scalars
The custom `yaml-parser.ts` does not handle YAML block scalar syntax
(`>` folded, `|` literal). When a SKILL.md uses `description: >` with
indented continuation lines, only the `>` character is captured as the
description value.
Switch both skill loading paths (`skill-load.ts` and `skill-manager.ts`)
to use the `yaml` npm package (already installed for hooks parsing) as
the primary parser, with a fallback to the simple parser for malformed
YAML that the strict parser rejects.
This also simplifies `skill-manager.ts` by removing the special-case
re-parse for `hooks:` — the full parser now handles all fields uniformly.
Closes#4869
* fix(skills): address review feedback — explicit dep, null guard, shared parser
- Add `yaml` to `packages/core/package.json` explicit dependencies
- Move the full-YAML-with-fallback logic into `yaml-parser.ts` as the
single `parse()` entry point, eliminating the duplicated wrapper
- Add null/type guard so `yaml.parse('')` (returns null) falls back
cleanly instead of throwing TypeError
- Add debug logging when the full parser fails and fallback triggers
- Tighten `|` block scalar test to use `toBe` instead of `toContain`
- Move block scalar tests to `yaml-parser.test.ts` (no mock interference)
- Add `>-` strip chomping test
* chore: make parseSimple private, remove stale yaml-dep comment
- `parseSimple` is an internal fallback, no external caller needs it —
stop exporting it via `index.ts`'s `export *`
- Remove the outdated "yaml package would need to be added" comment in
`subagent-manager.ts` since `yaml` is now an explicit dependency
* test: add comprehensive edge-case tests, use YAML 1.2 core schema
- Use `schema: 'core'` to prevent date-like strings (e.g. `2024-01-01`)
from being coerced into Date objects, which would break parse→stringify
roundtrips in claude-converter and subagent-manager
- Add yaml-parser unit tests for: Date non-coercion, bare keys returning
null, explicit null/tilde, yes/no as strings (YAML 1.2), empty input,
comment-only input, malformed YAML fallback
- Expand skill-load real-parser integration tests: literal block scalar,
strip-chomped folded, allowedTools array, complex mixed frontmatter,
malformed YAML graceful fallback
* fix: use loose null check for optional frontmatter fields
`yaml.parse` returns `null` for bare keys (`hooks:` with no value),
while the old simple parser returned `''`. The existing `!== undefined`
guards don't catch null, which would cause a TypeError in
parseHooksConfig and a misleading error for allowedTools.
Change to `!= null` (catches both null and undefined) in both
skill-load.ts and skill-manager.ts.
* refactor: normalize null values in yaml parser instead of caller-side guards
Move the null→undefined normalization into `parse()` via `stripNullValues`
so that callers keep using strict `!== undefined` checks. This avoids the
loose `!= null` pattern and fixes the contract for all five call sites
(skill-load, skill-manager, subagent-manager, claude-converter,
rulesDiscovery) in one place.
* fix(yaml-parser): address review round 2 — security hardening and dedup
- Prevent prototype pollution via Object.create(null) in both
stripNullValues and parseSimple (fixes __proto__ injection vector)
- Recursively sanitize nested objects (Date/Uint8Array/null/__proto__)
- Upgrade fallback/non-object log level from debug to warn
- Filter !!timestamp/!!binary explicit tags (belt-and-suspenders)
- Extract shared parseAllowedToolsField helper to deduplicate
skill-load.ts and skill-manager.ts
- Add runtime type guard for hooksRaw in skill-manager.ts
- Fix fallback tests to use genuinely invalid YAML input
* fix(yaml-parser): apply stripNullValues to parseSimple fallback path
The fallback path returned parseSimple() output directly, bypassing
stripNullValues(). This caused inconsistent output: the main path
stripped null values so callers could use `!== undefined`, but the
fallback preserved them. Wrap the fallback return in stripNullValues()
and add a test verifying null-stripping consistency across both paths.
* feat(telemetry): propagate W3C traceparent on outbound LLM requests
Part 1 of #4384 (sub-issue of #3731 P3 deeper observability).
Today qwen-code's only OTel instrumentation is `HttpInstrumentation`,
which only patches Node's `http`/`https` modules. The `openai` and
`@google/genai` SDKs use `globalThis.fetch` (undici), so outbound LLM
requests carry no `traceparent` header and trace context dies at the
qwen-code process boundary.
Adds `@opentelemetry/instrumentation-undici@0.14.0` (peer-compatible
with the installed `@opentelemetry/instrumentation@0.203.0`) and wires
it into `initializeTelemetry()` next to the existing
`HttpInstrumentation`. Default propagator (W3C tracecontext + baggage)
remains unchanged — no explicit `textMapPropagator` needed.
`ignoreRequestHook` skips OTLP exporter endpoints to avoid the
classic feedback loop (OTel SDK uses fetch to upload OTLP data; without
the hook each upload would create a span that gets uploaded, infinitely).
Configured `otlpEndpoint` / per-signal endpoints are stripped of trailing
slash and query string for robust prefix matching against undici's
`request.origin + request.path`.
Outbound LLM calls now also produce a client-side HTTP span (separating
network TTFB / transfer time from the existing `api.generateContent`
total-duration span).
Design doc: docs/design/telemetry-outbound-propagation-design.md
(Part A — traceparent; Part B — session id header — lands in a
follow-up PR per the design's split rationale.)
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* fix(telemetry): harden OTLP feedback-loop guard + slim lockfile diff
Review feedback on #4390:
1. CI was failing on npm ci because the lockfile was generated with npm 11
locally (it sprinkles `peer: true` annotations npm 10 reads differently
and rejects). Regenerated with npm 10 (matching CI's Node 22.x default),
so the diff vs main is now 18 lines (the actual instrumentation-undici
entry) instead of 105 lines of npm-version drift noise.
2. (Copilot inline at sdk.ts:330) `otlpUrlPrefixes` was derived from raw
Config strings, so a settings.json `"otlpEndpoint": "\"http://...\""`
(quoted) or trailing `#fragment` would silently miss the prefix match
and reintroduce the feedback loop the hook exists to prevent. Replaced
the regex-based suffix trim with a WHATWG URL parser:
- strips ?query, #fragment, trailing slash
- trims symmetric ASCII quotes a user may have placed in settings.json
- falls back to safe suffix trimming if URL parsing fails (misconfigured
endpoint still gets SOME protection)
3. (CodeQL inline) Replaced the `/\?.*$/` regex in ignoreRequestHook with
`indexOf('?')`/`indexOf('#')` slicing for ReDoS hygiene. The regex was
linear in practice but flagged as polynomial — using indexOf removes
the ambiguity and is arguably simpler.
Added 3 tests in sdk.test.ts covering the new normalizations (#fragment
on incoming path, quoted endpoint, #fragment on configured endpoint).
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* feat(telemetry): propagate X-Qwen-Code-Session-Id on outbound LLM requests
Part 2 of #4384. Stacks on top of PR #4390 (traceparent via undici).
Adds a product-namespaced HTTP header X-Qwen-Code-Session-Id to every
outbound LLM request when telemetry is enabled, so server-side ingestion
can correlate observed requests with qwen-code session metric/log records.
Pattern matched from claude-code (X-Claude-Code-Session-Id, verified at
src/services/api/client.ts:108 in their open-source repo).
Critical design decision (design doc section 4.3): the OpenAI / Anthropic
providers use a per-request fetch wrapper rather than the SDK defaultHeaders
option, because content-generator SDK clients are constructed once and NOT
recreated on /clear-triggered session resets (Config.resetSession updates
this.sessionId but the contentGenerator keeps using the stale header value).
Reading config.getSessionId() from inside the wrapper at request time gives
the live value.
Gemini provider uses static httpOptions.headers — @google/genai HttpOptions
interface does not expose a fetch hook (only headers, baseUrl, apiVersion,
timeout, extraParams). This is a known limitation: after session reset,
Gemini X-Qwen-Code-Session-Id stays stale until the contentGenerator is
recreated. Documented in telemetry.md and the design doc section 8.6;
spans/logs continue to carry the live session id for trace/log correlation.
Lazy-invalidate fix is a follow-up sub-issue.
Header is omitted when telemetry is disabled OR when getSessionId returns
an empty string (some HTTP middleware rejects empty header values).
Integration sites:
- packages/core/src/core/openaiContentGenerator/provider/default.ts
(base class — automatically covered by deepseek/minimax/mistral/
modelscope/openrouter subclasses; openrouter calls super.buildHeaders)
- packages/core/src/core/openaiContentGenerator/provider/dashscope.ts
(overrides buildClient — must be touched separately; QwenContentGenerator
inherits via this provider)
- packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts
- packages/core/src/core/geminiContentGenerator/index.ts (factory function,
not the GeminiContentGenerator class — no signature change)
End-to-end verification (local HTTP server in tmux):
PASS: traceparent + X-Qwen-Code-Session-Id on every LLM request
PASS: session id refreshes after simulated /clear (staleness regression
guarded by llm-correlation-fetch.test.ts)
PASS: OTLP upload traffic not traced (no feedback loop — PR A
ignoreRequestHook working)
Robot generated with Qwen Code https://github.com/QwenLM/qwen-code
* fix(telemetry): R2 review fixes — critical correctness + tsc + boundary safety
Adopts 7 review findings from wenshao on #4390 (+ duplicates from now-closed
#4393). Critical bugs first, polish second.
CRITICAL:
1. tsc TS2322 — wrapper return type incompatible with Anthropic SDK Fetch.
`typeof fetch` (Node WHATWG, 2 overloads) is not structurally assignable
to Anthropic's narrower `Fetch = (input: RequestInfo, init?) => ...`,
even though they're call-compatible at runtime. Make wrapper generic
`<TFetch extends FetchLikeLoose>` so callers preserve their exact fetch
signature; cast the Anthropic call site through `unknown` with a comment
explaining why.
2. tsc TS2352 / TS2493 — `baseFetch.mock.calls[0]![1] as RequestInit` was
out-of-bounds when wrapped was called with no init arg. Replaced with a
`makeFetchMock()` helper returning typed accessors.
3. normalizeOtlpPrefix catch fallback was DANGEROUS — a config of `"http"`
produced prefix `"http"` which `startsWith`-matched every outbound HTTP
request → silently disabled ALL instrumentation (no client spans, no
correlation header — defeats the entire feature). Fixed: catch returns
undefined + diag.warn. Misconfigured endpoint loses its feedback-loop
guard (acceptable) instead of disabling all guards (catastrophic).
4. `url.startsWith(prefix)` matching was NOT boundary-safe — port collision
(`:4318` matches `:43180`), hostname suffix collision (`otlp.example.com`
matches `otlp.example.com.evil.net`), path-segment collision (`/v1`
matches `/v1foo/x`). Replaced with origin-equality + path-prefix +
boundary-char check (next char must be `/`, `?`, `#`, or end-of-string).
5. HttpInstrumentation also lacked the OTLP feedback-loop guard. The OTLP
HTTP exporter (`@opentelemetry/exporter-trace-otlp-http`) uses node:http
(patched by HttpInstrumentation, NOT undici). Without this, every OTLP
upload batch creates a parasitic client span → feedback loop. Added
`ignoreOutgoingRequestHook` that reuses the same `matchesOtlpPrefix` /
`stripPathSuffix` helpers as the undici instrumentation.
SAFETY:
6. Request input + undefined init dropped the Request's own headers
(Authorization etc.) because `new Headers(undefined)` → `{...init, headers}`
replaced them with just our session header. Fix: when input is a Request
and init.headers is unset, seed from input.headers before adding ours.
7. Wrapped fetch had no try/catch — a throwing Config getter or Headers
constructor would propagate as TypeError and break the LLM request path.
Wrapped header construction in try/catch; on failure, fall through to
baseFetch with original init (no header) + diag.warn. Telemetry must
never break the model call.
COVERAGE:
- 3 new sdk.test.ts boundary tests (port/host/path)
- 1 new sdk.test.ts normalizeOtlpPrefix catch-branch coverage
- 1 new sdk.test.ts HttpInstrumentation OTLP guard test
- 1 new sdk.test.ts proxy-mode wrapped-fetch test (default.test.ts)
- 1 new anthropic test asserting wrapped fetch installed on Anthropic SDK
- 2 new llm-correlation-fetch.test.ts (Request-headers preservation + try/catch fall-through)
All 668 tests pass (1 pre-existing Anthropic User-Agent failure on main is
unrelated). tsc clean.
Declined: #10 DRY-refactor of baseFetch extraction across 3 sites — the
duplication was pre-existing (default/dashscope buildClient was already
near-identical), refactoring is a separate cleanup PR not gated by this
feature. Will reply on the thread.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* chore(deps): allow patch updates for @opentelemetry/instrumentation-undici
Switch from exact pin `0.14.0` to `^0.14.0` for consistency with the rest
of the `@opentelemetry/*` deps in this block (all carated).
For 0.x semver, npm treats `^0.14.0` as `>=0.14.0 <0.15.0`, so patch
updates within the 0.14.x line — which are tied to the same
`@opentelemetry/instrumentation@0.203.x` peer — flow in via `npm update`
without requiring a manual package.json edit. A bump across the 0.x
minor (e.g. 0.15.x) would shift the instrumentation peer compatibility
and still requires explicit attention, which the caret correctly blocks.
Per review feedback on #4390 (wenshao).
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* test(telemetry): stub getTelemetryEnabled + getSessionId in Gemini factory tests
The X-Qwen-Code-Session-Id commit added a `staticCorrelationHeaders(gcConfig)`
call inside the Gemini content generator factory. That helper reads
`gcConfig.getTelemetryEnabled()` and `gcConfig.getSessionId()` per request.
Both pre-existing Gemini tests in `contentGenerator.test.ts` build a minimal
partial Config stub via `as unknown as Config` and only stub the methods the
factory used to need. The new call path now hits the unstubbed methods at
runtime, surfacing as `TypeError: config.getTelemetryEnabled is not a function`
on all three CI platforms.
Add the two missing stubs to both test cases. The Gemini factory continues
to ignore the values when telemetry is off — these stubs only have to exist,
not return anything in particular.
Local check ran the full test suite for the four directories `/loop` covers
plus `src/core/contentGenerator.test.ts` itself; all green. Also re-ran the
other test files that build partial Config mocks via the same idiom
(`client.test.ts`, `config.test.ts`, `nextSpeakerChecker.test.ts`,
`content-generator-config.test.ts`) — none exercise the new code path.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* fix(telemetry): R3 review fixes — port + protocol + quote + safety
Four issues found by wenshao reviewing the R2 boundary-safety pass on PR
#4390. All four close gaps where the OTLP feedback-loop guard or the
correlation-header path could fail silently.
1. **Port normalization mismatch** (sdk.ts ignoreOutgoingRequestHook):
`normalizeOtlpPrefix` builds prefixes via `URL.origin`, which strips
default ports (`:80` for http, `:443` for https). The hook reconstructed
request origin manually as `${proto}://${host}${portPart}`, keeping the
port. Result: prefix `http://collector` (no explicit port) didn't match
a request to `http://collector:80/v1/traces` because their `.origin`
differed → guard bypassed → feedback loop. Now the reconstructed origin
is also routed through `URL` so both sides apply the same default-port
stripping.
2. **HTTPS proto silent fallback** (sdk.ts ignoreOutgoingRequestHook):
The `(req.protocol && ...) || 'http'` fallback would silently mis-bucket
HTTPS requests as HTTP when `req.protocol` was unset, so HTTPS OTLP
endpoints couldn't match their prefix. Changed to fail open: when proto
can't be determined, return false (request gets instrumented). Worst
case is a parasitic client span — observable, recoverable — versus the
previous unbounded silent feedback loop. Picked fail-open over the bot's
port-based heuristic because non-standard HTTPS ports break the
heuristic but not fail-open.
3. **Quote-stripping divergence** (sdk.ts normalizeOtlpPrefix):
`parseOtlpEndpoint` (line 109) uses `/^["']|["']$/g` which strips
asymmetric leading/trailing quotes; `normalizeOtlpPrefix` previously
only stripped symmetric pairs. A settings.json typo like `"value'` would
let the exporter connect (parseOtlpEndpoint trims) but leave the guard
returning `undefined` (normalizeOtlpPrefix rejected) → parasitic loop.
Aligned `normalizeOtlpPrefix` to the same lenient regex.
4. **`staticCorrelationHeaders` missing try/catch** (llm-correlation-fetch.ts):
`wrapFetchWithCorrelation` already catches all internal exceptions and
falls through to baseFetch — same "telemetry must never break LLM path"
contract was missing on the static-headers helper. A throw here would
propagate up to the Gemini content-generator factory and crash
content-generator init for the whole session. Wrapped the body in
try/catch with `diag.warn` fall-through to `{}`.
Tests: added 4 regression tests covering each scenario:
- default-port HTTP request matched against portless prefix (1)
- hook returns false when req.protocol missing on https endpoint (2)
- asymmetric-quoted endpoint normalizes for guard parity (3)
- staticCorrelationHeaders returns {} when config getter throws (4)
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* docs(telemetry): fix misleading "BOTH" wording in wrapFetchWithCorrelation
The comment described the header-seeding logic as merging "BOTH the
init.headers AND the Request's own headers", but the two branches are
mutually exclusive — `new Headers(init?.headers)` runs unconditionally
(empty Headers when init.headers is undefined), and the Request-headers
copy only runs when init.headers is undefined. So in practice it's
either-or, not BOTH.
Reworded to match the actual logic per #4390 review feedback (wenshao).
Behavior unchanged.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* fix(telemetry): strip port from req.host fallback + document undici scope
Two issues found by wenshao reviewing the R3 boundary-safety fixes on PR
#4390.
1. **`req.host` may already include `:port`** (sdk.ts ignoreOutgoingRequestHook):
When `req.hostname` is absent and `req.host` is the fallback, the value
may already be `"collector:4318"`. Naively appending `:${req.port}`
produced `"http://collector:4318:4318"` → `new URL()` rejects → catch
returns false → silent guard bypass for that request. Currently
unreachable because `@opentelemetry/otlp-exporter-base` always sets
`hostname` from WHATWG URL parsing, but the fallback exists in the
code and must be correct — a future OTLP transport that emits `host`
without `hostname` would silently trigger the feedback loop. Strip the
port when falling back; bracketed IPv6 literals like `"[::1]:443"`
keep their bracketed host intact.
2. **Undici scope honesty** (telemetry.md):
Previous docs framed the propagation as "outbound LLM requests", but
`UndiciInstrumentation` actually patches `globalThis.fetch` for the
whole process — `WebFetch`, MCP clients, IDE extension calls all get
spans + `traceparent` injection too. Added a "Scope: all fetch() calls,
not just LLM" subsection covering: (a) trace ID leakage to third-party
URLs (the user-supplied destinations of `WebFetch` see our trace ID;
not secret per W3C but worth knowing); (b) non-LLM span volume
inflating OTLP batches with a workaround tip. Per-destination scoping
toggle deferred as a follow-up — out of scope for this PR.
Added regression test for the host:port-fallback path. Test exercises
the previously broken combination (hostname absent, host carries port)
through the existing test harness.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* feat(telemetry): scope X-Qwen-Code-Session-Id to first-party hosts by default
Address LaZzyMan's REQUEST_CHANGES review of PR #4390.
The original design injected `X-Qwen-Code-Session-Id` on every outbound
LLM request gated only by `telemetry.enabled`. Review caught that this
broadcasts a stable cross-request client identifier to every configured
third-party provider (OpenAI, Anthropic, OpenRouter, MiniMax, ModelScope,
Mistral, vanilla Gemini, ...), which the claude-code precedent does NOT
justify — claude-code is a first-party Anthropic→Anthropic flow; qwen-code
is an open-source CLI connecting to many providers.
Fix: add a host allowlist with a deliberately narrow default. The header
is now only attached to destinations whose hostname matches:
dashscope.aliyuncs.com
dashscope-intl.aliyuncs.com
*.dashscope.aliyuncs.com
*.dashscope-intl.aliyuncs.com
*.alibaba-inc.com
*.aliyun-inc.com
This is exactly the set where the LLM provider, the upstream telemetry
backend (ARMS Tracing), and qwen-code itself are the same legal entity —
mirroring the first-party claude-code pattern and preserving the real
product value (server-side trace stitching against DashScope) without
exposing the session id to third parties.
Operators with broader correlation requirements override via:
"telemetry": {
"sessionIdHeaderHosts": ["*"] // restore broadcast
"sessionIdHeaderHosts": [] // fully disable
"sessionIdHeaderHosts": ["api.example.com", "*.foo"] // custom allowlist
}
Implementation:
- NEW `telemetry/trusted-llm-hosts.ts`: `DEFAULT_SESSION_ID_HEADER_HOSTS`
+ `matchesTrustedHost(hostname, patterns)` + `extractRequestHost(input)`.
Pattern syntax is intentionally tiny (bare hostname OR `*.suffix`,
dot-anchored to reject `evil-alibaba-inc.com` style attacks). Unit-tested
in dedicated test file including TLD/sub-domain attack vectors.
- `wrapFetchWithCorrelation` (openai + anthropic providers): resolves the
allowlist at wrap time (Config snapshot), inspects each request's
destination URL inside `correlationFetch`, falls through to baseFetch
for non-trusted destinations. Wildcard escape hatch via `["*"]`.
- `staticCorrelationHeaders` (Gemini factory): now takes an optional
`destinationUrl` and applies the same host gate. The Gemini SDK default
endpoint `generativelanguage.googleapis.com` is NOT on the default
allowlist, so vanilla Gemini calls receive no header — matching the
"first-party only" scope. Operators who put the Gemini SDK on a
DashScope-compatible endpoint via `baseUrl` get the header naturally.
- `Config.getTelemetrySessionIdHeaderHosts()` getter +
`TelemetrySettings.sessionIdHeaderHosts` interface field + JSON schema
entry in `settingsSchema.ts`. Wired through `resolveTelemetrySettings`.
- Defensive optional-chaining + try/catch on the Config getter call at
wrap time so partial test mocks (or pre-getter Config implementations)
fall back to the default allowlist rather than crashing buildClient.
Tests: 12 new cases covering host match/skip on default allowlist,
sub-domain handling, TLD-suffix attack rejection, `["*"]` broadcast
override, `[]` full-disable, custom operator allowlist, unparseable
destination (fail closed), and the three Gemini factory paths
(googleapis.com default → omit; DashScope `baseUrl` → inject; custom
allowlist → inject).
Docs updated in `docs/developers/development/telemetry.md` Session
correlation header section, including override examples and the new
Gemini host-gate semantics.
Closes the LaZzyMan REQUEST_CHANGES blocker. The cross-vendor
fingerprint-broadcast failure mode is now opt-in rather than default,
restoring the first-party-only semantics that make the claude-code
precedent applicable.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* fix(telemetry): R5 review fixups — Vertex destination + ["*"] trim + docs
Self-review pass on commit 1c8528a56 (host-scoped session-id header):
1. **Vertex AI destination guessing**
(geminiContentGenerator/index.ts)
`@google/genai` routes to `{region}-aiplatform.googleapis.com` (not
`generativelanguage.googleapis.com`) when `vertexai: true` and no
`baseUrl`. The previous "guess generativelanguage" default would have
mis-bucketed Vertex traffic under any operator-supplied allowlist that
covered the public Gemini endpoint but not the Vertex one. Today
invisible (both off the default allowlist), but a latent gotcha for
operators tuning `telemetry.sessionIdHeaderHosts`.
Fix: pass `undefined` when `config.baseUrl` is unset (fail-closed —
no header). Operators who want correlation against Google endpoints
must set `baseUrl` explicitly, which is also the SDK's input for
destination resolution.
2. **`["*"]` broadcast escape hatch tolerates whitespace**
(llm-correlation-fetch.ts)
`[" * "]` (a settings.json hand-edit with a stray space) previously
silently fell back to "no host matches" — the opposite of operator
intent. Now `.trim()` before comparing, so common whitespace mistakes
still trigger broadcast.
3. **Doc note on wrap-time allowlist snapshot**
(llm-correlation-fetch.ts JSDoc)
The session id is read live per-request, but `trustedHosts` is
snapshotted once at `wrapFetchWithCorrelation` call time. Spell this
out in the JSDoc so a future maintainer doesn't read the live
`getSessionId()` and assume the allowlist is the same shape.
4. **Defensive test coverage**
(trusted-llm-hosts.test.ts, llm-correlation-fetch.test.ts)
Added: extractRequestHost with explicit port / userinfo / query /
fragment / IPv6 bracket form. Whitespace `[" * "]` broadcast test.
IPv6 case documents the "bracketed → never matches" behavior is
intentional fail-closed for the named-host allowlist scope.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* chore: regenerate settings.schema.json for sessionIdHeaderHosts
Lint check `Check settings schema is up-to-date` failed because the
checked-in `packages/vscode-ide-companion/schemas/settings.schema.json`
wasn't regenerated after adding `telemetry.sessionIdHeaderHosts` to
`settingsSchema.ts` in commit 1c8528a56. Regenerated via
`npm run generate:settings-schema`.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* docs(design): update telemetry-outbound-propagation design for R3 host-allowlist scoping
Adds a "修订历史" header table at the top and a new §11 "R3 修订 —
Host-Allowlist Scoping for X-Qwen-Code-Session-Id" capturing what changed
after LaZzyMan's REQUEST_CHANGES review, why, and how. Inline pointers
added at §3.1, §3.2, §4.3, §4.4, §9 (claude-code comparison table) to
point readers at §11 — original prose preserved as a record of the
decision path rather than rewritten in place.
Concretely §11 covers:
- The three-step LazzyMan critique and why R1's "broadcast to all
providers" was structurally wrong for an open-source multi-provider CLI
- The default allowlist (`DEFAULT_SESSION_ID_HEADER_HOSTS`) and its
semantic alignment with the DashScope provider detector
- Pattern grammar (bare hostname / `*.suffix` dot-anchored), the
TLD-suffix attack vectors it rejects, why no regex / port-aware globbing
- `wrapFetchWithCorrelation` host gate, wrap-time vs request-time
semantics, `[" * "]` whitespace tolerance
- `staticCorrelationHeaders` `destinationUrl` parameter, Gemini factory's
fail-closed treatment of unset `baseUrl` (avoids the Vertex
vs `generativelanguage.googleapis.com` ambiguity)
- All R3 file changes mapped to the original §5 file-change list
- Mapping of LazzyMan's three concerns to R3's responses
- §10 future-work additions: `traceparent` per-destination toggle,
`X-Qwen-Code-Request-Id`, IPv6 allowlist syntax
No code changes; documentation only.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* fix(telemetry): defensive allowlist normalization + positive proxy test
Three issues found by wenshao reviewing the R3 host-allowlist scoping.
1. **[Critical] `broadcastAll` outside safety try/catch**
(llm-correlation-fetch.ts wrapFetchWithCorrelation)
The try/catch only fires when `getTelemetrySessionIdHeaderHosts()`
throws. If it returns a malformed value — a bare string (settings.json
typo `"sessionIdHeaderHosts": "host"` instead of `["host"]`), an array
containing `null`/`undefined`/number entries, or whitespace-padded
entries — `.some((p) => p.trim() === '*')` throws TypeError at
buildClient time, bricking the LLM session before the first prompt.
`staticCorrelationHeaders` already handled this via its end-to-end
try/catch but the sister helper diverged. Settings loader does no
runtime schema validation so this is reachable via a single typo.
Fix: normalize the allowlist at wrap time:
1. catch a throwing getter (existing)
2. reject non-array → default allowlist (NEW — bare string typo)
3. filter out non-string elements (NEW — [null, ...] typo)
4. trim every surviving entry uniformly (NEW — see #2 below)
Then `trustedHosts.includes('*')` instead of `.some((p) => p.trim() === '*')`,
since patterns are already pre-trimmed.
2. **Trim asymmetry between `*` detection and host-pattern match**
(llm-correlation-fetch.ts)
`[" * "]` was tolerated (trimmed before `===` compare) but
`[" dashscope.aliyuncs.com "]` silently never matched. The
normalization above fixes this by trimming uniformly upstream.
3. **Proxy fetch test: only negative assertions**
(openaiContentGenerator/provider/default.test.ts)
The test asserted `callArg.fetch !== proxyFetch` and `!== globalThis.fetch`
but both passed for ANY wrapper, including a buggy one that
accidentally wraps globalThis.fetch instead of proxyFetch. Added a
positive assertion: call the wrapped fetch and verify proxyFetch was
the delegation target.
Tests: 4 new cases — whitespace-padded host pattern, bare-string
malformed config (both wrapper and static), null/number-containing
array malformed config (both wrapper and static), positive proxy fetch
delegation. All pass; pre-existing Anthropic User-Agent failure
unrelated.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* refactor(telemetry): split outbound correlation out of telemetry scope (R4)
Address LaZzyMan round-8 follow-up review on PR #4390: even though R3's
host allowlist made the default behavior safe, the meta-architectural
concern remains: telemetry's namespace and consent flow shouldn't quietly
extend to wire-level behavior aimed at third-party LLM provider request
streams. The recipient sets differ; the consent decisions differ; they
deserve separate namespaces, separate threat models, separate PRs.
This commit (called R4 in the design doc) collapses the PR scope so it
lands ONLY telemetry observability work:
REMOVED from this PR:
- packages/core/src/telemetry/llm-correlation-fetch.ts(.test.ts)
- packages/core/src/telemetry/trusted-llm-hosts.ts(.test.ts)
- telemetry.sessionIdHeaderHosts setting + Config getter +
resolveTelemetrySettings wiring + settingsSchema entry
- wrapFetchWithCorrelation usage from four provider construction
points (default.ts, dashscope.ts, anthropicContentGenerator.ts,
geminiContentGenerator/index.ts)
- All session-id provider tests across the four providers + the
contentGenerator.test.ts mock stub
- "Session correlation header" section in telemetry.md
ADDED:
- OutboundCorrelationSettings interface in packages/core/src/config/config.ts,
standalone top-level namespace separate from TelemetrySettings —
SECURITY-RELEVANT label, all defaults off
- Config.getOutboundCorrelationPropagateTraceContext() getter
- outboundCorrelation top-level entry in settingsSchema.ts with
propagateTraceContext: { default: false } and explicit
SECURITY-RELEVANT framing in the description
- CLI config-load pipeline passes settings.outboundCorrelation into
ConfigParameters
- NOOP_PROPAGATOR (TextMapPropagator no-op) in sdk.ts, conditionally
installed on NodeSDK when propagateTraceContext is false (default).
When true, omits textMapPropagator from NodeSDK options so the SDK
keeps its default W3C composite propagator
- 2 new sdk.test.ts cases covering the propagator gate behavior
UNCHANGED:
- UndiciInstrumentation registration + OTLP feedback-loop guard +
HttpInstrumentation OTLP guard from R2/R3 stay intact — they are
pure telemetry (client HTTP spans into the operator's own OTLP
collector), no wire-level data egress
- Documentation rewrites telemetry.md to split "client-side HTTP
span on outbound fetch" (telemetry) from a new "Outbound
correlation (SECURITY-RELEVANT)" top-level section
- design doc gets R4 revision row + new §12 "R4 Scope Conflation
Split" capturing the rationale and follow-up PR outline
The session-id apparatus (R3 code) lives in git history at commits
1c8528a56 / cb162e716 / 7a1b4f8d0 / 40e1efc1f / 106598ca2; the
follow-up PR can cherry-pick or restore those files under the new
outboundCorrelation.* namespace as LazzyMan suggested.
Vscode-ide-companion settings.schema.json regenerated.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* docs(telemetry): disclose telemetry.enabled dependency on propagateTraceContext
Self-review pass on R4 commit 9bdd3bd6f flagged one footgun: both
`docs/developers/development/telemetry.md` and the settingsSchema.ts
description for `outboundCorrelation.propagateTraceContext` describe
the toggle's behavior without noting that the flag is a silent no-op
when `telemetry.enabled` is false. An operator who sets only
`outboundCorrelation.propagateTraceContext: true` and forgets the
telemetry switch gets zero behavior change — no error, no warning, no
traceparent.
Fix: add the dependency disclosure to both surfaces, plus a JSON
example showing both flags wired together for the ARMS+DashScope
cross-process trace continuation use case.
Also fix a minor comment accuracy nit at `sdk.test.ts:683`: said the
SDK installs `W3CTraceContextPropagator` instance when opt-in is true,
but the actual default is `CompositePropagator(W3CTraceContextPropagator
+ W3CBaggagePropagator)` per `@opentelemetry/sdk-node` source.
Vscode-ide-companion settings.schema.json regenerated to reflect the
expanded description string.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* test(config): cover getOutboundCorrelationPropagateTraceContext defaults
R4 (commit 9bdd3bd6f) added the getter but the test file didn't grow a
corresponding describe block — sibling telemetry getters all have unit
tests but this new one was missed.
Add 4 cases covering the security-relevant default-to-false invariant
and explicit-set behavior:
- omitted outboundCorrelation → false
- empty outboundCorrelation: {} → false (the `?? false` collapse on the
getter, complementing the same on the constructor)
- explicit true → true
- explicit false → false
PR #4390 review (wenshao).
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* docs(design): reflect post-R4 polish commits in §12
R4 (commit 9bdd3bd6f) was followed by two polish commits that the
design doc §12 didn't track:
- 0be0df270 (docs): telemetry.enabled dependency disclosure on
propagateTraceContext — added to telemetry.md + settingsSchema
description because a self-review pass identified the silent-no-op
footgun (operator sets propagateTraceContext: true but forgets
telemetry.enabled: true, sees zero behavior change with no error).
- c0352fd5b (test): 4 config.test.ts cases covering the
getOutboundCorrelationPropagateTraceContext default-false invariant
(omitted / {} / explicit true / explicit false) — wenshao review
flagged the test gap.
Updates §12.4 with a new "Hidden dependency: telemetry.enabled" sub-
section explaining the gating relationship and pointing forward at the
follow-up PR (future outboundCorrelation.* settings inherit the same
dependency). Updates §12.5 implementation table to add the
config.test.ts row and clarify the telemetry.md / vscode-schema rows
were touched again in the polish pass.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* refactor: simplify post-R4 polish per /simplify review
/simplify review pass on commits 0be0df270 + c0352fd5b + 62cf6b4ee
flagged 4 concerns. Fix all 4:
1. **settingsSchema.ts description**: footgun warning ("Depends on
telemetry.enabled: true") was at char 600+ of a 650-char description.
VS Code settings UI truncates to ~300 chars inline → the most
important warning was hidden in the most-glanced view. Hoist to first
sentence ("Requires telemetry.enabled: true.").
2. **config.test.ts**: drop the task-narration comment
("PR #4390 R4: keep wire-level toggle out of telemetry namespace.")
that just restated the change context. The remaining 2-line comment
explaining WHY default-to-false is security-relevant survives.
3. **config.test.ts**: collapse 4 separate `it()` blocks into a single
`it.each([...])` covering the same 4 precondition × expectation
combinations. Removes boilerplate (`new Config({...baseParams, ...})`
repeated 4×) without losing assertion power; case-3 ("explicit false")
was a weak duplicate of case-2 ("empty object") since both hit the
same `?? false` branch, but keeping all 4 in the parametric table
documents intent more clearly than dropping case-3.
4. **design doc §12.4 + §12.5**: strip specific commit SHAs
(`0be0df270`, `c0352fd5b`) — design docs should be evergreen, not
doubled-up commit logs (those live in `git log`). Keep the design
intent ("two panels both document the dependency" / "test block
added") without naming the specific commits.
Regenerated vscode-ide-companion/schemas/settings.schema.json to
reflect the hoisted description sentence.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
* chore: bump version to 0.16.0 and normalize bat line endings
* revert: restore install-qwen-standalone.bat to original CRLF encoding
The previous bump commit inadvertently normalized line endings from
CRLF to LF. Windows batch files must retain CRLF in the repository
to work correctly with cmd.exe.
* revert: remove spurious NOTICES.txt change from version bump
* chore(deps): upgrade ink 6.2.3 -> 7.0.2 + bump Node engine to 22
ink 7 requires Node >=22 and react-reconciler 0.33 with React >=19.2,
so this PR also bumps:
- Node engines (root + cli + core) 20 -> 22
- React/react-dom 19.1 -> 19.2.4 (pinned exact via overrides to keep
the transitive React graph deduped to a single instance)
- @types/node pinned to 20.19.1 via overrides to avoid an unrelated
Dirent NonSharedBuffer regression in sessionService tests
- @vitest/eslint-plugin pinned to 1.3.4 to avoid an unrelated lint
regression introduced by the 1.6.x rule additions
- react-devtools-core 4.28 -> 6.1 (ink 7 peerOptional requires >=6.1.2)
- ink hoisted to root devDeps so workspace-private peer-dep contention
doesn't push ink-link/spinner/gradient into nested workspace
installs (which would skip transitive resolution for terminal-link)
Workflow + image + installer alignment:
- .nvmrc 20 -> 22
- Dockerfile node:20-slim -> node:22-slim
- CI test matrix drops 20.x (keeps 22.x + 24.x)
- terminal-bench workflow Node 20 -> 22
- Linux/Windows install scripts upgrade their Node version targets
Documentation alignment:
- README.md badge + prerequisites
- AGENTS.md, CONTRIBUTING.md, docs/users/quickstart.md,
docs/users/configuration/settings.md, docs/developers/contributing.md,
docs/developers/sdk-typescript.md, docs/users/extension/extension-releasing.md,
packages/sdk-typescript/README.md, packages/zed-extension/README.md,
scripts/installation/INSTALLATION_GUIDE.md
Test gating:
- Two AuthDialog/AskUserQuestionDialog tests that drive <SelectInput>
through ink-testing-library now race ink 7's frame-throttled input
delivery and land on the wrong option. The maintainers had already
marked one of them unreliable (skip on Win32 + CI+Node20). Extend
that gate to cover all environments until upstream
ink-testing-library ships an ink-7-compatible release that flushes
input deterministically. The other test now uses it.skip with the
same comment. No business code changes.
Verified locally:
- npm run typecheck across all workspaces: clean
- npm run lint (root): clean
- npm run test --workspaces:
cli 312/312 files, 4918 passed, 9 skipped
core 266/266 files, 6836 passed, 3 skipped
webui 6/6, 201 passed
sdk 40/40, 283 passed, 1 skipped
- npm ls ink: single ink@7.0.2 instance across all peer deps
- single react@19.2.4 instance
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* chore: align Node 22 floor across all shipping artifacts
Reviewer (tanzhenxin) flagged five surfaces where the >=22 engine bump
leaked: SDK package metadata, web-templates engines, /doctor runtime
check, main bundler target, and SDK bundler target. Each was a separate
escape hatch letting Node 18/20 consumers install or run the artifact
on an unsupported runtime.
- packages/sdk-typescript/package.json: engines.node >=18.0.0 -> >=22.0.0
- packages/web-templates/package.json: engines.node >=20 -> >=22
- packages/cli/src/utils/doctorChecks.ts: MIN_NODE_MAJOR 20 -> 22
- esbuild.config.js: target node20 -> node22 (main CLI bundle)
- packages/sdk-typescript/scripts/build.js: target node18 -> node22 (esm + cjs)
- packages/cli/src/utils/doctorChecks.test.ts: rename test label to v22+
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* ci(e2e): bump E2E workflow Node matrix to 22.x
Reviewer (tanzhenxin) flagged that e2e.yml still pinned node-version
20.x while root engines is now >=22, so every E2E run on push would
either fail at npm ci with engine error or silently exercise the bundle
on a runtime that's no longer in ci.yml's test matrix.
The macOS job in the same workflow already reads .nvmrc (which is 22)
so this only updates the Linux matrix.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(deps): drop root wrap-ansi override so ink 7 gets its declared dep
Reviewer (tanzhenxin) flagged that the root overrides.wrap-ansi: 9.0.2
predates this upgrade and forces every consumer (including ink) to v9,
while ink 7 declares wrap-ansi: ^10.0.0. The lockfile had no nested
install under node_modules/ink/, so ink 7 was running with a transitive
dep one major below its declared minimum.
Dropping the global override lets ink resolve its own wrap-ansi 10
nested install (now visible in the lockfile under
node_modules/ink/node_modules/wrap-ansi), while the cli package's own
direct `wrap-ansi: 9.0.2` dependency keeps the cli code path
(TableRenderer.tsx) on the version it has been tested against. The
nested cliui override is preserved for yargs which still needs v7.
Verified via `npm ls wrap-ansi`:
- ink@7.0.2 -> wrap-ansi@10.0.0 (newly nested)
- @qwen-code/qwen-code -> wrap-ansi@9.0.2 (unchanged)
- yargs/cliui -> wrap-ansi@7.0.0 (unchanged)
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(InputPrompt): un-skip placeholder ID reuse after deletion
Reviewer (tanzhenxin) flagged that the new it.skip on the
'should reuse placeholder ID after deletion' test was undisclosed in
the PR description and removed coverage of real product behavior
(freePlaceholderId / bracketed-paste backspace path) without a
TODO(#NNNN) link.
Their argument was sound: the skip rationale pointed at ink 7's input
throttle, but this same file just bumped the wait helper from 50ms to
150ms specifically to give ink 7 frame time. Re-running the test under
the bumped wait shows it passes reliably (5/5 runs in the full-file
context, 9/10 alone), so the skip was masking the throttle-flake that
the wait bump already addresses, not a real product bug.
Drop the it.skip and the now-stale comment so coverage of the
freePlaceholderId reuse logic is restored.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(InputPrompt): bump first prompt-suggestion test wait to 350ms
The "accepts and submits the prompt suggestion on Enter when the buffer
is empty" test is the first in its describe block, so it pays the
renderer cold-start cost. On macOS-22.x CI runners that pushes the
Enter → onSubmit microtask past the default 150ms post-Enter wait. Match
the 350ms initial render wait used immediately above to absorb the cold
start.
* Revert "test(InputPrompt): bump first prompt-suggestion test wait to 350ms"
This reverts commit 6add83b62ea80c551c81f54af1fda3e6e7478f55.
* test(InputPrompt): wait for followup suggestion debounce before pressing Enter
Root cause of the failing prompt-suggestion tests on macOS and Windows
CI is not flaky timing of the test post-Enter wait — it's the 300ms
debounce inside createFollowupController.setSuggestion (shared core).
The Enter handler reads followup.state.isVisible synchronously, so if
the debounce timer has not fired before stdin.write('\\r'), the
suggestion path is skipped and onSubmit never runs. No amount of
post-Enter wait can recover from that — the keypress was already
processed against stale state.
The original wait(350) only left ~50ms margin over the 300ms debounce,
which ink 7 / React 19.2 mount overhead consumed on slow Windows
runners. Bump the initial wait to 700ms (named SUGGESTION_VISIBLE_WAIT_MS)
to give the debounce timer + cold-start render a generous buffer.
Apply to the two sibling tests too — without the wait their "does not
accept" assertions pass trivially when suggestion is never visible,
which is a false green that hides regressions in the actual reject path.
* fix(deps): align cli wrap-ansi with ink 7 (9.0.2 -> ^10.0.0)
Ink 7 ships its own wrap-ansi@10. CLI's direct dep was pinned to 9.0.2,
causing two copies of wrap-ansi in node_modules and a potential drift in
CJK width / ANSI handling between ink's internal text wrapping and our
TableRenderer.
Upgrading the CLI's direct dep to ^10.0.0 lets npm dedupe to a single
wrap-ansi@10 used by both ink and TableRenderer. API surface is
identical; the only documented behaviour change is that tabs are
expanded to 8-column tab stops before wrapping, which TableRenderer
doesn't feed in.
TableRenderer test suite (43 tests) passes against wrap-ansi@10.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* chore(deps): document @types/node 20.x pin in overrides
The override pinning @types/node to 20.19.1 (while engines require
Node >=22) is intentional: bumping to @types/node@22.x re-introduces
a Dirent<NonSharedBuffer> type regression that breaks
@qwen-code/qwen-code-core/sessionService tests.
Add a sibling "//@types/node" note inside `overrides` so future
maintainers see the rationale and know when to revisit the pin
without having to dig through PR #3860 history.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(AskUserQuestionDialog): link skipped Submit-tab test to tracking issue
The 'shows unanswered questions as (not answered) in Submit tab' test
was switched to `it.skip` in the ink 7 upgrade because
`ink-testing-library@4.0.0` doesn't flush input deterministically
through ink 7's 30fps throttle.
Add a `// TODO(#4036):` marker so the skip is greppable and can be
re-enabled once upstream ships an ink-7-compatible release.
Refs #4036
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(deps): move @types/node pin comment out of overrides block
npm's `overrides` field requires every key to be a real package name —
the `"//@types/node"` comment-key added in 205855875 trips Arborist with
"Override without name" and breaks `npm ci` across all CI jobs.
Move the explanation to a sibling top-level `"//overrides"` key, which
npm ignores at the document root. Same documentation value, no
override-parser collateral damage.
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Update all package versions from 0.15.2 to 0.15.3 across the monorepo
including root package.json, package-lock.json, and all sub-packages
(channels, cli, core, vscode-ide-companion, web-templates, webui).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Upgrade all package versions from 0.14.5 to 0.15.0 across the monorepo, including package-lock.json and sandbox image references.
* refactor: merge test-utils package into core
Consolidate the standalone @qwen-code/qwen-code-test-utils package
into packages/core/src/test-utils/, eliminating the need for a
separate package that only provided createTmpDir, cleanupTmpDir,
and FileSystemStructure type.
Changes:
- Move file-system-test-helpers.ts into core/src/test-utils/
- Re-export from core's test-utils index
- Update 3 core test files to use relative imports
- Update cli useAtCompletion test to import from @qwen-code/qwen-code-core
- Remove test-utils devDependency from core and cli package.json
- Delete packages/test-utils/ directory
All affected tests pass (fileSearch, crawler, ignore, useAtCompletion).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix: remove deleted test-utils from build order
The test-utils package was merged into core but the build script still
tried to build it separately, causing CI failures.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The previous version bump commit (bb4376c) only updated the root
package.json but did not run `npm run release:version` to propagate
the version and sandboxImageUri to all workspace packages.
This caused Docker sandbox integration tests to fail in CI with
"manifest unknown" because build_sandbox.js built image 0.14.1
(from packages/cli/package.json) while sandboxConfig.ts expected
image 0.14.2 (from root package.json).
Fixes: https://github.com/QwenLM/qwen-code/actions/runs/24135197272/job/70424966323
- Update all packages from 0.13.x to 0.14.0
- Update sandbox image URI to 0.14.0
This prepares the 0.14.0 release with updated version numbers
across all workspace packages.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The previous version (1.1.0) has a native-level bug on macOS where each
PTY spawn leaks one /dev/ptmx file descriptor that is never closed. Over
a long session with hundreds of shell commands, this exhausts the
system-wide PTY pool (kern.tty.ptmx_max = 511), breaking other programs
like tmux and new terminal windows.
Root cause: microsoft/node-pty#882
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>