* fix(cli): stop double-wrapping and double-printing API errors in non-interactive mode
In non-interactive (-p) mode, any upstream 4xx ended up on stderr three
times: once from the stream-error handler, once from handleError after
the thrown Error.message (already containing the formatted text) was
fed back through parseAndFormatApiError producing
"[API Error: [API Error: ...]]", and once more from
JsonOutputAdapter.emitResult writing the same errorMessage out in TEXT
mode. The top-level catch then framed the resulting throw as
"An unexpected critical error occurred:" with a stack trace, which
made a routine 4xx look like a CLI crash.
Three coordinated changes:
- AlreadyReportedError marks a throw whose message is already on the
wire. handleError short-circuits on it: no second writeStderrLine,
no second parseAndFormatApiError, just propagate the exit code.
- The non-interactive stream-error handler now throws
AlreadyReportedError, and the catch block skips the adapter's
emitResult in TEXT mode so we don't get a third copy.
- The top-level .catch in packages/cli/index.ts treats
AlreadyReportedError as a routine, already-reported failure: exit
with the carried code without printing the "unexpected critical"
framing or the stack trace.
parseAndFormatApiError is also made idempotent — input that already
starts with "[API Error: " and ends with "]" is returned unchanged.
That is the safety net: even if a future caller forgets to mark its
throw, the double-wrap symptom is impossible.
Tests cover all three layers: idempotency in errorParsing, the
short-circuit in handleError, and a regression test on
runNonInteractive that asserts no "[API Error: [API Error: ...]" line
is ever produced on stderr.
Fixes#3748
* fix(cli): make API error formatting idempotent for 429 suffix + use AlreadyReportedError
* fix(cli): follow up on error reporting review
Primary change: prevent the model from burning tokens in an infinite retry
loop when a tool call repeatedly fails schema validation with the same
error (observed with ask_user_question and a malformed `questions`
parameter retrying 10+ times with the same validation error).
- Track consecutive validation failures per (tool name, error message)
pair in CoreToolScheduler via a `validationRetryCounts` Map.
- After 3 consecutive failures for the same (tool, error) pair, append a
RETRY LOOP DETECTED directive to the error response instructing the
model to stop, re-examine the schema, try a fundamentally different
approach, or surface the issue to the user.
- Reset per-tool counters when the tool invocation succeeds; reset
globally when an incoming batch shares no tool name with any
previously failing tool; reset the per-tool counter when the tool
returns a different validation error so unrelated mistakes do not
accumulate toward the threshold.
- Distinct from LoopDetectionService, which tracks model-behavior loops
(repeated thoughts, stagnant actions); this change catches tool-API
misuse loops at the scheduler layer.
Piggyback fixes bundled in the same PR:
- packages/cli/index.ts, packages/core/src/services/shellExecutionService.ts:
treat PTY `EAGAIN` on the read path as an expected read error alongside
`EIO`, avoiding noisy surface-level failures from transient
non-blocking reads.
- scripts/build.js: switch the settings-schema generation step from
`npx tsx` to `node --import tsx/esm` for Bun compatibility.
Tests:
- Unit tests in coreToolScheduler.test.ts cover: directive injection on
the 3rd consecutive failure, counter reset when a different tool is
called, and counter reset after a successful invocation of the same
tool (fail → fail → succeed → fail → fail must not trip the directive).
feat(cli): add startup performance profiler (#3219)
Add a lightweight startup profiler activated via QWEN_CODE_PROFILE_STARTUP=1.
When enabled, collects performance.now() timestamps at 7 key phases in main()
and writes a JSON report to ~/.qwen/startup-perf/. Also records
process.uptime() at T0 to capture module loading time not covered by
checkpoint-based measurement.
Key design decisions:
- Only profiles inside sandbox child process to avoid duplicate reports
- initStartupProfiler() is idempotent (resets state on each call)
- Filename uses report.sessionId for consistency with JSON content
- Zero overhead when disabled (single env var check)
Initial measurement: module loading ~1342ms (94%), main() ~85ms (6%),
confirming barrel exports and eager dependency loading as primary
optimization targets for #3011.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Add global uncaught exception handler to suppress known race condition
in @lydell/node-pty where a deferred resize fires after the pty process
has already exited on Windows.
Tracking bug: https://github.com/microsoft/node-pty/issues/827
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>