Files
qwen-code/scripts/cli-entry.js
907de17fd7 feat(memory): add scoped external import jobs for CLI and HTTP (#14281)
* feat(memory): add scoped external import jobs for CLI and HTTP

* fix(memory): align import contracts and isolate runtime scope

* feat(memory): bound and batch external import consolidation

* test(cli): scope the mod-session mock to the test's own Config (#14281)

AppContainer mount effects from earlier tests can still be in flight
(the beforeEach leaves the real config.initialize() un-awaited), and an
unscoped getInteractiveModSession mockReturnValue lets those stale
effects call the current test's initialize() mock, intermittently
inflating the exact-once assertion to 51+ calls. Scoping the mock by
Config identity makes the stale calls a no-op, matching the real
function's behavior for unregistered configs.

* fix(cli): harden memory import auth, argv parsing, output flush, and recovery guards

- pass isInitialAuth to refreshAuth so a non-interactive confirmed import
  fails closed on missing credentials instead of starting a device flow
- pin the import positional to 'codex' and reject a repeated --source
  instead of silently importing the default directory
- flush the JSON result envelopes before exit so piped output is not
  truncated
- clear a failed item's stale reason on retry
- bound remark parsing with an over-approximate heading pre-scan
- enforce the no-Git-checkout runtime invariant on recovery, not only on
  admission
- key the publication ledger on section identity so identical text from
  distinct files no longer collapses onto one entry
- pin the memory fast-path member in the cli-entry wrapper test

* fix(cli): harden memory import bootstrap, auth, and batch recovery

- memory fast path prints help to stdout for bare/-h invocations
  instead of failing demandCommand (R4-1, R3-1)
- local import resolves auth through the config and enforces
  security.auth.enforcedType before any model spend (R4-28, R1-8)
- rollout_summaries admission matches the discovery class so a
  referenced but undiscoverable name is rejected up front (R4-26)
- section splitting uses the shallowest heading depth present rather
  than the constant 1 (R3-4)
- a moved linked worktree reaches the registration diagnostic instead
  of a raw ENOENT, and recall warns once with the repair hint (R4-27)
- the flushed result writer tolerates EPIPE from a departed reader
  (R3-2)
- retry() waits out a still-registered worker instead of losing the
  race (R1-1); recover()/list() isolate a damaged batch directory
  instead of failing the whole walk (R1-2)

* fix(cli): fence import lock loss and bound preview resources

* fix(cli): recover invalid import proposals and handle pipe closure

* test(web-shell): await return boot before asserting connection URL

* fix(cli): recover imports across groups and stale publication intents

* fix(cli): retry failed workspace memory import recovery

* fix(cli): preserve durable import receipts and classify HTTP failures

* test(cli): complete injected runtime storage fixture

* fix(cli): reject symlink import roots before model usage

* fix(build): allow CLI compilation beyond the 4 GiB heap cap

* fix(memory): revalidate cached repository identities

* fix(cli): bound the existing-memory context index (#14281)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(memory): preserve native user index capacity during import

* test(managed-agent): fix hosted MCP checkstyle violations

* fix(memory): preserve relevant evidence and unpublished intent recovery

* fix(memory): keep CR sections, mid-document terms and admission budgets

* test(ci): account for TUI parity integration coverage

* test(cli): synchronize monitor stop observation

* test(cli): wait for the memory preview write in the flush test

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): scale Hosted budgets for contended gate hosts (#14281)

The review gate drops RUNNER_NAME and clamps vitest to 25% of the cores,
so the load-average fallback never reached the core count and the heavy
Hosted suites kept their fast-host ceilings. Halve the contention
threshold and scale the two budget tests' ceilings with it.

* test(ci): remove exemptions for enrolled no-AK tests

* fix(cli): harden memory import scopes and auth checks

Normalize setext and closed ATX heading titles before scope
classification, stop unquoted cwd captures at whitespace and reject
prose or trailing punctuation, demote imported documents regardless of
scan scope, and validate the auth method before paid import calls.

* fix(cli): close memory import review gaps (#14281)

---------

Co-authored-by: qwen-autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: Qwen Autofix <qwen-autofix@localhost>
Co-authored-by: claudeethan <[email protected]>
Co-authored-by: qwen-code autofix <[email protected]>
2026-10-24 10:04:55 +00:00

509 lines
18 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* @license
* Copyright 2025 Qwen Code
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Production bin entry wrapper.
*
* For most commands, global.gc() must be available for the memory-pressure
* monitor's critical-tier cleanup. On POSIX under Node the entry exposes gc at
* runtime and imports cli.js in this process; on Windows and under Bun it
* launches the bundled CLI as a child with --expose-gc.
*
* For bootstrap fast paths: imports cli.js directly in-process, skipping the
* spawnSync overhead. These paths do not need global.gc().
*/
const relaunchArgs = process.env['QWEN_CODE_RELAUNCH_ARGS'];
let cliArgs = process.argv.slice(2);
try {
cliArgs = relaunchArgs ? JSON.parse(relaunchArgs) : cliArgs;
} catch {
// Ignore stale or user-provided junk; normal argv is still usable.
}
delete process.env['QWEN_CODE_RELAUNCH_ARGS'];
delete process.env['QWEN_CODE_PENDING_COMPILE_CACHE'];
function hasFlag(flag, alias) {
for (const arg of cliArgs) {
if (arg === '--') {
return false;
}
if (arg === flag || arg === alias) {
return true;
}
}
return false;
}
function isInProcessFastPath() {
const first = cliArgs[0];
if (
first === 'serve' ||
first === 'mcp' ||
first === 'memory' ||
first === 'managed-runtime-bootstrap' ||
first === 'managed-runtime-bootstrap-worker'
) {
return true;
}
if (first === undefined || first.startsWith('-')) {
return hasFlag('--help', '-h') || hasFlag('--version', '-v');
}
return false;
}
const isTopLevelVersion =
(cliArgs[0] === undefined || cliArgs[0].startsWith('-')) &&
hasFlag('--version', '-v');
if (isTopLevelVersion && process.env['CLI_VERSION']) {
process.stdout.write(`${process.env['CLI_VERSION']}\n`);
process.exit(0);
}
const { existsSync, readFileSync, realpathSync, statSync } = await import(
'node:fs'
);
const { createHash } = await import('node:crypto');
const { homedir, tmpdir } = await import('node:os');
const { parseEnv } = await import('node:util');
const { fileURLToPath, pathToFileURL } = await import('node:url');
const { delimiter, dirname, join, parse, resolve, sep } = await import(
'node:path'
);
const __dirname = dirname(fileURLToPath(import.meta.url));
const currentEntryPath = realpathSync(fileURLToPath(import.meta.url));
function getHomeDir() {
try {
return homedir() || tmpdir();
} catch {
return tmpdir();
}
}
function resolveQwenHome() {
const configured = process.env['QWEN_HOME'];
if (!configured) return join(getHomeDir(), '.qwen');
if (configured === '~') return getHomeDir();
if (configured.startsWith('~/') || configured.startsWith('~\\')) {
return join(
getHomeDir(),
...configured
.slice(2)
.split(/[/\\]+/)
.filter(Boolean),
);
}
return resolve(configured);
}
function preResolveQwenHome() {
if (Object.hasOwn(process.env, 'QWEN_HOME')) return;
const home = getHomeDir();
for (const candidate of [join(home, '.qwen', '.env'), join(home, '.env')]) {
try {
const source = readFileSync(candidate, 'utf8')
.replace(/^\uFEFF/, '')
.replace(/^(\s*(?:export\s+)?QWEN_HOME):[^\S\r\n]+/gm, '$1=');
const configured = parseEnv(source)['QWEN_HOME'];
if (configured) {
process.env['QWEN_HOME'] = configured;
return;
}
} catch {
// Match the CLI's quiet handling of missing or invalid home env files.
}
}
}
delete process.env['QWEN_CODE_MANAGED_NPM_UPDATE'];
preResolveQwenHome();
function getManagedNpmPin() {
try {
const pin = JSON.parse(process.env['QWEN_CODE_MANAGED_NPM_PIN'] ?? '');
if (pin.bootstrap !== currentEntryPath) return undefined;
if (
typeof pin.updateRoot !== 'string' ||
resolve(pin.updateRoot) !== pin.updateRoot
) {
return undefined;
}
if (pin.version === null) {
return { version: null, updateRoot: pin.updateRoot };
}
return typeof pin.version === 'string' &&
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
pin.version,
)
? { version: pin.version, updateRoot: pin.updateRoot }
: undefined;
} catch {
return undefined;
}
}
const managedNpmPin = getManagedNpmPin();
const managedNpmUpdateRoot =
managedNpmPin?.updateRoot ?? join(resolveQwenHome(), 'updates', 'npm');
function getManagedNpmInstallation() {
try {
if (managedNpmPin?.version === null) return undefined;
const launcherRoot = join(
managedNpmUpdateRoot,
createHash('sha256').update(currentEntryPath).digest('hex').slice(0, 16),
);
let version = managedNpmPin?.version;
if (version === undefined) {
const active = JSON.parse(
readFileSync(join(launcherRoot, 'active.json'), 'utf8'),
);
const basePackageJsonPath = [
join(__dirname, 'package.json'),
join(__dirname, '..', 'package.json'),
].find((candidate) => existsSync(candidate));
if (!basePackageJsonPath) return undefined;
const basePackage = JSON.parse(readFileSync(basePackageJsonPath, 'utf8'));
if (
typeof active.version !== 'string' ||
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
active.version,
) ||
typeof active.bootstrap !== 'string' ||
realpathSync(active.bootstrap) !== currentEntryPath ||
active.baseVersion !== basePackage.version ||
active.bootstrapCtimeMs !== statSync(currentEntryPath).ctimeMs
) {
return undefined;
}
version = active.version;
}
const packageRoot = join(
launcherRoot,
'versions',
version,
'node_modules',
'@qwen-code',
'qwen-code',
);
const packageJsonPath = join(packageRoot, 'package.json');
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
const cliPath = join(packageRoot, 'cli.js');
if (
pkg.name !== '@qwen-code/qwen-code' ||
pkg.version !== version ||
!existsSync(cliPath)
) {
return undefined;
}
process.env['QWEN_CODE_MANAGED_NPM_UPDATE'] = 'true';
return { cliPath, packageJsonPath, version };
} catch {
return undefined;
}
}
// The entry a subprocess should call to reach THIS build.
//
// A skill that shells out to `qwen …` gets whatever `qwen` PATH resolves to, which
// is not necessarily the code that launched it: with an older global install on the
// machine, a current-source daemon's `qwen review agent-prompt --role 0` landed in a
// v0.19.10 binary whose `agent-prompt` predates `--role`, and the run died on
// "Missing required argument: chunk". This file is the executable entry and the one
// thing that knows its own path, so it publishes it; `getShellContextEnvVars` passes
// it to every shell subprocess, and a caller prefers it over a bare `qwen`.
//
// Assignment, not `||=`: an inherited value is another session's CLI — an outer
// qwen shelling out to this one — and honouring it re-creates the exact skew above,
// one level up. Each entry stamps itself, so nested sessions each call their own
// build. The managed-version pin keeps nested calls on the running session's build
// even if a background update changes the active pointer. A post-update relaunch
// clears that pin so the launcher's wrapper selects the newly active build.
//
// One exception, and it points the SAME way: the standalone package launches this
// file through a shim (`bin/qwen`) that selects the BUNDLED Node — the host may
// have none — and announces itself via QWEN_CODE_LAUNCHER_PATH. There, "the entry
// that reaches this build" is the shim: stamping this file instead would hand
// subprocesses a `#!/usr/bin/env node` script on a machine where that resolves to
// nothing. Read before the spawn path deletes the variable below.
// Captured AND deleted here, not just read: the serve/mcp fast path below never
// reaches the spawn branch that used to delete it, so the hint leaked into every
// child of a standalone daemon — and a child qwen from a DIFFERENT checkout
// would read the outer shim and republish it as its own entry: the wrong build,
// wearing this one's stamp.
const standaloneShim = process.env['QWEN_CODE_LAUNCHER_PATH'];
delete process.env['QWEN_CODE_LAUNCHER_PATH'];
const managedRuntimeBootstrap =
cliArgs[0] === 'managed-runtime-bootstrap' ||
cliArgs[0] === 'managed-runtime-bootstrap-worker';
const managedNpmInstallation = managedRuntimeBootstrap
? undefined
: getManagedNpmInstallation();
const validStandaloneShim =
standaloneShim && existsSync(standaloneShim) ? standaloneShim : undefined;
process.env['QWEN_CODE_CLI'] =
validStandaloneShim ?? fileURLToPath(import.meta.url);
if (validStandaloneShim) {
delete process.env['QWEN_CODE_MANAGED_NPM_PIN'];
delete process.env['QWEN_CODE_MANAGED_NPM_ROOT'];
} else {
process.env['QWEN_CODE_MANAGED_NPM_ROOT'] = managedNpmUpdateRoot;
process.env['QWEN_CODE_MANAGED_NPM_PIN'] = JSON.stringify({
bootstrap: currentEntryPath,
version: managedNpmInstallation?.version ?? null,
updateRoot: managedNpmUpdateRoot,
});
}
const cliPathCandidates = [
managedNpmInstallation?.cliPath,
join(__dirname, 'cli.js'),
join(__dirname, '..', 'dist', 'cli.js'),
].filter(Boolean);
const packageJsonPathCandidates = [
managedNpmInstallation?.packageJsonPath,
join(__dirname, 'package.json'),
join(__dirname, '..', 'package.json'),
].filter(Boolean);
const cliPath =
cliPathCandidates.find((candidate) => existsSync(candidate)) ??
cliPathCandidates[0];
const packageJsonPath =
packageJsonPathCandidates.find((candidate) => existsSync(candidate)) ??
packageJsonPathCandidates[0];
// Keep this separate from CLI_VERSION: esbuild replaces CLI_VERSION with the
// build package version, while this value must survive nested wrapper calls.
if (!process.env['QWEN_CODE_STARTUP_VERSION']?.trim()) {
try {
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
process.env['QWEN_CODE_STARTUP_VERSION'] = pkg.version || 'unknown';
} catch {
// The CLI has its own version fallback when package metadata is unavailable.
}
}
if (isTopLevelVersion) {
try {
const { readFileSync } = await import('node:fs');
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
process.stdout.write(`${pkg.version || 'unknown'}\n`);
process.exit(0);
} catch {
// Fall through to cli.js, which has its own version fallback.
}
}
if (isInProcessFastPath()) {
const { default: module } = await import('node:module');
const compileCache = module.enableCompileCache?.();
if (
cliArgs[0] === 'serve' &&
!process.env['NODE_COMPILE_CACHE'] &&
compileCache?.status === module.constants?.compileCacheStatus?.ENABLED &&
compileCache?.directory
) {
process.env['QWEN_CODE_PENDING_COMPILE_CACHE'] = compileCache.directory;
}
process.argv[1] = cliPath;
await import(pathToFileURL(cliPath).href);
} else {
const { spawnSync } = await import('node:child_process');
const UPDATE_COMPLETE_EXIT_CODE = 44;
// A child that never started comes back from spawnSync as
// { status: null, signal: null, error }; say why instead of exiting silently.
const SPAWN_FAILURE_EXIT_CODE = 126;
const exitOnSpawnError = (command, error) => {
// Node's message already reads "<syscall> <path> <code>"; add only what it
// does not say.
const detail = [
...[error.code, error.syscall].filter(
(part) => typeof part === 'string' && !error.message.includes(part),
),
...(error.errno === undefined ? [] : [`errno ${error.errno}`]),
].join(' ');
process.stderr.write(
`Failed to start ${command}: ${error.message}${detail ? ` (${detail})` : ''}\n`,
);
process.exit(SPAWN_FAILURE_EXIT_CODE);
};
// cmd.exe metacharacters. Mirrors UNSAFE_CMD_CHARS in
// packages/cli/src/ui/standalone-update.ts — kept local because this
// plain-ESM entry ships in the standalone package and cannot import the
// TypeScript sources.
const UNSAFE_CMD_CHARS = /[&|<>^%!"`\n\r]/;
const entryPath = resolve(process.argv[1]);
delete process.env['QWEN_CODE_LAUNCHER_PID'];
const launchEnv = { ...process.env };
const findLauncher = () => {
const launcherNames =
process.platform === 'win32'
? ['qwen.cmd', 'qwen.exe', 'qwen']
: ['qwen'];
const entryRootLength = parse(entryPath).root.length;
const launcherFromEnv = standaloneShim;
const launcherCandidates = process.env['PATH']
?.split(delimiter)
.flatMap((dir) => launcherNames.map((name) => join(dir, name)))
.filter((candidate) => existsSync(candidate));
return launcherFromEnv && existsSync(launcherFromEnv)
? launcherFromEnv
: launcherCandidates
?.map((candidate) => {
if (resolve(candidate) === entryPath) {
return { candidate, score: Number.MAX_SAFE_INTEGER };
}
try {
if (realpathSync(candidate) === realpathSync(entryPath)) {
return { candidate, score: Number.MAX_SAFE_INTEGER };
}
} catch {
// Fall back to matching the installation prefix.
}
let parent = resolve(dirname(candidate));
while (
parent.length > entryRootLength &&
entryPath !== parent &&
!entryPath.startsWith(`${parent}${sep}`)
) {
const next = dirname(parent);
if (next === parent) break;
parent = next;
}
return { candidate, score: parent.length };
})
.filter(({ score }) => score > entryRootLength)
.sort((a, b) => b.score - a.score)[0]?.candidate;
};
const relaunchAfterUpdate = () => {
const launcher = findLauncher();
if (!launcher) {
process.stderr.write(
'Update successful! The new version will be used on your next run.\n',
);
process.exit(0);
}
const relaunchEnv = {
...launchEnv,
QWEN_CODE_RELAUNCH_ARGS: JSON.stringify(cliArgs),
QWEN_CODE_SKIP_UPDATE_CHECK_ONCE: 'true',
};
// This is a new startup after the managed update; let the new package stamp
// its own version instead of inheriting the old process's session version.
delete relaunchEnv['QWEN_CODE_STARTUP_VERSION'];
delete relaunchEnv['QWEN_CODE_MANAGED_NPM_PIN'];
if (
process.platform === 'win32' &&
launcher.endsWith('.cmd') &&
UNSAFE_CMD_CHARS.test(launcher)
) {
// The cmd.exe relaunch below hands its command line over verbatim
// (windowsVerbatimArguments), which drops Node's escaping safety net:
// a launcher path with cmd metacharacters would be re-tokenized into
// extra commands. The update itself already landed, so skip the
// relaunch instead of spawning a malformed command line.
process.stderr.write(
'Update successful! The new version will be used on your next run.\n',
);
process.exit(0);
// Unreachable in production; keeps tests with a mocked exit honest.
return;
}
const relaunchResult =
process.platform === 'win32' && launcher.endsWith('.cmd')
? spawnSync(
process.env['ComSpec'] ?? 'cmd.exe',
['/d', '/s', '/c', `""${launcher}""`],
// cmd.exe does not understand Node's MSVCRT argv escaping: it
// rewrites every embedded quote of the ""…"" idiom as \", and
// cmd /s rule 2 then strips the line down to a literal
// \"\"path\"\" program name (#12687). Hand the line over
// verbatim, as shellExecutionService already does for cmd.
{
stdio: 'inherit',
env: relaunchEnv,
windowsVerbatimArguments: true,
},
)
: spawnSync(launcher, [], {
stdio: 'inherit',
env: relaunchEnv,
});
if (relaunchResult.error) {
process.stderr.write(
'Update successful! The new version will be used on your next run.\n',
);
exitOnSpawnError(launcher, relaunchResult.error);
} else if (relaunchResult.signal) {
process.kill(process.pid, relaunchResult.signal);
} else {
process.exit(relaunchResult.status ?? 1);
}
};
// A fresh Node image (the spawned or relaunched CLI) reaches the cache only
// through NODE_COMPILE_CACHE; enabling it here also resolves the default
// directory and honours NODE_DISABLE_COMPILE_CACHE. Like the serve fast
// path, the value then reaches tool subprocesses too.
const { default: module } = await import('node:module');
const compileCache = module.enableCompileCache?.();
const compileCacheEnv =
!process.env['NODE_COMPILE_CACHE'] &&
compileCache?.status === module.constants?.compileCacheStatus?.ENABLED &&
compileCache?.directory
? { NODE_COMPILE_CACHE: compileCache.directory }
: {};
// Bun cannot expose gc at runtime (no v8.setFlagsFromString), so it keeps
// the child with --expose-gc in argv, as Windows does.
if (process.platform !== 'win32' && !process.versions.bun) {
// Running the CLI in this process instead of a child saves a Node boot and
// the idle launcher's memory for the whole session. The memory-pressure
// monitor's global.gc() comes from the runtime flag instead of argv.
const [{ setFlagsFromString }, { runInNewContext }] = await Promise.all([
import('node:v8'),
import('node:vm'),
]);
setFlagsFromString('--expose-gc');
globalThis.gc ??= runInNewContext('gc');
process.on('exit', (code) => {
if (code === UPDATE_COMPLETE_EXIT_CODE) relaunchAfterUpdate();
});
Object.assign(process.env, compileCacheEnv);
process.argv.splice(1, Infinity, cliPath, ...cliArgs);
await import(pathToFileURL(cliPath).href);
} else {
const result = spawnSync(
process.execPath,
['--expose-gc', cliPath, ...cliArgs],
{
stdio: 'inherit',
env: {
...process.env,
...compileCacheEnv,
QWEN_CODE_LAUNCHER_PID: String(process.pid),
},
},
);
if (result.error) {
exitOnSpawnError(`${process.execPath} ${cliPath}`, result.error);
} else if (result.signal) {
process.kill(process.pid, result.signal);
} else if (result.status !== UPDATE_COMPLETE_EXIT_CODE) {
process.exit(result.status ?? 1);
} else {
relaunchAfterUpdate();
}
}
}