From 6982b2f3746d16efacd0d36a887d116d8a6442d1 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sun, 31 May 2026 13:07:27 +0800 Subject: [PATCH 1/6] feat(core): inject context env vars (session/agent/prompt ID) into shell subprocesses When SubAgents execute SQL or Python scripts via Bash tool, the scripts have no way to know their execution context. This adds automatic injection of QWEN_CODE_SESSION_ID, QWEN_CODE_AGENT_ID, and QWEN_CODE_PROMPT_ID into all shell subprocess environments, enabling downstream scripts to perform trace correlation, audit logging, and business context attribution. Closes #4645 --- packages/core/src/config/config.ts | 7 ++ packages/core/src/hooks/hookRunner.ts | 2 + .../src/services/shellExecutionService.ts | 3 + packages/core/src/tools/monitor.ts | 2 + .../core/src/utils/shellContextEnv.test.ts | 69 +++++++++++++++++++ packages/core/src/utils/shellContextEnv.ts | 41 +++++++++++ 6 files changed, 124 insertions(+) create mode 100644 packages/core/src/utils/shellContextEnv.test.ts create mode 100644 packages/core/src/utils/shellContextEnv.ts diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 5aade5d0e0e..914a8be45b4 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1074,6 +1074,12 @@ export class Config { constructor(params: ConfigParameters) { this.sessionId = params.sessionId ?? randomUUID(); + // Only set the global env marker if no session has claimed it yet, + // so throwaway Config instances (e.g. telemetry-only) don't clobber + // the real interactive session's ID. + if (!process.env['QWEN_CODE_SESSION_ID']) { + process.env['QWEN_CODE_SESSION_ID'] = this.sessionId; + } this.sessionData = params.sessionData; setDebugLogSession(this); this.debugLogger = createDebugLogger(); @@ -1986,6 +1992,7 @@ export class Config { const previousSessionId = this.sessionId; this.sessionId = sessionId ?? randomUUID(); + process.env['QWEN_CODE_SESSION_ID'] = this.sessionId; this.sessionData = sessionData; setDebugLogSession(this); this.debugLogger = createDebugLogger(); diff --git a/packages/core/src/hooks/hookRunner.ts b/packages/core/src/hooks/hookRunner.ts index 6f664267d27..630cfcb6693 100644 --- a/packages/core/src/hooks/hookRunner.ts +++ b/packages/core/src/hooks/hookRunner.ts @@ -29,6 +29,7 @@ import { FunctionHookRunner } from './functionHookRunner.js'; import { PromptHookRunner } from './promptHookRunner.js'; import { AsyncHookRegistry, generateHookId } from './asyncHookRegistry.js'; import type { Config } from '../config/config.js'; +import { getShellContextEnvVars } from '../utils/shellContextEnv.js'; const debugLogger = createDebugLogger('TRUSTED_HOOKS'); @@ -573,6 +574,7 @@ export class HookRunner { GEMINI_PROJECT_DIR: input.cwd, CLAUDE_PROJECT_DIR: input.cwd, // For compatibility QWEN_PROJECT_DIR: input.cwd, // For Qwen Code compatibility + ...getShellContextEnvVars(), ...hookConfig.env, }; diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index 2fe8d491fb2..d6be9185dc1 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -21,6 +21,7 @@ import { type AnsiOutput, } from '../utils/terminalSerializer.js'; import { normalizePathEnvForWindows } from '../utils/windowsPath.js'; +import { getShellContextEnvVars } from '../utils/shellContextEnv.js'; import { createDebugLogger } from '../utils/debugLogger.js'; const { Terminal } = pkg; @@ -550,6 +551,7 @@ export class ShellExecutionService { QWEN_CODE: '1', TERM: 'xterm-256color', PAGER: 'cat', + ...getShellContextEnvVars(), }, }); @@ -1152,6 +1154,7 @@ export class ShellExecutionService { TERM: 'xterm-256color', PAGER: shellExecutionConfig.pager ?? 'cat', GIT_PAGER: shellExecutionConfig.pager ?? 'cat', + ...getShellContextEnvVars(), }, handleFlowControl: true, }); diff --git a/packages/core/src/tools/monitor.ts b/packages/core/src/tools/monitor.ts index e441df10e2a..d0b6ab4d577 100644 --- a/packages/core/src/tools/monitor.ts +++ b/packages/core/src/tools/monitor.ts @@ -53,6 +53,7 @@ import { isShellCommandReadOnlyAST, } from '../utils/shellAstParser.js'; import { getCurrentAgentId } from '../agents/runtime/agent-context.js'; +import { getShellContextEnvVars } from '../utils/shellContextEnv.js'; const debugLogger = createDebugLogger('MONITOR'); @@ -366,6 +367,7 @@ class MonitorToolInvocation extends BaseToolInvocation< QWEN_CODE: '1', TERM: 'dumb', // no color codes for streaming PAGER: 'cat', + ...getShellContextEnvVars(), }, }); } catch (err) { diff --git a/packages/core/src/utils/shellContextEnv.test.ts b/packages/core/src/utils/shellContextEnv.test.ts new file mode 100644 index 00000000000..d5804b975f3 --- /dev/null +++ b/packages/core/src/utils/shellContextEnv.test.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { getShellContextEnvVars } from './shellContextEnv.js'; +import { runWithAgentContext } from '../agents/runtime/agent-context.js'; +import { promptIdContext } from './promptIdContext.js'; + +describe('getShellContextEnvVars', () => { + let originalSessionId: string | undefined; + + beforeEach(() => { + originalSessionId = process.env['QWEN_CODE_SESSION_ID']; + delete process.env['QWEN_CODE_SESSION_ID']; + }); + + afterEach(() => { + if (originalSessionId !== undefined) { + process.env['QWEN_CODE_SESSION_ID'] = originalSessionId; + } else { + delete process.env['QWEN_CODE_SESSION_ID']; + } + }); + + it('returns empty strings for agent/prompt when no context is available', () => { + const env = getShellContextEnvVars(); + expect(env).toEqual({ + QWEN_CODE_AGENT_ID: '', + QWEN_CODE_PROMPT_ID: '', + }); + }); + + it('returns QWEN_CODE_SESSION_ID when set in process.env', () => { + process.env['QWEN_CODE_SESSION_ID'] = 'test-session-123'; + const env = getShellContextEnvVars(); + expect(env.QWEN_CODE_SESSION_ID).toBe('test-session-123'); + }); + + it('returns QWEN_CODE_AGENT_ID when called within agent context', async () => { + const env = await runWithAgentContext('my-agent-42', async () => getShellContextEnvVars()); + expect(env.QWEN_CODE_AGENT_ID).toBe('my-agent-42'); + }); + + it('returns QWEN_CODE_PROMPT_ID when called within prompt context', () => { + const env = promptIdContext.run('prompt-abc', () => getShellContextEnvVars()); + expect(env.QWEN_CODE_PROMPT_ID).toBe('prompt-abc'); + }); + + it('returns all vars when all contexts are active', async () => { + process.env['QWEN_CODE_SESSION_ID'] = 'sess-uuid'; + const env = await runWithAgentContext('agent-xyz', async () => promptIdContext.run('prompt-456', () => getShellContextEnvVars())); + expect(env).toEqual({ + QWEN_CODE_SESSION_ID: 'sess-uuid', + QWEN_CODE_AGENT_ID: 'agent-xyz', + QWEN_CODE_PROMPT_ID: 'prompt-456', + }); + }); + + it('sets empty string for agent/prompt to override inherited env', () => { + // Simulates a nested qwen-code process where parent injected these + const env = getShellContextEnvVars(); + expect(env.QWEN_CODE_AGENT_ID).toBe(''); + expect(env.QWEN_CODE_PROMPT_ID).toBe(''); + // Empty strings will overwrite any stale inherited values in process.env + }); +}); diff --git a/packages/core/src/utils/shellContextEnv.ts b/packages/core/src/utils/shellContextEnv.ts new file mode 100644 index 00000000000..7956eecb7dc --- /dev/null +++ b/packages/core/src/utils/shellContextEnv.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Returns context environment variables to inject into shell subprocesses. + * + * Reads dynamic context (agent ID, prompt ID) from AsyncLocalStorage at + * call time, and session ID from process.env (set by Config at session + * start). This enables downstream scripts to identify which session, + * agent, and prompt triggered their execution — useful for tracing, + * audit logging, and business context correlation. + * + * Must be called at spawn time within the executing async context to + * capture the correct agent/prompt frame. + */ + +import { getCurrentAgentId } from '../agents/runtime/agent-context.js'; +import { promptIdContext } from './promptIdContext.js'; + +export function getShellContextEnvVars(): Record { + const env: Record = {}; + + const sessionId = process.env['QWEN_CODE_SESSION_ID']; + if (sessionId) { + env['QWEN_CODE_SESSION_ID'] = sessionId; + } + + // For agent/prompt IDs: explicitly set empty string when no ALS context + // exists, so that stale values inherited from a parent qwen-code process + // (via process.env spread) are overwritten rather than leaked. + const agentId = getCurrentAgentId(); + env['QWEN_CODE_AGENT_ID'] = agentId ?? ''; + + const promptId = promptIdContext.getStore(); + env['QWEN_CODE_PROMPT_ID'] = promptId ?? ''; + + return env; +} From 478a20e779a6583073dbb7df815c409dcbd048af Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sun, 31 May 2026 13:16:24 +0800 Subject: [PATCH 2/6] fix(core): use module-level flag to guard session env claim Prevents nested qwen-code processes from inheriting the parent's session ID. The previous `if (!process.env[...])` check would keep the parent's value; a module-level flag ensures each process claims its own session ID on first Config construction. --- packages/core/src/config/config.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 914a8be45b4..f137393a0d9 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -872,6 +872,12 @@ const DEFAULT_BARE_CORE_TOOLS = [ ToolNames.SHELL, ]; +// Tracks whether the first Config in this process has claimed the global +// QWEN_CODE_SESSION_ID env var. Prevents throwaway Config instances from +// overwriting the real session's ID while still allowing nested qwen-code +// processes to claim their own (they start with a fresh module scope). +let sessionEnvClaimed = false; + export class Config { private sessionId: string; private sessionData?: ResumedSessionData; @@ -1074,11 +1080,15 @@ export class Config { constructor(params: ConfigParameters) { this.sessionId = params.sessionId ?? randomUUID(); - // Only set the global env marker if no session has claimed it yet, - // so throwaway Config instances (e.g. telemetry-only) don't clobber - // the real interactive session's ID. - if (!process.env['QWEN_CODE_SESSION_ID']) { + // Only set the global env marker once per process lifetime, so + // throwaway Config instances (e.g. telemetry-only) don't clobber + // the real interactive session's ID. Uses a module-level flag + // rather than checking env existence — otherwise a nested qwen-code + // launched from within a session would inherit the parent's ID and + // never claim its own. + if (!sessionEnvClaimed) { process.env['QWEN_CODE_SESSION_ID'] = this.sessionId; + sessionEnvClaimed = true; } this.sessionData = params.sessionData; setDebugLogSession(this); From a2ab29a72cbf9c9e53f9f5d05d7c4b1e4684cbb6 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sun, 31 May 2026 13:26:34 +0800 Subject: [PATCH 3/6] fix(test): use bracket notation for index signature properties CI tsc --build enforces noPropertyAccessFromIndexSignature; use env['KEY'] instead of env.KEY to satisfy strict type checking. --- .../core/src/utils/shellContextEnv.test.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/core/src/utils/shellContextEnv.test.ts b/packages/core/src/utils/shellContextEnv.test.ts index d5804b975f3..17680915eb6 100644 --- a/packages/core/src/utils/shellContextEnv.test.ts +++ b/packages/core/src/utils/shellContextEnv.test.ts @@ -36,22 +36,28 @@ describe('getShellContextEnvVars', () => { it('returns QWEN_CODE_SESSION_ID when set in process.env', () => { process.env['QWEN_CODE_SESSION_ID'] = 'test-session-123'; const env = getShellContextEnvVars(); - expect(env.QWEN_CODE_SESSION_ID).toBe('test-session-123'); + expect(env['QWEN_CODE_SESSION_ID']).toBe('test-session-123'); }); it('returns QWEN_CODE_AGENT_ID when called within agent context', async () => { - const env = await runWithAgentContext('my-agent-42', async () => getShellContextEnvVars()); - expect(env.QWEN_CODE_AGENT_ID).toBe('my-agent-42'); + const env = await runWithAgentContext('my-agent-42', async () => + getShellContextEnvVars(), + ); + expect(env['QWEN_CODE_AGENT_ID']).toBe('my-agent-42'); }); it('returns QWEN_CODE_PROMPT_ID when called within prompt context', () => { - const env = promptIdContext.run('prompt-abc', () => getShellContextEnvVars()); - expect(env.QWEN_CODE_PROMPT_ID).toBe('prompt-abc'); + const env = promptIdContext.run('prompt-abc', () => + getShellContextEnvVars(), + ); + expect(env['QWEN_CODE_PROMPT_ID']).toBe('prompt-abc'); }); it('returns all vars when all contexts are active', async () => { process.env['QWEN_CODE_SESSION_ID'] = 'sess-uuid'; - const env = await runWithAgentContext('agent-xyz', async () => promptIdContext.run('prompt-456', () => getShellContextEnvVars())); + const env = await runWithAgentContext('agent-xyz', async () => + promptIdContext.run('prompt-456', () => getShellContextEnvVars()), + ); expect(env).toEqual({ QWEN_CODE_SESSION_ID: 'sess-uuid', QWEN_CODE_AGENT_ID: 'agent-xyz', @@ -62,8 +68,8 @@ describe('getShellContextEnvVars', () => { it('sets empty string for agent/prompt to override inherited env', () => { // Simulates a nested qwen-code process where parent injected these const env = getShellContextEnvVars(); - expect(env.QWEN_CODE_AGENT_ID).toBe(''); - expect(env.QWEN_CODE_PROMPT_ID).toBe(''); + expect(env['QWEN_CODE_AGENT_ID']).toBe(''); + expect(env['QWEN_CODE_PROMPT_ID']).toBe(''); // Empty strings will overwrite any stale inherited values in process.env }); }); From 6ec95252b054cfa860c377b307f7798368687a63 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sun, 31 May 2026 13:54:50 +0800 Subject: [PATCH 4/6] fix(core): guard process.env assignment for mocked process environments Some test suites mock node:process without providing env, causing TypeError when Config constructor assigns QWEN_CODE_SESSION_ID. Add defensive check before env writes. --- packages/core/src/config/config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index f137393a0d9..ba23977c418 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1086,7 +1086,7 @@ export class Config { // rather than checking env existence — otherwise a nested qwen-code // launched from within a session would inherit the parent's ID and // never claim its own. - if (!sessionEnvClaimed) { + if (!sessionEnvClaimed && process.env) { process.env['QWEN_CODE_SESSION_ID'] = this.sessionId; sessionEnvClaimed = true; } @@ -2002,7 +2002,9 @@ export class Config { const previousSessionId = this.sessionId; this.sessionId = sessionId ?? randomUUID(); - process.env['QWEN_CODE_SESSION_ID'] = this.sessionId; + if (process.env) { + process.env['QWEN_CODE_SESSION_ID'] = this.sessionId; + } this.sessionData = sessionData; setDebugLogSession(this); this.debugLogger = createDebugLogger(); From 96eb00ca1f16b748d4ee657dfb37a0d4779a4d98 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Tue, 2 Jun 2026 00:12:42 +0800 Subject: [PATCH 5/6] test(config): add coverage for sessionEnvClaimed guard Addresses reviewer feedback (wenshao) requesting test coverage for the module-level `sessionEnvClaimed` guard in Config constructor. Tests verify: 1. First Config instance sets process.env['QWEN_CODE_SESSION_ID'] 2. Subsequent Config instances do not overwrite the env var --- .../src/config/config-session-env.test.ts | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 packages/core/src/config/config-session-env.test.ts diff --git a/packages/core/src/config/config-session-env.test.ts b/packages/core/src/config/config-session-env.test.ts new file mode 100644 index 00000000000..c91ca01c647 --- /dev/null +++ b/packages/core/src/config/config-session-env.test.ts @@ -0,0 +1,158 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +/** + * Tests for the module-level `sessionEnvClaimed` guard in Config. + * + * The guard ensures that only the first Config instance in a process sets + * `process.env['QWEN_CODE_SESSION_ID']`, preventing throwaway instances + * (e.g. telemetry-only) from overwriting the real session's ID. + * + * We use `vi.isolateModules` to get a fresh module scope (resetting the + * module-level flag) for each test. + */ + +// Shared mocks needed by Config constructor +vi.mock('node:fs'); +vi.mock('node:fs/promises'); +vi.mock('../telemetry/index.js', () => ({ + QwenLogger: vi.fn().mockImplementation(() => ({ + logStartSessionEvent: vi.fn().mockResolvedValue(undefined), + logEndSessionEvent: vi.fn().mockResolvedValue(undefined), + shutdown: vi.fn().mockResolvedValue(undefined), + })), + DEFAULT_TELEMETRY_TARGET: 'none', + DEFAULT_OTLP_ENDPOINT: '', + isTelemetrySdkInitialized: vi.fn().mockReturnValue(false), + shutdownTelemetry: vi.fn().mockResolvedValue(undefined), + refreshSessionContext: vi.fn(), +})); +vi.mock('../core/contentGenerator.js', () => ({ + resolveContentGeneratorConfigWithSources: vi.fn().mockReturnValue({ + config: { model: 'test-model', apiKey: 'test-key' }, + sources: {}, + }), + createContentGeneratorConfig: vi.fn().mockReturnValue({}), + createContentGenerator: vi.fn().mockReturnValue({}), + AuthType: { API_KEY: 'apiKey' }, +})); +vi.mock('../core/baseLlmClient.js'); +vi.mock('../core/toolHookTriggers.js', () => ({ + fireNotificationHook: vi.fn().mockResolvedValue({}), +})); +vi.mock('../services/skillManager.js', () => { + const SkillManagerMock = vi.fn(); + SkillManagerMock.prototype.startWatching = vi + .fn() + .mockResolvedValue(undefined); + SkillManagerMock.prototype.refreshCache = vi + .fn() + .mockResolvedValue(undefined); + SkillManagerMock.prototype.stopWatching = vi.fn(); + SkillManagerMock.prototype.listSkills = vi.fn().mockResolvedValue([]); + SkillManagerMock.prototype.addChangeListener = vi.fn(); + SkillManagerMock.prototype.removeChangeListener = vi.fn(); + SkillManagerMock.prototype.matchAndActivateByPath = vi + .fn() + .mockResolvedValue([]); + SkillManagerMock.prototype.matchAndActivateByPaths = vi + .fn() + .mockResolvedValue([]); + return { SkillManager: SkillManagerMock }; +}); +vi.mock('../subagents/subagent-manager.js', () => { + const SubagentManagerMock = vi.fn(); + SubagentManagerMock.prototype.loadSessionSubagents = vi.fn(); + SubagentManagerMock.prototype.addChangeListener = vi + .fn() + .mockReturnValue(() => {}); + SubagentManagerMock.prototype.listSubagents = vi.fn().mockResolvedValue([]); + return { SubagentManager: SubagentManagerMock }; +}); +vi.mock('../ide/ide-client.js', () => ({ + IdeClient: { + getInstance: vi.fn().mockResolvedValue({ + getConnectionStatus: vi.fn(), + initialize: vi.fn(), + shutdown: vi.fn(), + }), + }, +})); +vi.mock('../memory/const.js', () => ({ + setGeminiMdFilename: vi.fn(), +})); + +import * as fs from 'node:fs'; +import type { Mock } from 'vitest'; +import type { ConfigParameters } from './config.js'; + +const baseParams: ConfigParameters = { + cwd: '/tmp', + targetDir: '/tmp', + debugMode: false, + model: 'test-model', + telemetry: { enabled: false }, + usageStatisticsEnabled: false, + overrideExtensions: [], +}; + +describe('Config sessionEnvClaimed guard', () => { + let originalEnv: string | undefined; + + beforeEach(() => { + originalEnv = process.env['QWEN_CODE_SESSION_ID']; + delete process.env['QWEN_CODE_SESSION_ID']; + + (fs.existsSync as Mock).mockReturnValue(true); + (fs.readdirSync as Mock).mockReturnValue([]); + (fs.statSync as Mock).mockReturnValue({ + isDirectory: vi.fn().mockReturnValue(true), + }); + vi.mocked(fs.realpathSync).mockImplementation((p) => String(p)); + (fs.mkdirSync as Mock).mockImplementation(() => undefined); + (fs.writeFileSync as Mock).mockImplementation(() => undefined); + (fs.renameSync as Mock).mockImplementation(() => undefined); + (fs.copyFileSync as Mock).mockImplementation(() => undefined); + (fs.unlinkSync as Mock).mockImplementation(() => undefined); + (fs.readFileSync as Mock).mockImplementation(() => undefined); + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env['QWEN_CODE_SESSION_ID'] = originalEnv; + } else { + delete process.env['QWEN_CODE_SESSION_ID']; + } + vi.resetModules(); + }); + + it('first Config sets process.env QWEN_CODE_SESSION_ID to its sessionId', async () => { + const { Config } = await import('./config.js'); + const config = new Config({ ...baseParams }); + + expect(process.env['QWEN_CODE_SESSION_ID']).toBe(config.getSessionId()); + }); + + it('subsequent Config does not overwrite the env var set by the first', async () => { + const { Config } = await import('./config.js'); + const firstConfig = new Config({ ...baseParams }); + const firstSessionId = firstConfig.getSessionId(); + + // Second Config (e.g. telemetry-only throwaway instance) + const secondConfig = new Config({ + ...baseParams, + sessionId: 'throwaway-session-id', + }); + + // The env var should still be the first config's session ID + expect(process.env['QWEN_CODE_SESSION_ID']).toBe(firstSessionId); + expect(process.env['QWEN_CODE_SESSION_ID']).not.toBe( + secondConfig.getSessionId(), + ); + }); +}); From 63a835620fde88f91d42327dc22109ae41b7813a Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Tue, 2 Jun 2026 00:20:23 +0800 Subject: [PATCH 6/6] test(config): add startNewSession env var test and clarify comment - Add test verifying startNewSession updates process.env to new session ID - Add comment explaining why startNewSession bypasses sessionEnvClaimed guard (only callable on the canonical Config instance that already claimed) --- .../core/src/config/config-session-env.test.ts | 14 ++++++++++++++ packages/core/src/config/config.ts | 3 +++ 2 files changed, 17 insertions(+) diff --git a/packages/core/src/config/config-session-env.test.ts b/packages/core/src/config/config-session-env.test.ts index c91ca01c647..8f300eacb27 100644 --- a/packages/core/src/config/config-session-env.test.ts +++ b/packages/core/src/config/config-session-env.test.ts @@ -155,4 +155,18 @@ describe('Config sessionEnvClaimed guard', () => { secondConfig.getSessionId(), ); }); + + it('startNewSession updates env var to the new session ID', async () => { + const { Config } = await import('./config.js'); + const config = new Config({ ...baseParams }); + const originalSessionId = config.getSessionId(); + + expect(process.env['QWEN_CODE_SESSION_ID']).toBe(originalSessionId); + + // Simulate /clear or session switch + config.startNewSession('new-session-uuid-123'); + + expect(process.env['QWEN_CODE_SESSION_ID']).toBe('new-session-uuid-123'); + expect(process.env['QWEN_CODE_SESSION_ID']).not.toBe(originalSessionId); + }); }); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index ba23977c418..218b36097ca 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -2002,6 +2002,9 @@ export class Config { const previousSessionId = this.sessionId; this.sessionId = sessionId ?? randomUUID(); + // Unconditional: startNewSession is only called on the canonical Config + // instance (the one that already claimed via sessionEnvClaimed), so this + // correctly updates the env var to reflect the new active session. if (process.env) { process.env['QWEN_CODE_SESSION_ID'] = this.sessionId; }