Commit graph

4717 commits

Author SHA1 Message Date
qqqys
f84ddd434b
feat(core): fail impossible goals (#4230)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(core): fail impossible goals

* fix(core): refine impossible goal judgement

* fix(core): include goal feedback when continuing

* fix(core): clarify impossible goal terminal state

* fix(core): harden impossible goal feedback

* fix(core): log suppressed impossible verdicts

* fix(goal): address review suggestions

* test(goal): cover impossible parsing suggestions
2026-05-18 00:31:51 +08:00
Shaojin Wen
c93d66cd23
fix(serve): align build and integration test coverage (#4248)
* fix(serve): align test coverage with build inputs

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(serve): address review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-18 00:01:47 +08:00
xMillogx
79f38e0701
fix: add cache limits to prevent OOM during build/test (#4188)
* fix: add cache limits to prevent OOM during build/test

* chore: remove intermediate OOM analysis document

* fix(core): enforce MAX_TOTAL_PATHS cap when updating existing crawlCache key

Before: !crawlCache.has(key) guard in the MAX_TOTAL_PATHS eviction loop
short-circuited eviction when updating an existing key, allowing cache to
grow beyond 50,000 paths (F1 bug - OOM protection bypassed).

After: totalPaths is calculated excluding the current key, and the eviction
loop protects the key being written from being evicted as "largest".
FIFO bump (delete+set) ensures frequently updated keys move to end of queue.

Per @wenshao review: MAX_CACHE_ENTRIES guard on line 59 is preserved
(updating existing key doesn't increase entry count).

* test(core): add cache eviction tests and fix MAX_TOTAL_PATHS loop guard

* test(core): add eviction coverage for fileReadCache and crawlCache

* test(core): unskip bumped entries eviction test now that upsert bump is implemented

---------

Co-authored-by: Юрий Острожных <millog@mail.ru>
2026-05-17 23:01:23 +08:00
jinye
60fe594e8f
feat(serve): add read-only status routes (#4241)
* feat(serve): add read-only status routes

Add read-only daemon status endpoints for workspace MCP, skills, providers, session context, and session supported commands.

Expose matching typed SDK helpers and document the new additive v1 status surface.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): harden read-only status snapshots

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): address read-only status review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-17 21:37:20 +08:00
jinye
aef35c390e
feat(serve): session metadata and close/delete lifecycle (#4175 Wave 2.5 PR 11) (#4240)
* feat(serve): session metadata and close/delete lifecycle (#4175 Wave 2.5 PR 11)

Add explicit session close and metadata management to the daemon serve
infrastructure, closing the Stage 1 limitation that sessions could only
end via child crash or daemon shutdown.

- DELETE /session/:id — force-closes a live session (cancels active
  prompt, resolves pending permissions, publishes session_closed event)
- PATCH /session/:id/metadata — update mutable displayName
- Enriched GET /workspace/:id/sessions with createdAt, displayName,
  clientCount, hasActivePrompt
- session_closed + session_metadata_updated SDK event types with
  validation, reducer, and terminal event priority
- DaemonClient.closeSession / updateSessionMetadata + session client
  wrappers
- Capabilities: session_close, session_metadata

* fix(serve): address review feedback on session lifecycle PR

- Fix JSDoc on closeSession: clarify that bridge throws SessionNotFoundError
  (SDK absorbs 404 for client-side idempotency)
- Tighten event validators: isSessionClosedData checks closedBy type,
  isSessionMetadataUpdatedData checks displayName type
- PATCH /session/:id/metadata now returns effective stored metadata
  instead of echoing request fields, avoiding ambiguous no-op responses
- Only publish session_metadata_updated event when displayName changes
- Update chooseTerminalEvent comment to reflect session_closed

* fix: address PR 4240 review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: address remaining PR 4240 suggestions

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: update serve sessions test mock

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-17 20:42:15 +08:00
JerryLee
9abd704e09
fix(cli): record mid-turn queued user prompts (#4215) 2026-05-17 20:21:06 +08:00
jinye
4e06967c2b
feat(serve): mutation gating helper and --require-auth (#4236)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(serve): mutation gating helper and --require-auth

Implements issue #4175 Wave 4 PR 15. Adds the centralized
state-changing-route gate that Wave 4 follow-ups (memory CRUD, file
edit, MCP restart, device-flow auth) will reuse, plus the
`--require-auth` deployment knob that hardens the loopback developer
default for shared dev hosts / CI runners.

- `createMutationGate({ tokenConfigured, requireAuth })` factory in
  serve/auth.ts — per-route middleware with a 4-cell behavior matrix:
  pass-through under `requireAuth` or any token configured;
  `401 token_required` for `strict: true` routes on no-token loopback
  defaults; baseline pass-through otherwise.
- Existing Wave 1-2 mutation routes (POST /session, /session/:id/{load,
  resume,prompt,cancel,model}, /permission/:requestId) opt into the
  default non-strict factory call as the centralization marker. Wave 4
  routes will pass `{ strict: true }` to require a token even on
  loopback.
- `--require-auth` CLI flag + `ServeOptions.requireAuth`. Boot refuses
  without a token; closes the `/health` exemption when on so loopback
  `/health` also requires bearer auth; stderr breadcrumb so the
  hardened mode is visible in journald/docker logs.
- Conditional `require_auth` capability tag advertised only when the
  flag is on. New `CONDITIONAL_SERVE_FEATURES` registry primitive so
  future per-deployment toggles follow the same shape.
- 5 new unit tests in auth.test.ts covering the gate matrix; 5 added
  in server.test.ts for capability advertisement, conditional tag,
  /health 401 under --require-auth, and runQwenServe boot
  refusal + happy path. 245/245 serve tests pass; typecheck + eslint
  clean.

Refs: #4175

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fixup(serve): address PR #4236 review feedback

Three small follow-ups from the automated reviewers on PR #4236:

1. **Drop misleading `--require-auth` from `token_required` error
   message** (Copilot inline auth.ts:262). The strict-mode 401
   listed three remediations but `--require-auth` is paired-required
   with a token at boot — naming it standalone would loop the operator
   into a different boot error. Keep the two valid standalone fixes
   (env var, --token); add inline note explaining the omission.
   `auth.test.ts` regex updated to `not.toMatch(/--require-auth/)`
   to anchor the new wording.

2. **Mention `/health` gating in `--require-auth` CLI description**
   (auto-reviewer Medium #2). Operators flipping the flag without
   reading the protocol doc would get paged when k8s/Compose probes
   start 401-ing. One sentence in the yargs description prevents that.

3. **Drift insurance comment between registry and
   `CONDITIONAL_SERVE_FEATURES`** (auto-reviewer Low #3). Document
   the four-step procedure for adding a new conditional tag so a
   future contributor doesn't update only the registry and silently
   advertise the tag unconditionally. Notes the Map<predicate>
   refactor as the right move when a second tag lands.

Deferred (not in this fix-up):
- Module-level PASSTHROUGH singleton (High #1) — micro-optimization,
  unmeasurable.
- Map<feature, predicate> for conditional features (High #2) —
  premature abstraction with one tag.
- Per-route `// non-strict marker` comments (Medium #1) — noise.
- `@see` cross-ref in types.ts (Low #2) — sugar.
- JSDoc bullet-list vs table (Low #1) — current format is fine.

Refs: #4175 #4236

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fixup(serve): address PR #4236 round-2 review feedback

Five small follow-ups from @wenshao + DeepSeek (via Qwen Code /review)
on PR #4236:

1. **Map<predicate> refactor for `CONDITIONAL_SERVE_FEATURES`**
   (review threads #3254467192 + #3254485912). Two reviewers asked
   for the same shape on the grounds that the `Set` + per-feature
   `if`-branch needed FOUR coordinated changes per new conditional
   tag and silently fail-CLOSED when the branch was missed. The Map
   collapses the predicate-decision and the set-membership into one
   entry per feature — adding a new conditional tag is now two
   coordinated changes (registry + Map entry) and a missing predicate
   is a TypeScript error rather than a silent omission. JSDoc
   updated.

2. **Drift-insurance test that iterates `CONDITIONAL_SERVE_FEATURES`**
   (review thread #3254467192 option 1, layered on top of #1).
   `server.test.ts` now walks every Map entry and asserts the
   predicate accepts/rejects as expected; future entries that don't
   add an assertion branch fail the test loudly so a missing
   predicate cannot ship silently. Adoption-of-record for the Map
   shape rather than relying on a hand-maintained invariant.

3. **Cache `strictDenier` for allocation symmetry** (review thread
   #3254467193). Wave 4 PRs will mount strict mode on multiple
   routes; without the cache each `mutate({strict:true})` call would
   allocate a fresh 401 closure. Now both the passthrough and the
   strict denier are pre-built singletons. Identity assertion in
   `auth.test.ts` anchors the cache so a future change that loses it
   surfaces in CI.

4. **Doc cosmetic — extra blank line in qwen-serve.md** (review
   thread #3254467198). Single blank line between the `>` quoted
   example and the following non-quoted bash block now.

5. **Doc correctness — `require_auth` is post-auth confirmation**
   (review thread #3254485910 from DeepSeek). When `--require-auth`
   is on, the global `bearerAuth` middleware gates every route
   including `/capabilities`, so an unauthenticated client cannot
   pre-flight `caps.features` to discover that auth is required —
   the discovery surface is the 401 response body itself. Both
   `qwen-serve.md` and `qwen-serve-protocol.md` rewritten to
   describe the tag as a post-authentication confirmation, matching
   the auth.ts JSDoc which already stated this correctly.

Trade-offs documented (no code change):

- **Body-parser ordering** (review thread #3254485915 from DeepSeek)
  noted as a comment block in `auth.ts`. Strict-mode 401 fires AFTER
  `express.json()` because the gate is per-route middleware. On
  loopback no-token defaults a strict route therefore parses the
  request body before refusing it — bounded by
  `express.json({limit: '10mb'})` × `--max-connections` (256
  default). Strict routes Wave 4 actually adds carry small bodies in
  legitimate use, so this isn't a production hot path. Future routes
  accepting large bodies should lift the gate to app-level (maintain
  a strict-path Set in `createServeApp`); flagged as a Wave 4
  follow-up rather than re-architecting the helper.

- **`bearerAuth` body-shape inconsistency** (review thread
  #3254467197 from @wenshao) flagged as a Wave 4 cross-PR
  follow-up. `bearerAuth` returns `{error: 'Unauthorized'}` while
  the strict gate returns `{code: 'token_required', error: '...'}`;
  SDK clients have to branch on both shapes. Standardizing
  `bearerAuth` to also carry a `code` field is orthogonal to this
  PR's scope.

Validation: 260/260 cli serve tests pass (was 258 — added the drift
insurance test + strict denier identity test); typecheck + eslint
clean.

Refs: #4175 #4236

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-17 20:10:37 +08:00
易良
eef06ce376
feat(cli): add structured memory diagnostics JSON (#3785)
* feat(cli): add memory diagnostics doctor command

* fix(core): platform-aware maxRSS conversion and accurate risk message

- Extract platform detection before building diagnostics so the correct
  unit conversion can be applied: multiply by 1024 on Linux (where
  process.resourceUsage().maxRSS is in KB) but leave the value unchanged
  on macOS/Windows (where it is already in bytes).
- Correct the native-memory-pressure risk message to accurately state
  that the threshold is 2× heap used, not just "larger than heapUsed".
- Add a dedicated test to assert that maxRSS is not multiplied on a
  non-Linux platform (darwin).

All 3 core and 9 CLI tests pass; typecheck clean.

Agent-Logs-Url: https://github.com/QwenLM/qwen-code/sessions/9b413337-68ed-4d5c-af99-0d42378900c3

* test(core): cover active request memory risk

* fix(cli): address memory diagnostics review feedback

* fix(cli): harden memory diagnostics review fixes

* fix(memory-diagnostics): tighten risk thresholds and expand readable output

- Add 64MB absolute floor on native-memory-pressure so cold processes don't trip
  the 2x ratio check; raise active-handles threshold from 100 to 256
- Show detachedContexts, nativeContexts, maxRSS, CPU times, smapsRollup
  availability, and v8HeapSpaces summary in the readable /doctor memory output
- Validate unknown memory subcommand args with a usage hint instead of silently
  dropping them
- Wrap human-readable strings in t(...) for i18n parity with the rest of doctor
- Advertise the memory subcommand via /doctor argumentHint while keeping
  acceptsInput false so the parent still auto-submits
- Document _getActiveHandles/_getActiveRequests as undocumented Node internals
- Update tests for new thresholds, expanded output, unknown-arg path, and
  abort-during-json

* fix(cli): harden memory doctor diagnostics

* fix(core): correct maxRSS byte handling and heapRatio consistency

- Remove incorrect * 1024 multiplier for maxRSS on Linux (Node.js >=14.10 returns bytes on all platforms)
- Use v8HeapStats.usedHeapSize for heapRatio to avoid cross-API inconsistency
- Update test expectations and rename "does not multiply" test

* fix(cli): resolve rebase conflicts in memory diagnostics

- Rename local formatMemoryDiagnostics to formatCoreDiagnostics to avoid
  naming conflict with the imported utility from memoryDiagnostics.js
- Update Session.test.ts to use objectContaining for _meta field added
  in recent main commits
- Align doctorCommand.test.ts assertions with current parent command
  state (argumentHint includes --sample/--snapshot from main)

* fix(core): use null instead of undefined for optional probes, deduplicate active count helpers

- optionalProbe/optionalSyncProbe now return null on failure so
  JSON.stringify preserves the keys instead of silently omitting them.
- Merge getActiveHandlesCount/getActiveRequestsCount into a single
  parameterized getProcessInternalCount helper.
- Update MemoryDiagnostics interface: v8HeapSpaces, openFileDescriptors,
  smapsRollup are now T | null instead of T | undefined.

* fix(cli): finish memory diagnostics review fixes

* fix(cli): address memory diagnostics review feedback

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-17 19:52:46 +08:00
Yan Shen
9985d91e08
feat(cli): add configurable plansDirectory for Plan Mode (#4062)
* feat(cli): add configurable plansDirectory for Plan Mode

Add a plansDirectory setting that allows users to define a custom
directory for approved Plan Mode files. Relative paths are resolved
against the project root and validated to prevent path traversal.

- Storage: add isPathWithinDirectory() with realpathSync-based symlink
  resolution to prevent traversal bypass attacks (direct, intermediate,
  and cross-drive)
- Config: cache plansDir at construction time, use atomic write
  (write-temp then rename) to prevent corrupted plan files on crash
- CLI: respect bareMode by clearing plansDirectory in minimal mode
- Docs: document plansDirectory with requiresRestart and gitignore hint
- Tests: 26 new tests covering path validation, symlink attacks
  (direct and intermediate), Windows cross-drive paths, mixed
  separators, and configuration integration

Closes #3548

* fix(core): align symlink test with return value

* fix(core): harden plans directory handling

* fix(config): address PR #4062 review findings for plansDirectory

- Handle EXDEV during atomic plan writes (cross-device rename fallback)

- Sanitize session IDs to prevent path traversal in plan filenames

- Expand tilde (~) in configured plansDirectory paths

- Preserve plansDirectory in bare mode

- Add EACCES/EPERM handling to getPlanFileNames with user-visible warnings

- Close TOCTOU gap with post-write path containment validation

- Fix docs to clarify plansDirectory is a top-level key

- Add happy-path I/O tests for configured plansDirectory
2026-05-17 19:43:24 +08:00
jinye
d2d426fad0
feat(serve): SSE replay sizing + slow_client_warning backpressure (#4175 Wave 2.5 PR 10) (#4237)
* feat(serve): SSE replay sizing + slow_client_warning backpressure

#4175 Wave 2.5 PR 10. Closes the SSE replay / backpressure knobs
called out in #3803 §02 so chatty Stage 1 sessions get an honest
reconnect window and operators get a heads-up signal before clients
are summarily evicted.

- **`DEFAULT_RING_SIZE` 4000 → 8000.** Per-session replay ring depth
  now matches the #3803 §02 target for chatty sessions.
- **`--event-ring-size <n>`** CLI flag (default 8000) lets operators
  tune the ring per daemon. Threaded `ServeOptions` →
  `BridgeOptions.eventRingSize` → both `new EventBus()` construction
  sites (fresh sessions + restore path). Validation is fail-CLOSED
  (positive finite integer; 0 / NaN / negative throw at boot).
- **`slow_client_warning` SSE frame.** When a subscriber's queue
  crosses 75% full the bus force-pushes a synthetic
  `slow_client_warning` to that subscriber once per overflow
  episode, carrying `{queueSize, maxQueued, lastEventId}`. The flag
  re-arms after the queue drains below 37.5% (hysteresis, no flap
  near threshold). If the queue actually overflows after the
  warning, the existing `client_evicted` terminal frame path still
  fires. Like `client_evicted`, the warning has no `id` (synthetic
  frame; must not burn a sequence slot for other subscribers).
- **`?maxQueued=N`** query param on `GET /session/:id/events`
  (range `[16, 2048]`, default 256). Lets cold reconnect clients
  pre-size their per-subscriber backlog so a large `Last-Event-ID:
  0` replay doesn't trip the warning on the first publish. Range
  rationale: lower bound 16 (smaller is useless for any replay);
  upper bound 2048 (so a single subscriber can't pin ~1 MB just by
  asking). Out-of-range / non-decimal returns `400
  invalid_max_queued` BEFORE opening the SSE stream — clean 4xx
  beats half-opening a stream + emitting a `stream_error` (which
  EventSource would auto-reconnect on).
- **`slow_client_warning` capability tag** — single source of truth
  for the warning frame + `?maxQueued` query param + ring-size
  knob. Old daemons silently lack all of these; pre-flight via
  `caps.features`.
- **SDK extensions** (`@qwen-code/sdk`): typed
  `DaemonSlowClientWarningEvent` (added to known event union and
  `DaemonStreamLifecycleEvent`); schema-validated by a new
  `isSlowClientWarningData` predicate; reducer
  (`reduceDaemonSessionEvent`) increments `slowClientWarningCount`
  + stores `lastSlowClientWarning`. Warning is **non-terminal** —
  `alive` stays true (only `client_evicted` / `stream_error` /
  `session_died` close the stream). Re-exported from the public
  SDK entry.
- **Docs**: `qwen-serve-protocol.md` updates the features list (adds
  `slow_client_warning` and the previously-missing `client_identity`
  to match reality post-#4231), documents the `?maxQueued` query
  param, adds the warning frame to the event table, and notes the
  new default ring size. `qwen-serve.md` adds the `--event-ring-size`
  flag row.

Tests: 19 eventBus (4 new: warning at 75%, once per episode,
no `id` on the synthetic frame, hysteresis re-arm), 106 bridge
(2 new: validate eventRingSize accept/reject), 111 server (4 new:
?maxQueued accept/absent/non-decimal/out-of-range +
EXPECTED_STAGE1_FEATURES update), 14 SDK daemonEvents (2 new:
schema validation + non-terminal reducer behavior). 321 focused
tests total, all green.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* refactor(serve): adopt PR #4237 review feedback (eventBus polish)

Address the actionable items from the Qwen Code review bot's pass
on PR #4237:

- Pre-compute `warnThreshold` / `warnResetThreshold` per
  `InternalSub` at `subscribe()` time so `publish()`'s per-event
  hot path is one integer compare per subscriber instead of a
  multiply + compare. The `!warned` short-circuit still collapses
  the steady state to a single boolean read; this just shaves a
  multiply when the threshold check actually fires.
- Document the back-of-queue ordering choice for the synthetic
  `slow_client_warning` frame in `EventBus.publish()`: front-push
  was considered but mid-stream front-insertion would mis-count
  `forcedInBuf` in `BoundedAsyncQueue.next()`, and `forcePush`
  already short-circuits via `resolvers.shift()` for the
  active-consumer case — the back-of-queue path only matters for
  stalled consumers, who can't drain regardless of warning
  position.
- Reuse the existing `collect()` helper in the "default ring size
  8000" test for consistency with the rest of the file; the new
  test also tightens the assertion by checking that the first
  retained event id is 2 (id=1 dropped by the ring) and the last
  is 8001.
- Soften the "~500 B per session" magic number in
  `BridgeOptions.eventRingSize`'s JSDoc to a qualitative
  description (each retained `BridgeEvent` is a reference plus its
  serialized payload; ceiling scales as
  `ringSize × average-event-size`).

Rejected:
- Bot's claim that the error JSON contains `\`...\`` escape
  sequences — bot misread the JS template-literal source as the
  wire output; `JSON.stringify` does not escape backticks, and
  the existing `cwd` error messages use the same style.
- Bot's "use `Record<string, never>` instead of `[key: string]:
  unknown`" suggestion on `DaemonSlowClientWarningData` — every
  other event-data type in `sdk-typescript/src/daemon/events.ts`
  carries the same index signature for additive-field
  compatibility.
- Bot's "features list breaks alphabetical order" — the
  capability list is grouped by protocol lifecycle (health →
  capabilities → session lifecycle → events → permissions), not
  alphabetical.

Tests: 139 focused tests across eventBus + httpAcpBridge + SDK
daemon events — all passing. Behavior unchanged; this is
hot-path micro-opt + comment polish only.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(serve): correct queue tagging + plumb maxQueued through SDK

Address both P2 findings from the Codex review pass on PR #4237.

**Bug 1: `BoundedAsyncQueue.forcedInBuf` position-invariant break**

The previous `forcedInBuf` counter only tracked LIVE-vs-FORCED
correctly when all forced entries lived at the FRONT of the buffer
(subscribe-time `Last-Event-ID` replay). The new mid-stream
`slow_client_warning` path force-pushes to the BACK of the queue
while the queue is still open, which the existing accounting was
not designed for:

  - publish 6 events at maxQueued=8 → 75% threshold trips →
    force-push warning at the back → buf=[1..6, warning],
    forcedInBuf=1.
  - consumer shifts `1` → forcedInBuf decremented to 0 (incorrect:
    `1` was a live frame, not the forced one).
  - consumer drains 2..6 + warning → buf=[], forcedInBuf=0, true
    live count = 0, but `size` getter and `push()` cap check then
    use `buf.length - forcedInBuf` which drifts over subsequent
    refills, causing premature warn / eviction before the cap is
    actually reached.

Replace the position-dependent counter with a per-entry
`{value, forced}` tag. `liveCount` is incremented in `push()` /
decremented in `next()` only when the shifted entry was non-forced
— position becomes irrelevant. `size` getter returns `liveCount`
directly. The class doc comment is rewritten to call out that the
new tag is the position-independent replacement for the old
"forced frames must stay at the front" invariant.

Regression test in `eventBus.test.ts` reproduces the codex trace
(warn at 75%, drain past warning, refill to cap) and asserts no
premature eviction.

**Bug 2: SDK does not expose `?maxQueued`**

`docs/users/qwen-serve.md` and `docs/developers/qwen-serve-protocol.md`
both document `?maxQueued=N` as something SDK clients can request,
but `SubscribeOptions` on `DaemonClient` only declared `lastEventId`
+ `signal`, and `subscribeEvents()` always fetched `/events` without
a query string. Typed-SDK consumers had no way to opt in without
hand-crafting URLs.

  - Add `SubscribeOptions.maxQueued?: number` with JSDoc noting the
    daemon range `[16, 2048]` and the pre-flight requirement on
    `caps.features.slow_client_warning`.
  - `DaemonClient.subscribeEvents` builds the URL with an optional
    `?maxQueued=<n>` segment. No client-side range validation —
    the daemon's `parseMaxQueuedQuery` is the source of truth and
    returns structured `400 invalid_max_queued`; duplicating the
    bounds in two layers would diverge on the next tweak.
  - `DaemonSessionSubscribeOptions extends SubscribeOptions` so the
    new field flows through `DaemonSessionClient` automatically.

Three new SDK tests:
  - subscribeEvents appends `?maxQueued=N` when set
  - omits the query string when absent (existing behavior preserved)
  - propagates a `400 invalid_max_queued` unchanged

Tests: 214 focused tests across eventBus / bridge / SDK
DaemonClient / DaemonSessionClient / daemonEvents, plus 111 in the
server suite. All green; the new eventBus regression case proves
the position-invariant fix.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* refactor(serve): adopt PR #4237 copilot review feedback

Address 6 of 8 copilot-reviewer findings on PR #4237; the other 2
(#1 forcedInBuf live-size corruption, #5 SDK lacks maxQueued) were
already fixed in bae42c88b — replied on the threads with the
commit hash.

- **[2] server.ts:1068** — `?maxQueued=` (present-but-empty) now
  fails closed with `400 invalid_max_queued` instead of silently
  falling back to the default queue cap. The API documents
  fail-closed for any malformed value before opening SSE, so an
  empty string is unambiguously malformed. New server.test.ts
  case locks this in.
- **[3] commands/serve.ts:93** — CLI help text for
  `--event-ring-size` no longer mis-shapes `Last-Event-ID` as a
  query parameter. It is an HTTP header, and the daemon's SSE
  route does not parse a `?Last-Event-ID=` query.
- **[4] docs/developers/qwen-serve-protocol.md:351** — clarify
  that `?maxQueued=N` controls the LIVE-event backlog cap.
  Replay frames are force-pushed and exempt from the cap; what
  consumes it is live events that arrive while the subscriber is
  still draining a cold-reconnect replay. Bumping for cold
  reconnects is still the right answer, but for the live tail,
  not for the replay frames themselves.
- **[6] eventBus.ts:214** — stale `ringSize=4000` performance
  comment updated to the new `ringSize=8000` default with a note
  about the O(n) `shift()` cost scaling.
- **[7] sdk-typescript events.ts:492** — `isSlowClientWarningData`
  now uses the existing `isFiniteNumber` helper instead of bare
  `typeof === 'number'`. Mirrors the sibling predicates and
  rejects `NaN` / `Infinity` payloads as schema garbage. New
  daemonEvents.test.ts assertions cover both.
- **[8] server.ts:127** — `createServeApp`'s default-bridge
  construction now also forwards `opts.eventRingSize` to
  `createHttpAcpBridge`, symmetric with the `runQwenServe.ts`
  path. Direct embeds / tests that called `createServeApp`
  without supplying their own bridge but did pass
  `ServeOptions.eventRingSize` were silently getting the
  default 8000 ring.

Tests: 326 focused tests across eventBus / bridge / SDK
DaemonClient / DaemonSessionClient / daemonEvents / server. All
green; the new server.test.ts case + the extended
daemonEvents.test.ts assertions cover the tightened guards.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* refactor(serve): adopt PR #4237 wenshao round-2 review feedback

Six adopted findings from @wenshao's second review pass on
PR #4237. The seventh ([10] forcedInBuf 3rd case invariant) was
already fixed in bae42c88b — replied on that thread.

- **[9] + [14] server.ts** — Sanitize attacker-controlled values
  before stderr interpolation in both `parseMaxQueuedQuery` and
  `parseLastEventId`. New `safeLogValue()` helper uses
  `JSON.stringify` to escape control characters (`\n`/`\r`/…) so a
  URL-encoded newline in `?maxQueued=%0a` can't inject extra log
  lines into journald/Loki/Splunk pipelines. Matches the
  `workspace_mismatch` sanitization style in `sendBridgeError`.
  Fixed in both helpers (the sibling pre-existing
  `parseLastEventId` had the same shape) so the file stays
  consistent.

- **[11] httpAcpBridge.ts** — `!Number.isFinite(eventRingSize)`
  was redundant: `Number.isInteger(NaN)` and
  `Number.isInteger(Infinity)` both return `false`, so the sibling
  `!Number.isInteger` already catches both. Drop the dead guard.

- **[12] httpAcpBridge.ts** — Add soft upper bound
  `MAX_EVENT_RING_SIZE = 1_000_000` on `eventRingSize` to catch
  operator typos (`--event-ring-size 80000000` vs `8000000`). At
  ~500 B per `BridgeEvent` an 1M-frame ring already pins ~500 MB
  per session — well past any realistic workload. Not a security
  boundary (operator-controlled flag), pure typo defense. Existing
  bridge construction test extended with an `80_000_000` case.

- **[13] commands/serve.ts** — CLI `--event-ring-size` flag now
  sources its default from `DEFAULT_RING_SIZE` (imported from
  `serve/eventBus.js`) instead of the hardcoded literal `8000`.
  Without this, a future bump of the bus default would silently
  not take effect for daemons launched through the CLI because
  the flag always overrides — single source of truth fixes that.

- **[15] eventBus.ts** — Drop unreachable `event.id ?? this.lastEventId`
  fallback in the `slow_client_warning` frame. `event` is locally
  constructed at the top of `publish()` with `id: this.nextId++`
  and is guaranteed defined. Use `event.id as number` directly +
  an inline note about the invariant.

Tests: 197 (eventBus 20 / bridge 107 / SDK DaemonClient 57 / SDK
daemonEvents 14) + 112 server. All green; the new upper-bound
bridge case + the existing log assertions pin the changed
behaviors.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-17 19:30:43 +08:00
JerryLee
6a1d55c737
fix(core): apply tool name migrations at dispatch (#4213) 2026-05-17 19:04:08 +08:00
jinye
0a4a08e443
feat(serve): add client heartbeat (#4175 Wave 2.5 PR 9) (#4235)
* feat(serve): add client heartbeat route

Adds POST /session/:id/heartbeat plus SDK helpers so long-lived
adapters (TUI/IDE/web) can refresh the daemon's last-seen
bookkeeping. Bridge stores per-session and per-client timestamps
behind a getHeartbeatState() snapshot accessor that PR 12
read-only diagnostics and PR 24 revocation policy will consume.

- Capability tag: client_heartbeat (advertised on /capabilities.features)
- Identified clients must echo X-Qwen-Client-Id; the bridge validates
  the id BEFORE bumping any timestamp so a forged id can't mask
  client absence
- Per-client entries are dropped together with the registration
  ref-count in unregisterClient, so churn doesn't leak stale ids
- getHeartbeatState returns a snapshot Map; mutating it does not
  leak into bridge state
- Anonymous heartbeats bump only the per-session watermark

Errors mirror the rest of the routes — 404 SessionNotFoundError, 400
invalid_client_id (header malformed or unknown for this session).

Roadmap PR 9 from #4175. Depends on PR 7 (#4231 client identity,
merged) for the trusted clientId registry.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* feat(sdk): re-export HeartbeatResult from package root

The published @qwen-code/sdk only exposes the root entrypoint via
`exports`; daemon subpath imports are not part of the public API.
Adding HeartbeatResult to packages/sdk-typescript/src/daemon/index.ts
made it reachable internally but not for downstream consumers writing
`import type { HeartbeatResult } from '@qwen-code/sdk'` — every other
daemon result type (PromptResult, SetModelResult, DaemonSession, etc.)
is forwarded through the root barrel, so HeartbeatResult was the only
hole in the heartbeat helper's public surface.

Inserted alphabetically between DaemonStreamLifecycleEvent and
KnownDaemonEvent to match the existing ordering convention.
2026-05-17 18:57:28 +08:00
jinye
07e0e82258
feat(serve): advertise typed_event_schema + pin SDK public surface (#4175 PR 4 follow-up) (#4226)
* feat(serve): advertise typed_event_schema capability

Follow-up to #4217 (`feat(protocol): add typed daemon event schema v1`,
Wave 1 PR 4 of #4175), which landed the SDK-side typed schema +
`KnownDaemonEvent` union + reducer but did not register a daemon-side
capability tag for it. Without the tag, non-SDK clients (web debug
UI, third-party adapters, channel/IDE backends not yet on
`@qwen-code/sdk`) have no way to detect at the protocol envelope
level that the daemon promises to emit only `KnownDaemonEvent`-shaped
frames — they would either pin against SDK version, or pre-flight
every frame defensively.

Add `typed_event_schema: { since: 'v1' }` to `SERVE_CAPABILITY_REGISTRY`,
inserted right after `session_events` (the route that delivers the
frames whose schema this tag describes). The capability is purely
informational — `narrowDaemonEvent`/`asKnownDaemonEvent` already
fall back to "unknown" for older daemons that don't advertise the
tag, so the SDK does not gate any behavior off the tag.

Sync `EXPECTED_STAGE1_FEATURES` (server.test.ts) and the integration
test array (qwen-serve-routes.test.ts) with the registry order, the
same lockstep discipline #4214 codified.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* test(sdk): pin typed event surface at the public SDK entry, point DaemonSessionClient docstring at it

Two small follow-ups to #4217 (Wave 1 PR 4 of #4175).

1. Public-entry regression fence

   `@qwen-code/sdk` is a single-entry package: `package.json.exports`
   only exposes `.` (`dist/index.{cjs,mjs,d.ts}`), and the bundle
   is built from `src/index.ts`. Symbols re-exported only from
   `src/daemon/index.ts` are unreachable to consumers unless they
   are also forwarded by `src/index.ts`. #4217 forwards the typed
   event schema correctly today, but the two-layer chain has no
   compile-time test pinning it — a future daemon export that lands
   in `src/daemon/index.ts` but is missed by `src/index.ts` would
   ship invisibly.

   Add `test/unit/daemon-public-surface.test.ts` that imports
   `* as Public from '../../src/index.js'`, asserts at runtime that
   every PR 4 value is `typeof === 'function'` (or a primitive of
   the expected shape), round-trips a raw `DaemonEvent` through the
   public `asKnownDaemonEvent` to prove the wire-up actually works,
   and compile-imports every PR 4 type so any drift fails to build.

2. DaemonSessionClient docstring pointer

   The class docstring already deferred typed event consumption to
   "the protocol schema layer" without a concrete pointer. Now that
   #4217 has put `asKnownDaemonEvent` and `reduceDaemonSessionEvent`
   in `./events.js`, name them so future readers can find the
   typed surface without grepping. No code change.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
2026-05-17 18:43:38 +08:00
易良
ab03badaae
fix(core): extend DashScope provider detection with additional hostname rules (#4157)
* fix(core): extend DashScope provider detection with additional hostname rules

Add internal domain patterns (*.dw.alibaba-inc.com and
*.data.aliyun-inc.com) to isDashScopeProvider() so that
DashScope-compatible private gateways are recognized
without requiring DASHSCOPE_PROXY_BASE_URL.

Closes #4138

* feat(core): add providerType field for explicit DashScope provider opt-in

Allow modelProviders entries to declare providerType: 'dashscope'
so that custom endpoints on private gateways are recognized as
DashScope-compatible without relying solely on hostname detection.

Closes #4138

* revert(core): remove DashScope providerType opt-in

* refactor(core): broaden dashscope internal domain match to *.alibaba-inc.com / *.aliyun-inc.com

Drop the business-specific dw./data. subdomain checks and treat any
*.alibaba-inc.com or *.aliyun-inc.com host as DashScope-compatible.
If a future internal service under these domains is not compatible,
narrow it via a runtime provider property rather than re-introducing
business-name subdomains.

* fix(core): clarify internal dashscope origin matching

* docs(core): clarify DashScope internal host matching
2026-05-17 18:36:42 +08:00
kkhomej33-netizen
a5e4839e07
fix(cli): restore ACP prompt counter on resume (#4233) 2026-05-17 18:32:44 +08:00
Shaojin Wen
0240c310fd
feat(core): PR-2.5 — post-promote stream redirect + natural-exit registry settle (#3831 follow-up) (#4102)
* feat(core): PR-2.5 — post-promote stream redirect + natural-exit registry settle

Closes the two limitations PR-2 (#3894) deferred for the Phase D part
(b) Ctrl+B promote flow (#3831):

1. **Post-promote stream redirect**: today the `bg_xxx.output` file
   is frozen at promote time because `ShellExecutionService` detaches
   its data listener as part of PR-1's ownership-transfer contract.
   PR-2.5 wires a caller-side `onPostPromoteData` callback so bytes
   from the still-running child append to the file via an
   `fs.createWriteStream` opened in `handlePromotedForeground`.
2. **Natural-exit registry settle**: today the registry entry stays
   `'running'` until `task_stop` / session-end `abortAll` fires its
   abort listener. PR-2.5 wires `onPostPromoteSettle` so natural
   child exit transitions the entry to `'completed'` / `'failed'`
   with the right exitCode / signal / error message.

- New exported types: `ShellExecuteOptions`, `ShellPostPromoteHandlers`,
  `ShellPostPromoteSettleInfo`.
- `execute()` options bag now accepts `postPromote?: { onData, onSettle }`.
  Threaded through to both `executeWithPty` and `childProcessFallback`.
- PTY's `performBackgroundPromote` (line ~1159): after disposing
  the foreground data + exit + error listeners, RE-ATTACH minimal
  forwarders that call `postPromote.onData` / `postPromote.onSettle`
  when the caller opted in. Backwards compat: when `postPromote` is
  unset the PR-2 detach-everything contract is preserved (the
  re-attach is gated on each callback being defined).
- `childProcessFallback`'s `performBackgroundPromote` (line ~706):
  same pattern — re-attach `stdout.on('data', ...)`, `stderr.on('data',
  ...)`, `child.once('exit', ...)`, `child.once('error', ...)` when
  the caller opted in. `error` listener routes through `onSettle`
  with `error` populated, so spawn-side errors after the foreground
  errorHandler detached don't crash the daemon via the default
  unhandled `'error'` event.
- Both paths wrap caller callbacks in try/catch so a thrown handler
  doesn't crash the child's data loop / unhandled-rejection the
  service.

- New `PromoteArtifacts` type — slots shared between the foreground
  `execute()` postPromote handlers (which fire on the service side
  as soon as promote happens) and the post-resolve
  `handlePromotedForeground` finalizer (which runs after
  `await resultPromise` returns). The two race; the buffer +
  settle-queue absorb that race so neither chunks nor the eventual
  exit info are lost.
- `executeForeground` wires `postPromote` handlers that route data
  to either `promoteArtifacts.stream` (if open) or
  `promoteArtifacts.buffer` (drained when the stream opens), and
  queue settle info if the wired handler isn't yet installed.
- `handlePromotedForeground` opens `fs.createWriteStream(outputPath,
  { flags: 'w' })`, writes the initial snapshot first, drains the
  buffer, then registers the entry and wires `onSettleWired` with
  the full registry decision table:
    - `error` set → `registry.fail(shellId, error.message, endTime)`
    - `exitCode === 0` → `registry.complete(shellId, 0, endTime)`
    - non-zero exitCode → `registry.fail(shellId, "Exited with code N", endTime)`
    - signal !== null → `registry.fail(shellId, "Terminated by signal N", endTime)`
    - all-null fallback → `registry.fail(shellId, "Exited with unknown status", endTime)`
- Fires queued settle synchronously after wiring so a fast command
  that exits between promote and finalizer doesn't get lost.
- Self-audit catch: closes the output stream on the
  `registry.register` throw path so the FD doesn't leak past the
  orphan-child kill.

- 3 new in `shellExecutionService.test.ts`:
  - `post-promote bytes route to postPromote.onData when callback provided`
  - `postPromote.onSettle fires on natural child exit after promote`
  - `backwards compat: without postPromote, listeners stay fully detached`
- 3 new in `shell.test.ts` under a `foreground → background promote
  PR-2.5` describe block:
  - `post-promote bytes APPEND to bg_xxx.output via write stream`
  - `natural child exit transitions registry entry to "completed"`
  - `non-zero exit / signal / error → "failed" with descriptive message`
- Bulk-replaced 50 prior `{},` (empty 6th-arg shellExecutionConfig)
  with `expect.objectContaining({}),` + added `expect.objectContaining({
  postPromote: expect.any(Object) }),` as the 7th-arg expectation for
  the foreground execute call.
- Updated the existing `registers a bg_xxx entry on result.promoted`
  test to assert on `fs.createWriteStream` + `stream.write` instead
  of the now-removed `fs.writeFileSync` snapshot path.

182/182 shell.test.ts pass + 73/73 shellExecutionService.test.ts pass
+ 111/111 coreToolScheduler.test.ts pass + 60/60 AppContainer.test.tsx
pass; tsc + ESLint clean.

Self-audit: 3 rounds (positive / reverse / cross-file) found one
issue — output stream FD leak on `registry.register` throw — and
fixed it before flagging complete. All flagged edge cases (stream
errors, child-exits-before-wire-up race, task_stop during natural-
exit window, promote-never-happens cleanup, backwards compat
without callbacks) have explicit handling and / or test pinning.

* fix(core): #4102 review wave — 3 Critical + UTF-8 + tests

3 Critical race/correctness issues + 1 multibyte-corruption suggestion
+ 3 test coverage gaps addressed:

**Critical 1 — child_process late-chunk drop (service)**
Settle was fired on 'exit', but stdout/stderr can emit buffered data
between 'exit' and 'close'. Late chunks landed in
`promoteArtifacts.buffer` after shell.ts had already closed the
stream + transitioned the registry → silently dropped → truncated
`bg_xxx.output`. Switched to listening on 'close' which guarantees
all stdio is fully drained. (code, signal) payload is identical to
'exit', just with proper ordering.

**Critical 2 — stream-flush wait before registry transition (shell)**
`stream.end()` is asynchronous; pending writes can still be in the
libuv queue when it returns. The old code transitioned the registry
immediately after `.end()`, so a /tasks consumer could observe a
`completed` entry and read the output file BEFORE the trailing
bytes were on disk. Fixed: wired settle now `stream.once('finish',
...)` BEFORE calling `registry.complete/fail`. `error` event also
short-circuits to the transition so a late ENOSPC doesn't hang the
settle path forever.

**Critical 3 — stream-open-fail buffer leak (shell)**
If `fs.createWriteStream` threw, the catch path set `stream = null`
but the foreground `onData` handler would still take the
`stream === null` branch and push chunks into `promoteArtifacts.buffer`
— unbounded growth under a sustained child whose output file
couldn't be opened. Added a `streamFailed: boolean` latch on
`PromoteArtifacts`. When set, `onData` drops chunks (with a debug
log) instead of buffering. The catch branch sets the latch.

**Suggestion — shared TextDecoder corrupts multibyte UTF-8 (service)**
child_process post-promote used ONE TextDecoder for both stdout AND
stderr. The decoder's continuation-byte state machine assumes one
byte source; interleaved multibyte chunks corrupted. Now uses
separate decoders + flushes both with `decode()` (no `stream: true`)
on settle so trailing bytes surface as their final characters.

**Suggestion — llmContent reflects already-settled status (shell)**
When the queued-settle drain transitions the registry synchronously
(fast-exit race), the model-facing copy was still saying "Status:
running. … task_stop({...})". Updated to branch on
`postPromoteAlreadySettled` / `postPromoteFinalStatus` — when the
process is already gone, the copy says "Status: completed/failed"
and replaces the `task_stop` suggestion with "Process has already
exited; no `task_stop` needed".

**Suggestion — test coverage gaps**
Added: (a) `queued-settle race: onSettle BEFORE
handlePromotedForeground completes` — custom service impl fires
onSettle synchronously before resolving the promote promise, pins
the drain path. (b) child_process post-promote tests for stdout/stderr
forwarding + 'close'-not-'exit' settle + spawn-error settle.

**Self-audit**: Round 1 + reverse audit. Stream.once mock added to
fire 'finish' synchronously so existing tests don't hang on the new
flush wait. 76/76 shellExecutionService.test.ts (+3) + 183/183
shell.test.ts (+1) pass; tsc + ESLint clean.

* fix(core): #4102 review wave-2 — 3 more

C1 (shell.ts:2227): the WriteStream `'error'` event handler only
logged. `fs.createWriteStream` reports common open failures
(ENOENT / EACCES / ENOSPC) asynchronously via that event rather
than throwing. Result: `promoteArtifacts.stream` kept pointing at
the failed stream; `onSettleWired` attached a `.once('finish')`
listener that would never fire → registry stuck on `running`
forever. Latch the failure (null the shared `stream` slot,
set `streamFailed`); `onSettleWired`'s existing `if (!stream)`
branch then transitions the registry immediately.

C2 (shellExecutionService.ts:1468): the promote handoff removes the
foreground `ptyErrorHandler` and only re-attaches data + exit
listeners. A subsequent PTY `error` event had no listener — Node
treats an unhandled `error` from an EventEmitter as a fatal
exception that takes the whole CLI down. Attach a post-promote
forwarder that ignores expected PTY read-exit codes (EIO / EAGAIN,
same filter the foreground handler uses) and routes unexpected
errors through `postPromote.onSettle` with `error` populated.
Single-fire latch shared with `onExit` so settle never fires twice.

C3 (shell.ts:2503): `onSettleWired` waits for the stream's
asynchronous `'finish'` event before flipping
`postPromoteAlreadySettled`, but the model-facing `statusLine` was
built immediately after invoking `onSettleWired` on the queued
settle. A fast-exited promoted command could therefore land
"Status: running" + a `task_stop` instruction in production even
though settle was already observed. Split into two flags:
`postPromoteSettleObserved` (set synchronously when settle is
classified) drives the model copy; the registry transition stays
behind the stream flush.

Tests: +1 PR-2.5 wave-2 PTY error-routing test; +2 shell.ts tests
(stream open async error → registry still transitions; async
`'finish'` after queued-settle drain → llmContent says 'completed'
before registry transition fires).

* fix(core): #4102 review wave-3 — 4 actionable

T2 (shell.ts:2456) — Critical buffer-leak race
`onSettleWired` previously set `promoteArtifacts.stream = null`
BEFORE calling `stream.end()`. Any `postPromote.onData` chunk that
landed between that null assignment and the actual flush completing
saw `stream === null && streamFailed === false` and pushed into
`promoteArtifacts.buffer` — a buffer that has no further drain path
(the foreground finalizer has already returned). Result: chunks
stranded indefinitely; PTY mode in particular hits this because
`onExit` can fire while kernel buffers still hold data. Fix drains
the pre-settle buffer to the stream BEFORE nulling AND latches
`streamFailed = true` so any subsequent chunk drops via the
existing `else if (streamFailed)` arm in `onData` instead of
leaking. Updates the `streamFailed` doc to cover both setters
(open-fail and settle-done) so the dual semantic is explicit.

T3 (shell.ts:2262) — silent chunk-drop in catch path
When `fs.createWriteStream` throws synchronously (rare: ENOENT on
a vanished tmpdir), chunks already in `promoteArtifacts.buffer`
were silently lost with no observability — oncall reading a
truncated `bg_xxx.output` had no way to distinguish "stream open
failed" from "child produced nothing." Logs the dropped chunk
count and empties the buffer.

T5 (shell.ts:2443) — opaque all-null fallback
The "Exited with unknown status" fallback fired the registry to
'failed' without any context about which fields were null. This
branch is meant to be unreachable; hitting it indicates the
service emitted a defective settle info object. Includes the
field values in both the fail message and a warn log so the
oncall engineer can tell this path apart from the other "failed"
branches.

T6 (shellExecutionService.ts:1452) — leaked PTY post-promote listeners
`ptyProcess.onData(...)` returns an `IDisposable` that was being
discarded; same for `onExit`. The `'error'` listener function was
also not captured (no way to `removeListener` it). EventEmitter
holds refs to listener closures, which transitively hold refs to
`onPostData` / `onPostSettle` / the caller's `promoteArtifacts`.
While bounded by the PTY's lifetime, the closures keep the
caller's state pinned for the post-settle delay window. Captures
all three handles into `postPromoteDataDisposable` /
`postPromoteExitDisposable` / `postPromoteErrorListener`, then
releases them via a shared `disposePostPromoteListeners()` call
from `firePostSettle` (idempotent — each slot null-checked and
nulled after disposal).

Tests: +1 service test for IDisposable + error-listener cleanup;
+2 shell.ts tests for buffer drain race and catch-path snapshot
fallback. Existing tests stay green (262 → 265 in the touched
suites; 7819 → 7822 across the core package).

* fix(core/test): drop unused 'registry' in wave-3 T2 test (TS6133)

CI build failed across all platforms with src/tools/shell.test.ts(4395,15): error TS6133. The variable was a leftover from copying the queued-settle test pattern; the wave-3 T2 test inspects writeStreamMock.write call history directly and never reads the registry, so the assignment is dead code. Drop it.

* fix(core): #4102 review wave-4 — 6 actionable

T1 (Critical, shellExecutionService.ts:860 child_process onSettle
exactly-once)
The PTY path used a `firePostSettle` latch but child_process wired
`close` and `error` independently to `onPostSettle`. A spawn-side
error followed by Node's auto-emitted `'close'` would call the
caller's settle TWICE, racing the registry transition. Added the
same single-fire latch on the child_process path.

T2 (Critical, shell.ts:2264 handoff race reorder)
Original order was `write(snapshot) -> drain buffer -> assign stream`.
Synchronous today (no race in current code), but assign-after-drain
leaves a hazard for any future refactor that adds an `await` inside
the drain loop — a chunk arriving in that window would land in
`promoteArtifacts.buffer`, then post-assign chunks would write to
the stream first, producing out-of-order bytes until the settle
drain. Reordered to `write(snapshot) -> assign stream -> drain
buffer`, which closes the hazard regardless of future async
additions.

T3 (Suggestion, shellExecutionService.ts:816 decoder flush gated
on onSettle)
The trailing-multibyte flush ran inside the `child.once('close', ...)`
handler, which was only installed when `onSettle` was set. An
`onData`-only caller (no onSettle) lost trailing continuation
bytes silently. Hoisted flush into `flushPostPromoteDecoders`
called from `firePostSettle`, and made `firePostSettle` available
on the `'close'` path independent of onSettle (T6 install).

T4 (Suggestion, shell.ts:1700 promoted ANSI passthrough)
The regular `executeBackground` path strips ANSI before writing to
`bg_xxx.output`; the promoted-foreground onData path appended raw
chunks. Reading `bg_xxx.output` after Ctrl+B showed plain text up
to the snapshot then raw `\x1b[31m` / cursor-move / clear-screen
sequences for the post-promote tail — unreadable. Apply
`stripAnsi(rawChunk)` before write/buffer, matching the
executeBackground contract.

T5 (Suggestion, shellExecutionService.ts:786 UTF-8 hardcoded)
The post-promote child_process decoders were hard-coded to
`new TextDecoder('utf-8')`, but the foreground decoder runs
encoding detection via `getCachedEncodingForBuffer`. On a non-UTF-8
child (e.g. GBK on a Chinese Windows shell), the snapshot decoded
correctly but the post-promote tail was mojibake. Capture the
foreground decoder's `.encoding` property and reuse it for
post-promote (with utf-8 fallback if foreground hadn't seen any
bytes yet, and a try/catch around `new TextDecoder` for the rare
unsupported-encoding case).

T6 (Suggestion, shellExecutionService.ts:1540 `error` listener
gated on onSettle)
The post-promote `error` listener was attached only when `onSettle`
was set. An `onData`-only caller still had the foreground
errorHandler detached; a post-promote spawn error would then crash
the CLI via Node's unhandled-error default. Hoisted the close +
error listeners into `if (postPromote)` so any caller opting into
post-promote gets crash protection; if `onSettle` is absent the
listeners log + drop instead of routing.

T7 (Suggestion, shellExecutionService.ts:791 onSettle-only
pipe-block deadlock)
Same root cause as T6: when only `onSettle` is set, the foreground
`stdout`/`stderr` 'data' listeners are detached and no post-promote
listener replaces them. The Readables stay paused, the OS pipe
buffer fills (~64KB on Linux), the child blocks on `stdout.write`,
'close' never fires, onSettle never fires. Added `child.stdout?.resume()`
and `child.stderr?.resume()` in the no-onData branch so the child
can drain its pipes and reach exit.

T8 (Suggestion, shell.ts:2614 dead inspectLine ternary)
`inspectLine`'s ternary returned the same string on both sides —
copy-paste leftover from when the other two adjacent ternaries
(statusLine / stopLine) were correctly varied. Collapsed to a
single string assignment.

Tests: +5 regression tests (4 child_process: T1 double-fire latch,
T3 onData-only flush, T6 onData-only error survives, T7 onSettle-
only resume; +1 shell.ts: T4 ANSI strip).

265 -> 270 in the touched suites; 7822 -> 7827 across the core
package; full suite green.

* fix(core/test): use ShellOutputEvent type in wave-4 onData callbacks (TS2345)

CI lint failed on the wave-4 (T3 / T6) tests with TS2345: pushing
ShellOutputEvent into Array<{type:string;chunk:unknown}> narrows
incompatibly. Switch to ShellOutputEvent[] (matches earlier helpers
at lines 758/966) and discriminate the union via .type === 'data'
when reading .chunk so the narrowed multibyte assertion still
type-checks.

* fix(core): address PR #4102 review — PTY error guard, flush timeout, diagnostic marker, failed-settle test

- Move PTY post-promote error listener from `if (postPromote?.onSettle)` to
  `if (postPromote)` to match child_process path and prevent unhandled error
  crashes for onData-only callers
- Add 10s flush timeout in onSettleWired so stalled streams don't leave
  registry entries stuck on 'running' forever
- Append diagnostic marker to output file on stream error so truncation
  is visible without debug logging
- Add queued-settle test with exitCode:1 asserting 'Status: failed.' in
  llmContent

* fix(core): address PR #4102 review — align PTY/child_process guards, add flush timeout, diagnostic marker, and tests

- Widen PTY post-promote onExit + error listener guard from
  `if (postPromote?.onSettle)` to `if (postPromote)` to match
  child_process path — prevents unhandled error crash and listener
  leak for onData-only callers
- Add 10s flush timeout in onSettleWired so stalled streams don't
  leave registry entries stuck on 'running' indefinitely
- Append diagnostic marker to output file on stream error so
  truncation is visible without debug logging
- Remove model name references from code comments
- Add tests: PTY onData-only error/exit, flush timeout fallback,
  appendFileSync diagnostic marker, queued-settle with failed exit code

* fix(core): address PR #4102 review round 2 — listener cleanup, rename, constant hoist

- Fix expect.objectContaining({}) misused as runtime arg in 2 execute() call sites
- Add child_process post-promote stdout/stderr listener cleanup in firePostSettle
- Rename streamFailed → streamClosed to reflect its overloaded semantics
- Hoist FLUSH_TIMEOUT_MS to module-level PROMOTE_FLUSH_TIMEOUT_MS constant
- Fix dangling FLUSH_TIMEOUT_MS reference (was undefined at runtime)
- Add Windows note to streams pause/resume comment
- Document PTY onData dispose-before-settle as known limitation
2026-05-17 17:57:08 +08:00
ChiGao
c25e22b575
feat(serve): add session-scoped permission route (#4232)
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-05-17 17:48:30 +08:00
ChiGao
4d9cbe49c0
feat(serve): add daemon-stamped client identity (#4231)
* feat(serve): add daemon-stamped client identity

* fix(serve): harden daemon client identity handling

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-05-17 16:19:30 +08:00
kkhomej33-netizen
605e5eea16
fix(cli): include skill base dir in slash commands (#4224) 2026-05-17 15:52:29 +08:00
kkhomej33-netizen
d6914bdfd6
fix(core): align shell tool description with configured shell (#4170) 2026-05-17 15:36:59 +08:00
ChiGao
b90a2c91c9
feat(sdk): harden daemon session client (#4225)
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-05-17 15:05:37 +08:00
jinye
2453b82add
[codex] Add daemon session load/resume (#4222)
* feat(serve): add daemon session load resume

Adds HTTP and SDK support for restoring persisted daemon sessions through load/resume routes, including replay buffering for load and guarded concurrent restore handling.

Refs #4175

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): address review feedback on daemon session load/resume

- Gate `defaultEntry` claim in `restoreSession` on
  `defaultSessionScope === 'single'`, mirroring `doSpawn`. Without the
  gate, a restored session silently became the omitted-scope attach
  target on `'thread'`-default daemons.
- Rename advertised capability `session_resume` to
  `unstable_session_resume` to match the underlying ACP method
  (`connection.unstable_resumeSession`). `session_load` stays stable.
- Seed `lastEventId: 0` in `DaemonSessionClient.resume`, symmetric with
  `load`. The agent's `unstable_resumeSession` schedules an
  `available_commands_update` via `setTimeout(0)`; without the seed the
  SDK consumer would miss that frame.
- Add HTTP-level test for the `RestoreInProgressError → 409` envelope.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* docs(serve): adopt review feedback comments on session load/resume

- Cross-reference the `POST /session` disconnect-cleanup rationale
  from `restoreSessionHandler`'s `!res.writable` branch so future
  maintainers find the BQ9tV race + tanzhenxin attach-rollback
  context without grep.
- Document `DaemonSessionState.{models, modes, configOptions}` in
  the SDK so callers can narrow to the ACP `SessionModelState` /
  `SessionModeState` / `SessionConfigOption` shapes.
- Add JSDoc on `DaemonClient.restoreSession` explaining why
  `loadSession` and `resumeSession` collapse into one transport.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(serve): preserve restore state and harden in-flight restore races

Address the four Critical findings from PR #4222 review (wenshao):

- Coalesced restore waiters now observe the same ACP state the
  original restore caller did. `state: {}` in `restoreSession`'s
  coalesce branch was clobbering the spread `restored.state`, so
  concurrent callers got different payloads based purely on timing.
  Cache the load/resume response on `SessionEntry.restoreState` and
  return it from both the existing-byId early return and the
  coalesce branch.
- Drop the `defaultEntry` promotion on restore. Explicit
  `session/load` / `session/resume` is "give me THIS id"; it must
  not become the implicit attach target for subsequent omitted-id
  `POST /session` callers under `single` scope. Reserves
  `defaultEntry` for sessions created through `doSpawn` only.
- Reserve coalesced attaches synchronously via
  `InFlightRestore.coalesceState.count` so the spawn owner's
  `requireZeroAttaches` disconnect-reaper sees a non-zero
  `attachCount` on the freshly registered entry and skips the
  kill. Without this, B's `attachCount++` happened after `await
  inFlight.promise`, leaving a window where A's HTTP-disconnect
  cleanup could reap the session out from under B.
- Include `pendingRestoreIds` in the `killSession` channel-teardown
  decision. The last live session leaving while a restore is
  in-flight on the same channel would otherwise SIGTERM the
  channel mid-restore.
- Bump `RestoreInProgressError`'s `Retry-After` from 1s to 5s
  (matches `SessionLimitExceededError`); under the default
  `initTimeoutMs` of 10s, 1s pushed clients into tight loops.

Tests: new bridge cases covering state propagation through
coalesce, the spawn-owner-disconnect race, the
pendingRestoreIds-aware channel teardown, and the no-promote-
on-restore invariant. Existing "attaches twice" test rewritten
to assert the cached restore state propagates.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* test(serve): cover acpAgent load/resume + restore route error mappings

Close the test-coverage gaps wenshao called out in PR #4222 review:

- acpAgent.test.ts gains a `QwenAgent loadSession /
  unstable_resumeSession` block that locks down the new contract
  end-to-end at the agent layer:
  * `loadSession` missing persisted session → throws
    `RequestError.resourceNotFound("session:<id>")` (code -32002
    + `data.uri`).
  * `loadSession` existing session → returns LoadSessionResponse
    AND triggers `session.replayHistory(messages)` so SSE
    subscribers see the persisted turns.
  * `unstable_resumeSession` missing session → same
    resourceNotFound contract.
  * `unstable_resumeSession` existing session → returns the
    response WITHOUT replaying history (resume restores model
    context internally; UI replay is intentionally suppressed).
  Required extending the mocked `RequestError` with
  `resourceNotFound`, and mocking `SessionService` per case.
- server.test.ts adds the missing restore-route wire mappings:
  `WorkspaceMismatchError → 400 workspace_mismatch` and
  `SessionLimitExceededError → 503 + Retry-After: 5`. Combined with
  the existing 409 case for `RestoreInProgressError`, the route
  layer now has full structured-error coverage.
- Updated the 409 test's `Retry-After` expectation from `1` to `5`
  to match the bumped retry hint.

Disconnect-cleanup tests for the restore route were intentionally
not added — the cleanup branch is line-for-line identical to
`POST /session`'s handler (which itself ships without route-level
disconnect tests due to flaky supertest + Node http close-event
timing).

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* docs(serve): document daemon session load/resume routes

Sync the docs to the routes that landed via PR #4222:

- `docs/developers/qwen-serve-protocol.md`:
  * Add `session_load` and `unstable_session_resume` to the
    advertised features list, with a note on the `unstable_`
    prefix mirroring ACP's underlying method name.
  * Document `POST /session/:id/load` and `POST /session/:id/resume`
    — request body, response shape (including the cached `state`
    field that late attachers observe), and the full error
    envelope: 404 unknown id, 400 workspace_mismatch, 503
    session_limit_exceeded (counts in-flight restores), 409
    restore_in_progress (cross-action race).
  * Note the SSE replay ring bound (4000 frames default) and the
    "subscribe immediately after load" guidance for long histories.
- `docs/users/qwen-serve.md`:
  * Add a "Loading and resuming a persisted session" section with
    the SDK example (`DaemonSessionClient.load` /
    `DaemonSessionClient.resume`) and the load-vs-resume
    decision table.
  * Update the durability model — sessions are still ephemeral
    across daemon restarts in Stage 1, but persisted sessions on
    disk can now be reloaded.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(test): use _meta payload to satisfy ACP SessionConfigOption types

The two new state-propagation tests in `httpAcpBridge.test.ts` used
`{ id, name, value }` as a `SessionConfigOption`, but ACP's actual
`SessionConfigSelect` shape requires `currentValue` + `options`. vitest
runs through esbuild and skips strict typechecking, so the local
`vitest run` passed; CI's `tsc --build` (run during `npm run prepare`)
caught it.

Switch the fixture to `_meta: { tag: '...' }` instead — `_meta` is
typed as `Record<string, unknown> | null` on the ACP response shapes,
so any payload survives. The assertions only need the bridge to
forward the state object intact, which `_meta` proves equally well
without committing the test to the full SessionConfigOption union.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(serve): symmetric restore coalesce guard + transportClosed leak + defensive cleanup

Address the two new Critical findings + the test/cosmetic gaps from
wenshao's second review pass on PR #4222 (`a3f38da3a`):

- **[Critical] Symmetric coalesce guard.** The previous guard only
  rejected `load`-on-`resume`; `resume` arriving while a `load` was
  in flight silently coalesced and inherited the load's history-
  replay frames over SSE — directly violating resume's "no UI
  replay" contract (made worse by `DaemonSessionClient.resume()`
  seeding `lastEventId: 0`). Tighten the guard to
  `action !== inFlight.action` so any cross-action race throws
  `RestoreInProgressError`. Same-action coalescing is unaffected.

- **[Critical] `transportClosed` dangling rejection.** When
  `withTimeout` wins the `Promise.race` against `channel.exited`,
  the `.then(throw)` chain on `channel.exited` stays pending. A
  later channel exit (next session boundary, daemon shutdown, agent
  crash) fires the `throw` with no observer attached — Node 22 logs
  `unhandledRejection`, and `--unhandled-rejections=throw`
  deployments crash the daemon. Add `transportClosed.catch(() => {})`
  to suppress the dangling rejection after the race settles.

- **`isAcpSessionResourceNotFound` exact-match fallback.** The
  message-fallback path used `message.includes(expectedUri)`, which
  would falsely match a sessionId of `"a"` against a message
  containing `"session:abc"`. Tighten to exact equality on the
  canonical `Resource not found: <uri>` form. The primary
  `data.uri` path remains the dominant code path.

- **`loadSession` mcpServers default symmetry.** `loadSession` now
  uses `params.mcpServers ?? []` to mirror `unstable_resumeSession`.
  Defends against a future ACP schema loosening that makes
  `LoadSessionRequest.mcpServers` optional — without the
  null-coalesce, `newSessionConfig` would `TypeError` on iteration.

Tests added:
- `httpAcpBridge.test.ts`: `resume`-on-`load` rejection (mirror of
  the existing `load`-on-`resume` test); regression for the
  dangling `unhandledRejection` (resolves `channel.exited` after
  the restore promise has already settled and asserts no
  `unhandledRejection` event); shutdown-awaits-restore via
  `Promise.race`-based ordering.
- `server.test.ts`: 400 for non-string and over-length `cwd` on
  the restore routes (mirroring the equivalent `POST /session`
  cases for `parseOptionalWorkspaceCwd`).
- `acpAgent.test.ts`: load with `getResumedSessionData()` returning
  `undefined` — distinct code path that does NOT call
  `replayHistory`.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-17 12:58:47 +08:00
qqqys
2e773b0e60
[codex] Allow custom output directory for /export (#4193)
* feat(cli): support export output directories

* fix(cli): address export review feedback

* test(cli): cover JSON export directory handling

* fix(cli): constrain export output directories

* test(cli): cover export edge cases

* fix(cli): address export directory review feedback

* fix(cli): revalidate export directory before write

* fix(cli): validate export directory before mkdir

* fix(cli): harden export target writes

* fix(cli): refine export failure handling

* fix(cli): clarify export directory mode

* fix(cli): include export path context in errors

* fix(cli): add export debug logging

* fix(cli): make export tests path portable

* fix(cli): refine export validation diagnostics

* test(cli): cover export validation failures
2026-05-17 12:33:02 +08:00
ChiGao
80f1e266ba
feat(protocol): add typed daemon event schema v1 (#4217)
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-05-17 12:31:16 +08:00
Shaojin Wen
ef29700bce
fix(ui): trim background task results and show newest first (#4094) (#4125)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(ui): trim background task results and show newest first (#4094)

Two related improvements to the background task pill and dialog:

1. Trim outdated terminal task results.
   `BackgroundTaskRegistry` and `BackgroundShellRegistry` now cap
   retained terminal entries at 32 each (mirroring `MonitorRegistry`'s
   existing `MAX_RETAINED_TERMINAL_MONITORS` pattern). Running, paused,
   and cancelled-but-not-yet-notified entries are never evicted —
   pruning a not-yet-notified entry would break the SDK contract that
   every `register` pairs with exactly one terminal `task-notification`.

2. Show newest tasks at the top of the dialog.
   `useBackgroundTaskView` now sorts entries by `startTime` descending
   so the dialog opens with the cursor on the most recently launched
   task. `LiveAgentPanel` reverses internally back to ASC for its own
   visual layout (newest row sits closest to the composer).

* perf(shell-registry): batch abortAll prune + statusChange into one pass

abortAll() previously delegated to cancel() per entry, so each running
shell triggered its own pruneTerminalEntries() and statusChange wakeup.
On shutdown / `/clear` with N running shells the only subscriber
(useBackgroundTaskView) re-pulled getAll() N times for what is logically
a single batch transition.

Settle each entry inline via the new private settleAsCancelled() helper,
then fire prune + statusChange exactly once after the loop. The split
keeps the running-status guard at the public-API boundary so callers
can't accidentally re-settle a terminal entry.

* fix(ui): two-bucket sort so running tasks outrank fresh terminals

The earlier startTime DESC sort surfaced the newest LAUNCH but let an
older long-running / paused entry get pushed below a batch of newer
terminal entries — the user opening the dialog to check on the running
work would find it buried under stale completed rows.

Split the merge into two buckets:
  - active (running + paused): sorted by startTime DESC so the most
    recent launch sits at the very top of the dialog.
  - terminal (completed / failed / cancelled): sorted by endTime DESC
    so the most recently FINISHED entry leads the terminal section
    (matches "what changed while I wasn't looking" intuition; a long
    task that just settled outranks an old quick task that finished
    hours ago).

Pin the new behavior with two tests covering active-above-terminal
and the endTime-vs-startTime distinction inside the terminal bucket.

* fix: add missing outputFile and isBackgrounded to retention cap tests

The merge brought in required fields on AgentTaskRegistration that the
retention-cap test helpers were not supplying.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-17 09:13:24 +08:00
qqqys
07165a095c
Add stop hook blocking cap (#4208)
* feat(core): add stop hook blocking cap

* fix(core): tighten stop hook cap behavior

* fix(cli): show goal judge details

* fix(core): bound stop hook blocking cap

* fix(core): surface subagent stop cap warnings

* fix(core): clean up stop hook cap loop

* test(core): cover stop hook cap integrations

* test(core): strengthen stop hook cap coverage
2026-05-17 06:52:56 +08:00
jinye
0eed884c0b
fix(rewind): restore upstream TOCTOU ordering + heal sticky failed marker (#4216)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(rewind): restore upstream TOCTOU ordering + heal sticky failed marker

Two related bugs that landed in #4064:

1. **trackEdit widened the pre-write TOCTOU window**. PR #4064 inserted
   `await trackEdit(filePath)` between `checkPriorRead` and
   `writeTextFile` in both `edit.ts` and `write-file.ts`. trackEdit does
   `stat` + `copyFile` and on large files can take hundreds of
   milliseconds. The pre-existing comment above `checkPriorRead`
   acknowledged a stat-then-write race window of "two adjacent
   syscalls"; the new ordering widened that to "freshness check →
   potentially-multi-second backup → write", so an external mutation
   landing during the backup would no longer be detected before the
   write clobbered it.

   The fix is the ordering upstream `claude-code/src/tools/FileEditTool`
   uses, with its own explicit comment on the equivalent block:

   > These awaits must stay OUTSIDE the critical section below — a
   > yield between the staleness check and writeTextContent lets
   > concurrent edits interleave.

   Moved `trackEdit` to BEFORE `checkPriorRead` in both tools. Backups
   are idempotent (deterministic `{hash}@v{version}` filename), so
   running the backup on a path that ends up rejected just leaves an
   unused-but-correct backup of the pre-edit state, not corrupt state.

2. **The `failed` marker added in d59838338 stayed sticky in `trackEdit`**.
   When `makeSnapshot` recorded `failed: true` for a file (transient
   I/O error during per-file backup), the next `trackEdit` for that
   file skipped because the early guard only checked existence, not
   the failed bit. The pre-edit state never got re-captured even
   after the underlying I/O recovered, leaving the snapshot marked
   filesFailed forever for that path.

   The guard now skips only on confirmed (non-failed) entries:
   `if (existing && !existing.failed) return;`. The re-check guard
   at the end is updated symmetrically so the fresh backup actually
   replaces the failed entry (instead of being silently dropped by
   the "already present" branch).

Tests:

- `edit.test.ts`: pin the TOCTOU ordering via spy on `fileReadCache.check`
  and `mockFileHistoryService.trackEdit`. Verifies `trackEdit` is called
  before the last `cache.check` (the pre-write freshness guard). Same
  pattern in `write-file.test.ts`. Both tests fail on the old ordering
  (verified by stashing the production change).
- `fileHistoryService.test.ts`: `heals a failed entry on the next
  trackEdit attempt` — sabotage storage so makeSnapshot records
  failed, restore storage, run trackEdit, assert the entry is now
  non-failed with a real backupFileName.

* test(rewind): switch TOCTOU tests to behavioral assertions + harden heal test

Address review feedback from code-reviewer agent:

- The TOCTOU ordering tests previously asserted ordering via
  `callOrder.lastIndexOf('cache.check')`, which would silently degrade
  to a tautology if a future refactor added any `cache.check` call
  AFTER the pre-write check (e.g. the deferred atomic-write + content-
  hash post-check explicitly mentioned in the comment above the
  freshness guard).

  Switch to a behavioral assertion that directly tests the invariant:
  install a `trackEdit` mock that mutates the file on disk (bumps
  mtime) before returning. The pre-write `checkPriorRead` must catch
  the in-trackEdit mutation — that only happens if `trackEdit` runs
  BEFORE the pre-write check. The broken ordering would run pre-write
  check first (passing on pre-mutation stats), then trackEdit (which
  mutates), then write (which clobbers the external mutation).

  Asserting on `result.error?.type === FILE_CHANGED_SINCE_READ`
  exercises the actual race protection the fix exists to preserve,
  and survives any future refactor that keeps the invariant intact
  regardless of how many `cache.check` calls happen on the way.

  Verified by reverting just the production change to HEAD~1: both
  behavioral tests fail under the broken ordering (file is silently
  overwritten, no FILE_CHANGED_SINCE_READ error), and pass under the
  fix.

- The heal test (`heals a failed entry on the next trackEdit attempt`)
  previously asserted `backupFileName !== null` but not that the file
  on disk at that name actually contained the current content. Add a
  `readFile + expect(...).toBe('p2-content')` assertion so a regression
  that wrongly reuses `previous.backupFileName` (pointing to
  `p1-content`) fails loudly instead of silently passing.
2026-05-17 02:01:58 +08:00
易良
ba77ddd81b
fix(lsp): expose status and startup diagnostics (#3649)
* feat(lsp): add /lsp slash command to show server status

Implements the /lsp command that displays the status of all configured
LSP servers. Previously this was documented in the FAQ but never
implemented, leaving users with no way to check if their language
servers started successfully.

Changes:
- Add LspServerStatusInfo interface to lsp/types.ts
- Add getServerStatus() to LspClient and NativeLspClient
- Expose getServerHandles() from NativeLspService
- Create lspCommand.ts with status table output
- Register /lsp in BuiltinCommandLoader (only when LSP is enabled)

The command shows: server name, command, languages, and status
(NOT_STARTED / IN_PROGRESS / READY / FAILED + error message).

* fix(lsp): expose status and startup diagnostics

* fix(lsp): harden status command diagnostics

* fix(lsp): add stderr error listener and harden initialization error handling

- Add stderr 'error' event listener in LspConnectionFactory to prevent
  unhandled stream errors from crashing the process
- Wrap setLspInitializationError calls in try-catch in config.ts to guard
  against post-initialization state changes that would throw
2026-05-17 01:42:28 +08:00
ChiGao
57de269f45
feat(sdk): add DaemonSessionClient skeleton (#4201)
* feat(sdk): add daemon session client

* fix(sdk): harden daemon session event replay

* fix(sdk): replay attach-time daemon events

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-05-17 01:01:12 +08:00
jinye
54fd5c50f0
feat(telemetry): add detailed sensitive span attributes (#4097)
Layer detailed content attributes onto the existing hierarchical spans
(qwen-code.interaction / qwen-code.llm_request / qwen-code.tool) gated
by includeSensitiveSpanAttributes:

- Interaction span: user prompt (new_context)
- LLM request span: system prompt + hash + preview + length (full text
  deduped per session via SHA-256), tool schemas (per-tool tool_schema
  events, also hash-deduped), model output
- Tool span: tool input, tool result on every exit path (success +
  pre-hook block + post-hook stop + tool error + try-block cancel +
  catch-block cancel + execution exception)

All large content truncated at 60KB with *_truncated and
*_original_length metadata. Heavy serialization (safeJsonStringify on
tool I/O, partToString on user prompt) is guarded by the sensitive
flag at the call site so it doesn't run when telemetry is off.

Also adds:
- getActiveInteractionSpan() helper for client.ts to attach prompt
  attributes to the interaction span.
- Updated config schema description and docs (telemetry.md +
  settings.md) to reflect expanded scope and add security/cost notes.
- 28 unit tests for detailed-span-attributes, 4 tests for
  getActiveInteractionSpan, integration mocks updated.
2026-05-17 00:36:48 +08:00
qqqys
daaa85e98e
feat(cli): add fork-session resume flag (#4159)
* feat(cli): add fork-session resume flag

* fix(cli): address fork-session review feedback

* fix(cli): handle fork session copy failures

* fix(cli): guard sandbox session handoff flag
2026-05-17 00:27:52 +08:00
Shaojin Wen
b9590283c0
fix(cli): pass rewind selector test props (#4211)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-16 23:57:50 +08:00
jinye
878f35fc4f
feat(serve): per-request sessionScope override on POST /session (#4175 Wave 2 PR 5) (#4209)
* feat(serve): per-request sessionScope override on POST /session

Resolves the FIXME at httpAcpBridge.ts:BridgeOptions.sessionScope from
#3803 — clients can now override the daemon-wide sessionScope per
request instead of being stuck with whatever boot-time value the
operator picked. A VSCode window that wants strict isolation can ask
for `'thread'` against a default-`'single'` daemon, and vice versa.

Wire change:
- POST /session body accepts optional `sessionScope: 'single' | 'thread'`
- Per-request value wins; daemon-wide default remains the fallback when
  the field is omitted (bit-for-bit backward compat for every existing
  caller)
- Invalid values yield 400 `{ code: 'invalid_session_scope' }`
- New capability tag `session_scope_override` advertised on
  /capabilities.features for negotiation

Bridge changes:
- BridgeSpawnRequest gains optional `sessionScope`
- spawnOrAttach validates the per-request value and resolves
  effectiveScope = req.sessionScope ?? defaultSessionScope
- doSpawn now takes effectiveScope and only stamps `defaultEntry`
  (the single-scope attach slot) when the spawn is single-scope —
  fixes a mixed-scope leak where a thread-first call would let a
  later omitted-scope call attach to the supposedly-isolated session

SDK:
- CreateSessionRequest gains optional `sessionScope`
- DaemonClient.createOrAttachSession conditionally spreads it into the
  JSON body so omitted callers send the same wire shape as before

Tests:
- 4 new bridge tests (override single→thread, override thread→single,
  mixed-scope leak regression, invalid-value rejection)
- 3 new server tests (valid passthrough, invalid 400, omitted backward
  compat)
- 2 new SDK tests (forwards/omits sessionScope on the wire)
- EXPECTED_STAGE1_FEATURES updated for the new capability tag

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(serve): address Wave 2 PR 5 review findings

Three independent review passes found three real issues:

1. Bridge `TypeError` on invalid `sessionScope` collapsed to opaque 500
   in `sendBridgeError` instead of the typed `400 invalid_session_scope`
   the route layer guarantees. Direct embed / test / future entry-point
   callers bypassing the route would see a generic 500 with stack noise
   on stderr — disagreeing with the route contract.

   Fix: add `InvalidSessionScopeError` class (alongside
   `SessionNotFoundError` / `WorkspaceMismatchError` /
   `SessionLimitExceededError`); the `spawnOrAttach` validator now
   throws it, and `sendBridgeError` translates to the same
   `{ error, code: 'invalid_session_scope' }` shape.

2. SDK `DaemonClient.createOrAttachSession` used a truthy check
   (`req.sessionScope ? ...`) for the conditional spread, silently
   erasing falsy-but-defined values (`''`, `null`, `0`) on the wire.
   A buggy caller would never see the daemon's 400 — it'd inherit the
   daemon-wide default while believing it requested a specific scope.
   Fix: use `!== undefined` (matching the bridge's own validation
   shape). Same fix to the server-side spread for consistency.

3. JSDoc and docs referenced `serve --sessionScope` as if it were a
   shipping CLI flag. It isn't — `ServeOptions` has no field, neither
   `runQwenServe` nor `serve.ts` plumbs one, and the production daemon
   default is hardcoded to `'single'`. Strike the references; note
   that #4175 may add the flag in a follow-up.

Test coverage expanded:
- Cap-bypass guard: per-request `'thread'` overrides cannot bypass
  `maxSessions` on a daemon-default-`'single'` deployment. Without
  this, a future refactor that gated the cap on `defaultSessionScope`
  instead of `effectiveScope` would silently let `'thread'` overrides
  amplify past the limit — the exact N-amplification cliff #3803 was
  about.
- Symmetric mixed-scope leak: daemon-default-`'thread'` +
  single-first-call followed by omitted-scope-second-call must produce
  distinct sessions. Mirrors the existing daemon-default-`'single'` +
  thread-first leak regression.
- Concurrent mixed-scope coalescing: simultaneous single + thread
  `spawnOrAttach` against the same workspace under slow `initialize`
  must not collide on `inFlightSpawns` (tracker keys differ by scope).
- Updated invalid-scope rejection test to assert
  `InvalidSessionScopeError` instance + carried `sessionScope` field.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
2026-05-16 23:54:20 +08:00
易良
2c200a3d0a
fix(core): add heap-pressure auto-compaction safety net (#4186)
* fix(core): add heap pressure compaction safety net

Link: #4185

* fix(core): keep heap pressure compaction active

Let heap-pressure bypass also bypass the failed-attempt latch and cover the interaction with a regression test.

Link: #4185

* test(core): cover raw next-speaker history lookup

Verify next-speaker checks use the raw last history entry for function responses while the LLM prompt still uses curated history.

Link: #4185

* fix(core): avoid latching heap-pressure compression failures

* fix(core): back off failed heap-pressure compaction

* fix(core): back off heap-pressure no-op compaction
2026-05-16 23:37:05 +08:00
Dragon
8f54ae9c0f
feat(cli): add built-in status line presets with interactive dialog (#4120)
* chore(skills): add codex reproduce workflows

* feat(cli): add built-in status line presets with interactive dialog

Replace the shell-command-only status line with a preset system that
renders structured session info (model, context usage, git branch,
token counts, etc.) without external commands. Users can configure
which items to display via a new interactive dialog accessible through
/statusline or the settings UI.

- Add statusLinePresets module with 16 built-in item types
- Add StatusLineDialog component with search, multi-select, and preview
- Update /statusline command to open the preset dialog
- Extend settings schema to support { type: "preset", items: [...] }
- Enhance MultiSelect with separator items, active marker, and
  customizable checked text
- Update Footer to support theme-colored preset output

* fix(cli): refresh status line preset after saving

* chore: remove codex reproduce skills

* fix(cli): address status line preset review feedback
2026-05-16 23:22:11 +08:00
dreamWB
966b040359
feat(cli): readline Ctrl+P/N for history and selection navigation (#4082)
* feat(cli): readline Ctrl+P/N for history and selection navigation

Adds GNU-readline-style Ctrl+P (previous) and Ctrl+N (next) shortcuts
to the qwen-code TUI so users coming from bash/zsh, Emacs, or Claude
Code feel at home. The change has three orthogonal behavior groups:

1. Input prompt, history-versus-line-motion two-step edge

   Ctrl+P / Ctrl+N and the arrow keys behave identically and apply a
   two-step edge transition that matches GNU readline and Claude Code:
   inside a multi-line buffer they move the cursor between visual
   rows; on the top row with the cursor away from column 0 the first
   Up press snaps the cursor to column 0 without changing history, and
   only the second press walks one entry back. The mirror rule holds
   for Down at the last row (snap to end of line, then advance). After
   navigateUp the buffer is parked at offset 0 (the "start of older
   entry" landing position); after navigateDown setText's default
   end-of-text positioning keeps the cursor at the end. The same
   two-step rule applies to single-line buffers so the
   reverse-direction case the issue called out works: pressing Ctrl+N
   immediately after Ctrl+P loaded a single-line older entry (cursor
   at col 0) first snaps the cursor to end-of-line, and only the next
   Ctrl+N moves forward through the history. Bare k/j inside the
   input prompt remain ordinary typed letters — the vim aliases are
   selection-list shortcuts, not text-editing ones.

2. Selection lists: arrows, k/j, and Ctrl+P/N are interchangeable

   A new pair of Command bindings, SELECTION_UP and SELECTION_DOWN, is
   wired into the shared useSelectionList hook and every dialog that
   used to hand-roll an "up/down arrow only" or "up/k arrow + vim
   only" navigation check. Covered surfaces: the main selection-list
   hook itself, the MCP / extensions / agents / hooks / background-
   tasks / rewind / plugin-choice / ask-user-question dialogs, the
   memory dialog (both its file list and the auto-memory and
   auto-cleanup toggle panel above the list), the settings dialog
   list (with the in-place value editor's "block other keys while
   editing" guard preserved), and the manage-models dialog's top
   tabs row. The auth-provider wizard's Advanced Config focus rows
   and the resume-session picker's cross-mode arrows are extended
   with the readline Ctrl+P / Ctrl+N synonyms while keeping their
   existing arrow-key and (for the session picker) vim k/j semantics
   intact.

3. Selection surfaces that wrap an active text input

   AskUserQuestionDialog's "Other / type a custom answer" field,
   manage-models' search input, the resume-session picker's search
   field, and the auth-wizard's Context-window number input all
   coexist with the selection list on the same screen. In those
   surfaces typing k or j has to land in the text buffer, not scroll
   the surrounding list. The fix is to scope the input-aware handler
   to unambiguous non-letter shortcuts only — arrow keys plus
   readline-style Ctrl+P / Ctrl+N escape the text field, while bare
   letters (including k / j / p / n) are delivered to the active
   input. The keyBinding-level fix that backs this is the
   `{ key: 'k', ctrl: false }` / `{ key: 'j', ctrl: false }` clauses
   on SELECTION_UP / SELECTION_DOWN, which prevent Ctrl+K from
   accidentally matching SELECTION_UP and thereby firing both the
   list-up handler and the KILL_LINE_RIGHT handler in the same
   keystroke (the P0 finding the quality-gate review surfaced).
   Focus-traversal tokens (the agent tab bar and the background-task
   pill) and chord shortcuts (Ctrl+Shift+Up/Down for embedded-shell
   history) are deliberately left untouched because their existing
   "any printable letter yields focus back to the composer" UX would
   break under the new vim-style letter bindings, and the Help
   viewer's scroll is a viewer rather than a selection list and is
   out of this PR's scope.

Documentation: docs/users/reference/keyboard-shortcuts.md is updated
so the Ctrl+P / Ctrl+N entries describe the two-step edge rule and
the radio-button-select table mentions the new k/j and Ctrl+P/N
aliases. Per-dialog on-screen hints (which still read "↑↓ to
navigate") are intentionally not touched so the i18n string surface
stays unchanged; the global reference doc is the authoritative source
for the new shortcuts.

Tests:
 - packages/cli/src/ui/keyMatchers.test.ts adds positive cases
   covering ↑ / ↓ / bare k / bare j / Ctrl+P / Ctrl+N matching
   SELECTION_UP / SELECTION_DOWN and negative cases asserting that
   Ctrl+K and Ctrl+J do NOT match (the conflict guard).
 - packages/cli/src/ui/components/InputPrompt.test.tsx adds a
   "two-step edge transition for history navigation" describe block
   with four cases: a mid-line Ctrl+P snaps to col 0 without invoking
   navigateUp; an at-col-0 Ctrl+P does invoke navigateUp and then
   parks the cursor via moveToOffset(0); a not-at-end Ctrl+N snaps to
   end-of-line without invoking navigateDown; and arrow Up obeys the
   same rule as Ctrl+P for keyboard-parity. The test file's mock
   buffer's setText was also corrected to mirror the real buffer's
   "cursor lands at the end of the new text" semantic so the cursor
   field is internally consistent during keypress assertions; the
   small InputPrompt render-frame snapshot in the same file's
   __snapshots__/ directory was regenerated to reflect the now-
   accurate cursor render position. Three pre-existing arrow-key
   navigation tests were updated to pre-position the mock cursor at
   the relevant edge before pressing the arrow, because the new
   two-step rule means the first arrow press at a non-edge position
   is a cursor snap, not a history step. Multi-line cursor-between-
   rows movement is covered indirectly by the keyBinding-level
   matcher tests plus the end-to-end manual demo plan.

The work landed in three rounds against the planner's gate: round 1
added the unified SELECTION_UP / SELECTION_DOWN Command binding and
the cursor-first dispatch in the input prompt; round 2 picked up the
quality-gate review's P0 (the Ctrl+K double-fire in the "Other"
custom-input field) and the user's hand-test feedback on the missing
two-step edge in the reverse direction plus the MemoryDialog
top-panel sections that weren't wired through SELECTION_*; round 3
swept the remaining adjacent dialogs (SettingsDialog list,
ManageModelsDialog tabs and search transitions, ProviderSetupSteps
advancedConfig, useSessionPicker's cross-mode arrows) so the
keyboard model is uniform across the TUI.

The original issue also asks for Meta+B / Meta+F word motion and
smarter Ctrl+H token-aware backspace among other readline
conveniences. The user explicitly scoped this PR down to Ctrl+P /
Ctrl+N at the planner approval gate; the remaining wish-list items
are deferred to follow-up issues.

Closes #3821

* docs(cli): refine Ctrl+P/N input-history rows; fix Ctrl+J in selection-list comment

Both items came from a non-blocking COMMENTED review on PR #4082
(https://github.com/QwenLM/qwen-code/pull/4082#pullrequestreview-4271527787),
flagging two polish points in the readline Ctrl+P/Ctrl+N feature the parent
commit `feat(cli): readline Ctrl+P/N for history and selection navigation`
(f66427b) introduced.

The `Up Arrow`, `Down Arrow`, `Ctrl+P`, and `Ctrl+N` rows of the Input
Prompt table in `docs/users/reference/keyboard-shortcuts.md` are reworded
to describe the three-phase keystroke sequence the implementation walks
through — an intra-buffer visual-row step (a no-op in a single-line
buffer, where there's exactly one visual row), a column-edge snap when
the cursor reaches the buffer's first or last visual row with the
cursor not already at column 0 (for the up-direction pair) or
end-of-line (for the down-direction pair), and the readline-style
previous-history or next-history walk on the press after the snap. The
reviewer specifically pointed out that the prior wording described
single-line input as "navigates the input history directly", which no
longer matches the post-PR-#4082 behavior: single-line input also goes
through the snap-then-walk two-press rule (the snap is a no-op when
the cursor is already at the line's edge column, in which case the
keystroke does the history walk on its first press). The new sentence
covers the single-line and multi-line cases in one shape — single-line
is the degenerate zero-row-walk-prefix instance of the same rule. The
up-direction text is shared verbatim between the `Up Arrow` row (L31)
and the `Ctrl+P` row (L43), and the down-direction text between the
`Down Arrow` row (L27) and the `Ctrl+N` row (L42), so the keyboard-
parity alias relationship is signaled by source-side text duplication
rather than a prose cross-reference. The Input Prompt table's 234-byte
canonical row width (the separator row's `| <50-dash> | <177-dash> |`
template, which sets the column-1 and column-2 source-side widths the
file's existing untouched rows already align to) is preserved by
trailing-ASCII-space padding inside the description column.

The comment above `[Command.SELECTION_UP]` and `[Command.SELECTION_DOWN]`
in `packages/cli/src/config/keyBindings.ts` previously read

    // Selection list navigation — up/k/Ctrl+P move selection up; down/j/Ctrl+N move selection down
    // ctrl: false on k/j ensures Ctrl+K (kill-line) and Ctrl+N (history-down) are not captured here

The `Ctrl+N` half of the second line is wrong: `Ctrl+N` is intentionally
matched here as the selection-down readline alias — the
`{ key: 'n', ctrl: true }` entry in the `SELECTION_DOWN` array literal
directly below the comment, mirroring the input-prompt-side
`[Command.HISTORY_DOWN]: [{ key: 'n', ctrl: true }]` binding at L134 of
the same file. The Ctrl-modified key the bare-letter `k` and `j`
matchers actually guard against — the one already bound elsewhere
whose double-match with the bare-letter selection-key the `ctrl: false`
opt-out is preventing — is `Ctrl+J`, the ASCII line-feed (0x0A) encoding
of the Enter family that appears as `{ key: 'j', ctrl: true }` inside
the four-alternative `[Command.NEWLINE]` array a few lines below. The
corrected one-liner is

    // Selection-list nav: arrows + k/j + Ctrl+P/Ctrl+N
    // ctrl: false on bare k/j skips Ctrl+K and Ctrl+J

in the same terse no-trailing-period section-label style as the file's
adjacent `// Screen control` (L129), `// History navigation` (L132),
`// Auto-completion` (L213, post-edit numbering), and `// Text input`
(L219) header comments. A 64-line block-comment that earlier in the
review-fix cycle wrapped this same correct fact in dispatch-broadcast-
model prose plus `keyMatchers.test.ts` backreferences was condensed to
those two lines for cell-budget consistency with the rest of the file.

No code behavior change. The local verification surface the reviewer
named at the bottom of the review summary stays green: from
`packages/cli`,

    npx vitest run \
        src/ui/keyMatchers.test.ts \
        src/config/keyBindings.test.ts \
        src/ui/components/InputPrompt.test.tsx

runs 178 cases with 177 passed and one unrelated skip (the
implementation file `InputPrompt.tsx`'s feature flag for the keyboard-
queue-input-editing case that was already skipped on the parent commit),
including all four cases inside the `InputPrompt > two-step edge
transition for history navigation` describe-block — `Ctrl+P with cursor
mid-line snaps to col 0 without touching history`, `Ctrl+N with cursor
not at end-of-line snaps to end without touching history`, `Ctrl+P at
col 0 walks history and parks the cursor at offset 0`, and `arrow Up
applies the same two-step rule as Ctrl+P (snap before navigate)`. Those
four test-case names are the implementation-side anchors the new docs
wording verbally mirrors. `npx tsc --noEmit -p .` in the same package
directory reports zero diagnostics.

* fix(cli): align readline history shortcuts with dialogs

* test(cli): cover readline navigation aliases

* fix(cli): guard readline shortcuts in dialog inputs

* test(cli): cover readline aliases in more dialogs
2026-05-16 23:07:25 +08:00
tanzhenxin
8d765fec78
refactor(core): TaskBase envelope + foreground subagent persistence (#3970)
* refactor(core): TaskBase envelope + foreground subagent persistence

Establishes a shared `TaskBase` envelope across the agent / shell /
monitor task registries with a mandatory `outputFile` field. Brings the
foreground subagent path into compliance with the new contract, so it
now leaves the same JSONL transcript + meta sidecar on disk that
backgrounded subagents have always produced — closing the only gap
where a registered task wrote nothing. Renames the agent-task
discriminator from `flavor: 'foreground' | 'background'` to claw-code's
`isBackgrounded: boolean`; the deprecated names are kept as
one-release type aliases.

PR 1 of the task-registry-unification design. PR 2 will collapse the
three per-kind registries into one thin TaskRegistry plus per-kind
modules.

* refactor(core): drop unused BackgroundTaskFlavor type alias

The alias only preserved the type name; no in-tree caller used it,
and after the field rename no realistic external consumer use survives
(reading entry.flavor / writing { flavor: ... } both fail at the use
site regardless of whether the alias resolves). Drop it instead of
carrying a hollow shim.

* fix(core): tighten foreground subagent launch path

- Register before writing the meta sidecar so a register() failure can't
  leave an orphaned 'running' meta file behind. writeAgentMeta is
  best-effort and never throws, so the inverse failure mode (registry
  entry without sidecar) is a benign degradation.
- Cache getGitBranch by cwd at the agent module level so foreground
  launches don't pay a fresh git rev-parse exec each time. Branches
  don't change within a process under normal use; the transcript
  annotation is best-effort audit metadata.
- Document on cancel() that foreground entries take a partial path
  through the method — Map deletion is the caller's responsibility
  via unregisterForeground() in the tool-call's finally path.

* fix(agent): correct foreground meta status mapping and register order

The foreground finally block in agent.ts mapped any non-ERROR, non-CANCELLED
terminate mode (including MAX_TURNS, TIMEOUT, SHUTDOWN) to 'completed' in
the sidecar, so post-mortem readers and resume logic saw a successful
status for runs that actually hit a guardrail. Flip the ternary to mirror
the background path: GOAL -> completed, CANCELLED -> cancelled, else ->
failed.

Also reorder the background launch so registry.register() runs before
writeAgentMeta(), matching the foreground path. Both paths now share the
same orphaned-meta guarantee.

* test(agent): rename stale foreground-flavor test

The "default flavor (absent) behaves as background" test name and its
backwards-compat comment referenced the old optional flavor field, but
the registration shape has required isBackgrounded for a while now —
there is no "absent" path to exercise. Rename it to describe what the
assertion actually covers: that background entries fire a task-
notification on complete.

* refactor(core): alias BackgroundTaskStatus to TaskStatus

The local `BackgroundTaskStatus` union was byte-identical to the new
shared `TaskStatus` defined in `tasks/types.ts`. Replace it with a
`@deprecated` type alias so external consumers (notably
`nonInteractiveCli.ts`) keep compiling unchanged while the canonical
name lives in one place.

* refactor(core): tidy monitorRegistry signatures and document cancel ordering

Two small consistency wins flagged in review:

1. `dispatchOwnerLifecycleWake` and `dispatchNotification` were the only
   methods on the registry still typed with the deprecated `MonitorEntry`
   alias. Rename their parameters to `MonitorTask` to match every other
   signature in the file.

2. `cancel()` orders `settle()` and `abort()` differently between its two
   branches, which is intentional (silent cancel locks the terminal status
   before abort listeners run; default cancel lets a naturally-completing
   operation settle through its own terminal path). Document that
   asymmetry in a JSDoc on the method so the next reader doesn't have to
   reverse-engineer it.

* refactor(core): migrate internal BackgroundTaskStatus refs to TaskStatus

The `BackgroundTaskStatus` alias was introduced in 91b59a8fb as a
`@deprecated` synonym for external SDK consumers (notably
`nonInteractiveCli.ts`). New internal references in this PR's own
file kept the old name; migrate them so the only remaining usage of
the deprecated alias is the alias declaration itself.

No behavior change — the alias is `= TaskStatus` so the union is
identical.

* test(agent): cover foreground failed-mode terminal status mapping

The foreground finally block maps GOAL→completed, CANCELLED→cancelled,
and everything else (ERROR, MAX_TURNS, TIMEOUT, SHUTDOWN) → failed.
Only the GOAL branch was asserted; the failed-mode fallback had no
coverage even though the same mapping recently regressed (d67da6d50)
and had to be fixed by review.

Adds a table-driven case mocking getTerminateMode to ERROR /
MAX_TURNS / TIMEOUT and asserting patchAgentMeta receives
status: 'failed'. CANCELLED is already covered by the
"foreground CANCELLED prefixes the partial result" test below.

* test(agent): cover foreground CANCELLED → cancelled meta mapping

Extends the foreground terminate-mode it.each to assert that
CANCELLED is recorded as `cancelled` in the on-disk sidecar — the
existing cancel-prefix test only verified the LLM-visible payload,
leaving the patchAgentMeta mapping uncovered. A regression flipping
CANCELLED → 'failed' would now fail this case.

* test(agent): make registry path assertions platform-agnostic

The outputFile/metaPath regexes hardcoded forward slashes, so the
foreground JSONL+meta reservation test failed on Windows where paths
use backslashes. Accept either separator.

* fix(core): guard executeBackground register-throw window; correct outputFile contract

A throwing register() subscriber in executeBackground() would leak the
already-spawned child + open output stream, unreachable by /tasks /
task_stop. Mirror the promote path's defensive try/catch: abort the
entry's controller, destroy the stream, and rethrow so the launch fails
visibly.

Also correct the TaskBase.outputFile contract: agent JSONL is
materialized on the writer's first append, which is the launch prompt
at attach time — not the first runtime event. A subagent cancelled
before any event still leaves a prompt-only JSONL plus meta, not meta
alone.
2026-05-16 22:53:08 +08:00
jinye
8dfbdaa5d4
feat(telemetry): unify span creation paths for hierarchical trace tree (#4126)
* feat(telemetry): unify span creation paths for hierarchical trace tree (#3731 P3 Phase 1)

Replace disconnected withSpan/startSpanWithContext calls in runtime with
session-tracing typed helpers so LLM and tool spans become children of
the interaction span instead of siblings under the session root.

- Add toolContext ALS with runInToolSpanContext() for concurrent-safe
  tool span scoping (uses AsyncLocalStorage.run, not enterWith)
- Wire startLLMRequestSpan/endLLMRequestSpan in loggingContentGenerator
  for both streaming and non-streaming paths
- Wire startToolSpan/endToolSpan + startToolExecutionSpan/endToolExecutionSpan
  in coreToolScheduler with proper try/finally lifecycle
- Remove redundant withSpan('client.generateContent') wrapper from client.ts
- Fix endToolSpan to not override pre-set status when metadata is omitted
- Change startToolExecutionSpan to read parent from toolContext ALS
- Update tests for new span creation APIs and remove dead test infrastructure

* fix(telemetry): address CI build errors in session-tracing tests

- Remove unused _toolSpan variable (TS6133)
- Use bracket notation for index signature property access (TS4111)

* fix(telemetry): update coreToolScheduler and loggingContentGenerator test mocks

- coreToolScheduler.test.ts: mock startToolSpan/endToolSpan/runInToolSpanContext
  instead of withSpan; update cancellation tests for restored safeSetStatus call
- loggingContentGenerator.test.ts: fix attribute keys in mock, add try/catch in
  endLLMRequestSpan mock to match production best-effort behavior

* fix(telemetry): address review feedback from wenshao

- Add debugLogger.warn in catch blocks of endLLMRequestSpan/endToolSpan/
  endToolExecutionSpan instead of silent swallowing
- Add JSDoc on endToolSpan documenting intentional no-metadata-no-status
  contract with setToolSpanFailure/setToolSpanCancelled
- Add warning in startToolExecutionSpan when called outside
  runInToolSpanContext (no active toolContext)
- Sanitize error message in endToolExecutionSpan: use constant
  TOOL_SPAN_STATUS_TOOL_EXCEPTION instead of raw error message

* fix(telemetry): use partial mock for telemetry/index.js in coreToolScheduler tests

The full mock shadowed all re-exports (logToolCall, etc.) causing 49 test
failures. Use importActual to preserve other exports, only override span
functions.

* fix(telemetry): getLastToolSpan must skip tool.execution sub-spans

startToolExecutionSpan mock also pushes to toolSpanRecords, so at(-1)
returns the execution sub-span instead of the tool span. Use findLast
to filter by name.

* fix(telemetry): address second round review feedback

- Remove redundant safeSetStatus(span, OK) on success path — endToolSpan
  in finally already sets OK via metadata
- Add llm_request.stream attribute (true/false) to distinguish streaming
  vs non-streaming LLM requests in trace backends

* fix(telemetry): endToolSpan mock writes to record directly

Bypass span.setStatus() in mock to avoid potential interference from
vitest module resolution. Write to statusCalls/ended directly on the
ToolSpanRecord.

* fix(telemetry): mock session-tracing.js directly instead of telemetry/index.js

Mocking the barrel re-export (telemetry/index.js) with importActual was
unreliable — vitest's module resolution could bind production code to the
real endToolSpan before the mock override took effect. Mock the source
module (session-tracing.js) directly to guarantee interception.

* fix(telemetry): fix endToolSpan status on success — toolCalls is empty in finally

Root cause: checkAndNotifyCompletion clears this.toolCalls before the
finally block in executeSingleToolCall runs, so the tc lookup always
returns undefined.

Fix: set OK status explicitly in _executeToolCallBody's success path
via safeSetStatus(span, OK), and call endToolSpan() without metadata
in finally (just ends the span, preserves pre-set status from any path).

* fix(telemetry): address Codex review — activate OTel context, end span on failure

- Wrap non-stream generateContent API call + logging in context.with(spanContext)
  so nested OTel spans (HTTP instrumentation, log-bridge spans) parent to
  qwen-code.llm_request instead of session root (matches streaming path).
- runInToolSpanContext now also activates OTel context via otelContext.with,
  not just the custom toolContext ALS. Hooks/HTTP/IO during tool execution
  now correctly parent to qwen-code.tool span.
- Split end*Span helpers: span.end() runs in its own try/catch so a throwing
  setAttributes/setStatus can't leak unended spans.

* fix(telemetry): address Codex review v2 — session-root fallback + execution span timing

- start{LLMRequest,Tool,ToolExecution}Span now fall back to getSessionContext()
  when no parent context, instead of otelContext.active(). Side-query LLM calls
  (auto-title, recap) now stay in the session trace instead of starting a new
  detached trace.
- Move startToolExecutionSpan() to BEFORE invocation.execute(), matching
  claude-code. Previously the synchronous setup inside execute (shell command
  preprocessing, child_process.spawn) ran outside the execution span.

* fix(telemetry): address Codex review v3 — sync throw, idle timeout race, test coverage

- coreToolScheduler.executeSingleToolCall: move try-block to wrap
  invocation.execute() so synchronous throws (e.g. shell setup failure)
  flow into the same catch path as async rejections. Previously a sync
  throw would leak the execution span and skip failure hooks.

- loggingStreamWrapper: track spanEndedByTimeout flag so a stream that
  resumes after the 5-min idle timeout does not run the final
  endLLMRequestSpan (which would no-op anyway, but the flag also stops
  resetSpanTimeout from queuing further timer callbacks).

- coreToolScheduler.test: add execution sub-span assertions for success,
  ToolResult.error, thrown invocation exceptions, and pre-hook denial.

- loggingContentGenerator.test: capture setAttribute calls into the mock
  span attributes record; assert llm_request.stream is false for non-stream
  and true for stream paths.

* fix(telemetry): address Codex review v4 — consistency + test coverage gaps

- endLLMRequestSpan now uses spanCtx.span for mutations (matches
  endToolSpan/endToolExecutionSpan pattern). Same object, but consistent
  lookup pattern prevents future drift.

- Mocks capture endLLMRequestSpan and endToolSpan/endToolExecutionSpan
  metadata so tests can assert token counts, durationMs, success, error
  are forwarded correctly. Add assertions on:
    * Non-stream LLM: inputTokens, outputTokens, success on response path
    * Non-stream LLM: success: false + sanitized error on rejection
    * Stream LLM: final lastUsageMetadata reaches endLLMRequestSpan
    * Tool execution sub-span: success: true on happy path
    * Tool execution sub-span: success: false on ToolResult.error
    * Tool execution sub-span: success: false + sanitized error on throw

- Add OTel error resilience tests: when setAttributes or setStatus throws,
  span.end() must still run and the span must be removed from activeSpans.
  Covered for endLLMRequestSpan, endToolSpan, endToolExecutionSpan.

* fix(telemetry): address Codex review v5 — abort distinction + API symmetry

- session-tracing.ts SpanContext.type: comment 'tool.blocked_on_user' |
  'hook' as Phase 2 forward-declarations (no helpers wired yet).

- endToolExecutionSpan: align no-metadata-no-status behavior with
  endToolSpan. Currently no caller omits metadata, but the asymmetric
  default (OK vs preserve-pre-set) was a maintenance trap.

- loggingContentGenerator generateContent (non-stream) catch block:
  call endLLMRequestSpan BEFORE the logging block, mirroring the
  streaming path. Defense-in-depth against logging-side rejections.

- loggingContentGenerator: restore abort-specific span status message.
  All three LLM error paths (non-stream catch, stream eager-error catch,
  stream loggingStreamWrapper finally) now use
  API_CALL_ABORTED_SPAN_STATUS_MESSAGE when req.config.abortSignal.aborted,
  matching the original withSpan('client.generateContent') behavior.
  Trace backends can now distinguish cancellations from real failures.

- coreToolScheduler _executeToolCallBody catch: distinguish abort vs
  exception in execSpan error message. New constant
  TOOL_SPAN_STATUS_TOOL_CANCELLED prevents operators filtering exec
  spans for errors from seeing cancellation false positives.

- New test asserting exec span uses cancelled-by-user message when the
  invocation throws after abort.

* fix(telemetry): always write 'success' attribute on tool spans

E2E review found qwen-code.tool spans never carry the `success` boolean
attribute (the helper only writes it when metadata is passed, and the
finally block calls endToolSpan(toolSpan) without metadata). This breaks
the most common observability query — filtering tool failures with
`success = false` — because tool spans don't have that field at all.

Fix: setToolSpanFailure / setToolSpanCancelled now also call
span.setAttribute('success', false); the success path in
_executeToolCallBody adds span.setAttribute('success', true) after
safeSetStatus(span, OK). Mirrors the unconditional `success` attribute
on llm_request spans, so backends can use one query for both span types.

Add 4 scheduler-level tests asserting the success attribute on:
- success path
- ToolResult.error path
- thrown invocation path
- cancellation path
2026-05-16 22:29:55 +08:00
jinye
784182dfe3
feat(skills): add /stuck diagnostic skill for frozen sessions (#4133)
* feat(skills): add /stuck diagnostic skill for frozen sessions

Port the /stuck diagnostic capability to qwen-code as a bundled skill.
Scans for stuck processes, high CPU/memory, hung subprocesses, and
debug logs, then presents a structured diagnostic report.

Adapted from claude-code's internal /stuck skill with:
- Process identification via command path (node-based CLI, not compiled binary)
- Debug log path updated to ~/.qwen/debug/
- Cross-platform stack dump support (macOS sample + Linux /proc/stack)
- Direct user-facing output (no Slack dependency)

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): respect QWEN_RUNTIME_DIR/QWEN_HOME for debug log path in /stuck

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): add allowedTools and clarify diagnostic-only boundary in /stuck

- Add allowedTools (run_shell_command, read_file) for convention consistency
- Rephrase recommended actions as user-facing options, not model-executable commands

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): address review feedback for /stuck — security, accuracy, sidecar

- Add explicit PID argument validation (reject shell metacharacters) to
  prevent the model from substituting injection payloads into shell commands
- Mention macOS/BSD `U` state alongside Linux `D` for uninterruptible sleep,
  so I/O-blocked macOS sessions are not silently missed
- Add `-ww` to `ps` to disable column truncation, so long qwen paths don't
  fall outside the grep window and cause sessions to be missed
- Use `~/.qwen/projects/*/chats/*.runtime.json` sidecars as the primary
  source of (pid, sessionId, workDir) mappings; `ps` is now a supplement
  for CPU/RSS/state enrichment

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): apply minimal review fixes for /stuck

- Filter ps to current UID via -u "$(id -u)" — avoid leaking other users'
  Qwen processes on shared hosts
- Note that ps `rss=` is in KB; divide by 1024 before MB comparison
- Replace `pgrep -lP` with `pgrep -P` + `ps -p` so child state shows up
- Mention `advanced.runtimeOutputDir` setting alongside QWEN_RUNTIME_DIR /
  QWEN_HOME in the runtime-base description
- Add half-line about PID reuse handling and not quoting secrets from
  debug logs (without inflating the prompt into a full workflow)

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): round-3 review fixes for /stuck

- Resolve RUNTIME_DIR from QWEN_RUNTIME_DIR/QWEN_HOME and use it in the
  sidecar `ls`, debug log path, and `latest` symlink — the previous
  round only updated the prose and left the actual commands hardcoded
- Add explicit fallthrough: when sidecar enumeration finds nothing, fall
  through to step 2 instead of getting stuck trying to make sidecar work
- Replace metacharacter blacklist with digit-only PID whitelist — safer
  and shorter; "etc." in a blacklist outsourced completeness to the LLM
- Drop `strace -p <pid> -c -f` from the Linux stack-dump branch: `-c`
  blocks until the target exits, hanging the diagnostic on the very
  conditions it should diagnose; `ptrace_scope=1` would also misreport
  permission errors as process symptoms. Keep `cat /proc/<pid>/stack`
- Warn that `ps -ww` command lines may include CLI-arg credentials and
  that `sample` stack frames may include in-memory secrets — redact
  before quoting in the report
- Cover the "no sessions found at all" case so a fresh machine doesn't
  get reported as "all healthy" when zero data was collected

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): /stuck overview-vs-step3 consistency and self-explanatory state triage

- Update "What to look for" overview from `pgrep -lP <pid>` to
  `pgrep -P <pid>` to match step 3 (overview was left behind in the
  previous round when step 3 was upgraded to capture child state)
- Add a triage sentence to step 3: when the state alone explains the
  problem (`T` = stopped, `Z` = zombie), skip child/log/stack inspection
  and go straight to the report

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): correct /stuck runtime base priority order and resolution

The actual priority in `Storage.getRuntimeBaseDir()` is
`QWEN_RUNTIME_DIR` > `advanced.runtimeOutputDir` setting > `QWEN_HOME` >
`~/.qwen`. The previous round merged the `advanced.runtimeOutputDir`
mention but listed it after `QWEN_HOME`, and the shell snippet skipped
the settings layer entirely — so on a machine where only the setting
was configured, the skill would silently look in `~/.qwen` and miss all
sessions.

- Reorder the prose priority list to match the source
- Add a `jq`-based read of `~/.qwen/settings.json` between the env-var
  and `QWEN_HOME`/default fallbacks. Gracefully degrades if `jq` is
  absent or the setting is unset.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* feat(skills): improve /stuck diagnostic flow

Functional upgrades found in self-review (no reviewer raised these):

- Add network-hang detection bullet to step 3. Hung HTTPS requests to
  the model API are the most common qwen-code "stuck" mode and showed
  as healthy under all previous heuristics (low CPU + S state). macOS
  uses `lsof -i -p`, Linux uses `ss -tnp`.
- Add a fast path at the top of "Investigation steps": when the user
  passes a digit-only PID, skip enumeration and go straight to per-PID
  ps + step 3. Avoids a full sidecar+ps scan in the targeted case.
- Replace per-file sidecar liveness check with a single bash loop that
  emits only live (pid, sidecar-path) pairs. On machines with many
  stale sidecars this drops 50+ separate reads.
- Promote `~/.qwen/debug/latest` to the primary debug-log entry point
  (it usually points to the suspicious session). Sidecar-derived path
  becomes the fallback.
- Bound the debug-log read with `tail -n 200` so the model doesn't
  attempt to load multi-GB log files.
- Replace the placeholder `<child_pids>` for `ps -p` with a runnable
  `pgrep -P <pid> | xargs -I{} ps -p {} -o ...` composition.
- Drop the redundant "substitute <pid> only after validation" note in
  step 3 — the digit-only whitelist in Argument validation already
  enforces this; PIDs from ps/sidecar are inherently digit-only.

End-to-end tmux smoke test confirms the flow runs to completion with a
correct structured report.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): /stuck — RUNTIME_DIR preamble + jq-free sidecar liveness

Two issues caught by Codex review:

1. **PID fast path left $RUNTIME_DIR unset.** Step 3 references
   `"$RUNTIME_DIR"/debug/<session-id>.txt` but the fast path skipped
   step 1 where it was resolved, so debug-log lookup degraded to
   `/debug/latest` (broken absolute path). Fix: extract RUNTIME_DIR
   resolution into a preamble that runs before both paths. Also add a
   `grep -l "pid": <PID>` lookup in the fast path so it can match the
   given PID to its sidecar and recover the session ID for log lookup.

2. **Sidecar liveness loop required `jq`.** Default macOS / minimal
   Linux images don't ship `jq`, so the loop emitted nothing for every
   sidecar — the "preferred reliable" path silently failed and the
   skill fell back to the less accurate `ps | grep`. Replace with a
   single-spawn `node -e` script: node is guaranteed present (qwen-code
   itself runs on it). The settings.json `jq` lookup stays — that one
   gracefully degrades to QWEN_HOME/default if `jq` is missing.

Both verified by hand: liveness loop correctly emits live PID/sidecar
pairs (56219, 33534), `grep -l` lookup correctly finds the sidecar for
a given PID and emits empty for non-matches.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): /stuck — validate fast-path PID is a Qwen Code process

Codex review caught that the targeted PID fast path accepted any
digit-only PID and dumped its full command line, bypassing the Qwen-
process filter that the general scan applies via
`grep -E '(qwen|node.*qwen|bun.*qwen)'`. Cross-user PIDs are already
filtered (`kill -0` returns EPERM), but **same-user non-Qwen processes**
would have their argv (potentially including secret CLI flags) printed
into the chat.

Fix: add a single-line validation pipeline before the stats dump:
`kill -0 <pid> && ps -p <pid> -o command= -ww | grep -qE '(qwen|node.*qwen|bun.*qwen)'`.
If it returns non-zero, refuse with "PID is not a current-user Qwen
Code session" and stop the diagnostic. Otherwise proceed.

Verified by manual test against a real Qwen Code session PID (matches)
and PID 1 / launchd (correctly rejected).

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): /stuck — settings path, sidecar grep, ps regex, lsof safety

Four issues from PR review:

1. **Settings path honors QWEN_HOME.** The `jq` lookup in the preamble
   hardcoded `~/.qwen/settings.json`, but `getGlobalSettingsPath()`
   resolves to `$QWEN_HOME/settings.json` when set. Now uses
   `"${QWEN_HOME:-$HOME/.qwen}/settings.json"`.

2. **Sidecar grep uses `-El`.** Without `-E`, BSD `grep` on macOS may
   not treat `\b` as a word boundary in BRE. Also added a note: when
   PID reuse makes multiple sidecars match, prefer the most recently
   modified file via `ls -t | head -n 1`.

3. **Process regex tightened to avoid false positives.** The old
   `(qwen|node.*qwen|bun.*qwen)` matched any path containing "qwen"
   anywhere — so `qwen-playground/server.js`, `qwen-polyfill.js`,
   and even unrelated processes that pass a qwen-code path as `--cwd`
   (e.g., Codex plugin brokers) all matched. Replaced with
   `(qwen-code/[^ ]*\.(js|ts|mjs|cjs)( |$)|/qwen( |$))` — requires the
   `qwen-code/` substring to be followed by a script-file path, OR
   the bin invocation to end in `/qwen`. Verified on the local machine
   that broker processes are no longer matched while real Qwen
   sessions (worktree dev, dist/cli.js, qwen serve daemons) all are.

4. **lsof safety.** Added `-nP` to skip reverse-DNS and port lookups
   which can themselves hang. Mentioned `timeout 10` / `gtimeout 10`
   as an optional prefix when available — qwen-code's shell tool
   already has a backstop timeout, so this is belt-and-suspenders.

Note: tested `\b` in BSD ERE on macOS — it does work correctly with
`-E`, so the `-El` switch alone fully addresses concern #2's
portability claim (BRE-without-E remains broken but is no longer used).

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): /stuck — expand ~ and resolve relative paths in RUNTIME_DIR

`Storage.resolvePath()` in qwen-code expands `~` and resolves relative
paths before using `advanced.runtimeOutputDir`. The shell preamble was
reading the raw JSON value via `jq`, so a user with
`"runtimeOutputDir": "~/.qwen-runtime"` would pass the literal string
to the glob — bash does not expand `~` inside double quotes — and the
sidecar scan would silently find nothing and fall back to ps-only mode.

Add two bash lines after the jq lookup:
- `${RUNTIME_DIR/#\~/$HOME}` to substitute leading tilde
- `case ... cd && pwd` to resolve relative paths to absolute (clears
  RUNTIME_DIR if cd fails so the chain falls through to QWEN_HOME)

Smoke tested: tilde paths expand, absolute paths pass through, relative
paths resolve, nonexistent dirs clear cleanly, empty stays empty.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(skills): /stuck — round-N review feedback

Adopted 9 of the 16 review suggestions; declined 5; 1 already done.

- Anchor process regex to `(^|/)qwen-code[^ /]*/`. Now matches renamed
  clones (`qwen-code-dev`, `qwen-code-x1`, worktrees) AND rejects
  prefix false positives (`analyze-qwen-code/`, `my-qwen-code-tool/`).
  Verified against 10 cases.

- Clarify RSS unit conversion: KB ÷ 1024 = MB, KB ÷ 1048576 = GB. The
  4GB threshold is `4194304` KB raw, or 4 in GB. Prevents the model
  from dividing once and comparing to 4, which would over-alert by
  1024×.

- Add `State S with low CPU` to the Signs list so initial triage flags
  the most common hang signature (hung HTTPS to model API) instead of
  only catching it inside step 3.

- Split fast path validation into two guards with distinct messages:
  dead/wrong-user vs. yours-but-not-Qwen. Plus add the same
  credential-redaction note that step 2 already has.

- Replace `pgrep | xargs -I{} ps` with a single `ps -p $CHILDREN`
  call (avoids forking N times) and add `-ww` so long child cmdlines
  don't truncate.

- Wrap macOS `sample <pid> 3` with optional `timeout 15` (or
  `gtimeout 15`). Same belt-and-suspenders pattern used for `lsof`.

- Note that `ss -tnp -p` requires root/CAP_NET_ADMIN; non-root sees
  `-` in the PID column. Tell the model to fall back to `lsof` instead
  of concluding "no connections".

Declined: self-PID via `$$` (wrong PID — `$$` is the spawned shell,
not qwen), pgrep fallback for distroless (over-engineering), `\b`
matches negative numbers (false alarm — `:[[:space:]]*` won't match
through `-`), regex DRY abstraction (no value in markdown prompts),
project-level settings.json read (already declined; same trade-off).

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* test(skills): add integration test that parses every bundled SKILL.md

The bundled skill loader (`SkillManager.parseSkillFileInternal`) silently
catches and debug-logs frontmatter parse errors, so a typo in any
SKILL.md (missing `description`, broken `---` delimiter, `allowedTools`
written as a scalar) merges with green CI and only surfaces when a user
invokes the skill — at which point the skill is missing from
autocomplete with no indication why.

Add a tiny integration test that walks `packages/core/src/skills/bundled/`,
runs every `SKILL.md` through the real `parseSkillContent` (no mocks),
and asserts: name matches the directory, description is non-empty, body
is non-empty, and `allowedTools` (if present) is an array.

Lives in its own file because `skill-load.test.ts` mocks `fs/promises`
and the YAML parser, which would defeat the purpose of an integration
test. New file uses real fs and the real loader.

Negative-case verified: deliberately corrupting `stuck/SKILL.md`'s
frontmatter delimiter makes only that file's test fail; restoring it
returns the suite to all-green.

Addresses wenshao's standing [Critical] review (2026-05-15 12:29Z) about
the bundled skill system lacking automated tests for SKILL.md parsing.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
2026-05-16 22:23:02 +08:00
jinye
379d14ad00
feat(rewind): add file restoration support to /rewind command (#4064)
* feat(rewind): add file restoration support to /rewind command (#3697)

Previously /rewind only truncated conversation history — files modified
by the assistant remained on disk. This adds a file-copy-based backup
system (ported from claude-code's fileHistory) so users can optionally
roll back file changes when rewinding.

Core changes:
- New FileHistoryService with snapshot/backup/restore lifecycle
- trackEdit() called before each file write in edit and write-file tools
- makeSnapshot() at each user turn boundary in client.ts
- Three-phase RewindSelector UI: pick turn → choose restore option → execute
- RestoreOption type: 'both' | 'conversation' | 'code' | 'cancel'

Closes #3697

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(rewind): replace findLast with reverse loop for ES2022 compat

vscode-ide-companion targets ES2022 which lacks Array.findLast.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(rewind): add missing i18n translations and fix test expectation

- Add file restore i18n keys to all 8 locale files (zh-TW, ca, de, fr,
  ja, pt, ru were missing)
- Update useGeminiStream test to expect promptId in user history item

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(rewind): add getFileHistoryService mock to tool tests

edit.test.ts and write-file.test.ts mock configs lacked the new
getFileHistoryService method, causing trackEdit calls to throw.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(rewind): allow Esc during diff loading and add missing i18n footer strings

Allow users to press Esc/Ctrl+C to cancel during diff stats loading
phase. Add three missing footer navigation strings to all 9 locale files.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(rewind): address review feedback — restoreBackup correctness, missing promptId warning, dead code removal

- restoreBackup now returns boolean; applySnapshot only counts a file
  as restored when the backup was actually applied (fixes misleading
  "Restored N file(s)" when backup is missing on disk)
- Show warning when user selects file restore on a turn created before
  file checkpointing was enabled (promptId undefined)
- Remove unused snapshotSequence field, canRestore(), and hasAnyChanges()
  methods that had no callers

* fix(rewind): correct diff direction, truncate snapshots on rewind, add zero-files feedback

- Swap diffLines args to diffLines(backup, current) so +/- stats
  match git convention (insertions = lines added since checkpoint)
- Truncate snapshots after rewind to discard stale timeline state,
  preventing makeSnapshot from using wrong baseline
- Show "No files needed restoration." when rewind finds files already
  at target state (all 9 locales)

* test(tools): assert trackEdit is called before file writes

* fix(i18n): add missing rewind UI locale keys across all 9 locales

* fix(core): reset fileHistoryService on session change, clean up dead code

- Reset fileHistoryService in startNewSession() so /clear gets a fresh
  instance with the new sessionId
- Rebuild trackedFiles after rewind() to avoid stale stat() calls
- Remove unused setCurrentPromptId/getCurrentPromptId dead API

* fix(rewind): validate conversation before file restore, preserve snapshots for code-only

- For 'both': validate conversation can be truncated before restoring
  files to prevent inconsistent state (files rolled back but conversation
  stays at newer state)
- For 'code'-only: pass truncateHistory=false so snapshot timeline is
  preserved — conversation turns remain visible and their snapshots stay
  available for future rewinds

* fix: correct trackEdit race comment — overwrite not orphan

* fix(types): use HistoryItemWithoutId for addItem to preserve union member properties

* fix(types): revert addItem type change, use cast at call site for promptId

* fix(rewind): guard onRewind calls with .catch() to prevent unhandled rejection

* fix(rewind): only truncate snapshot timeline when conversation truncation will execute

* fix(rewind): address tanzhenxin review - gate, partial failure, tests

1. Disable file checkpointing for non-interactive (-p) mode by gating
   on `params.interactive !== false` in addition to `!params.sdkMode`.

2. Surface partial restore failures: `rewind()` now returns
   `RewindResult { filesChanged, filesFailed }`. In "both" mode,
   conversation truncation is skipped when any file fails to restore,
   preventing inconsistent state.

3. Add comprehensive unit tests for FileHistoryService (17 tests
   covering trackEdit, makeSnapshot, rewind, eviction, diffStats).

* fix(rewind): defensive trackEdit + fix version collision on re-track

1. Wrap trackEdit calls in edit.ts and write-file.ts with try/catch
   so file history failures never break core tool operations.

2. Replace hardcoded version:1 in trackEdit with max-version lookup
   across all snapshots. Prevents backup file overwrite when the same
   file is re-tracked after a code-only rewind (truncateHistory=false).

* fix(rewind): add missing i18n keys + fix makeSnapshot version collision

1. Add 'Failed to restore {{count}} file(s): {{files}}' to all 7
   missing locales (ca, de, fr, ja, pt, ru, zh-TW).

2. Use global max-version scan in makeSnapshot (same as trackEdit)
   to prevent backup filename collisions after snapshot eviction.

* fix(rewind): set hasRestoreFailure when promptId is missing

In "both" mode, if the target turn has no promptId, conversation
truncation was still proceeding because hasRestoreFailure was not set.
Now correctly blocks truncation to prevent inconsistent state.

* fix(rewind): show loading state during async restore, close selector in finally

Defer setIsRewindSelectorOpen(false) to a try/finally block so the
selector stays visible during async file restore. RewindSelector now
manages its own isRestoring state: shows "Restoring..." text and
disables all keypress handlers while the restore is in progress.

This prevents the user from seeing a bare prompt with no progress
indicator during slow restores, and eliminates the race where typing
during restore could clobber the pre-filled prompt.

* fix(rewind): skip timeline truncation on partial failure + fix wording

1. rewind() now only truncates the snapshot timeline when
   filesFailed is empty, preventing loss of future checkpoints
   when the caller skips conversation truncation due to failures.

2. Change "No files needed restoration." to the more idiomatic
   "No files needed to be restored." across all 9 locales.

* fix(rewind): address review — TOCTOU in createBackup + outer catch in handleRewindConfirm

- Extract safeCopyFile(src, dst) helper that distinguishes source-missing
  (TOCTOU: file deleted between stat and copyFile) from target-dir-missing,
  so trackEdit no longer silently fails when a file disappears mid-backup.
  Same helper now covers restoreBackup.
- Wrap handleRewindConfirm with an outer catch that surfaces unexpected
  failures via historyManager error item; previously a sync throw from the
  post-rewind block would silently close the selector and leave 'both'
  mode in a half-applied state.
- Add 'Rewind failed: {{error}}' i18n key in all 9 locales.

* test(rewind): cover restoreFromSnapshots, trackEdit no-snapshot path, partial-failure timeline guard

- restoreFromSnapshots: assert relative-path shortening + external-path preservation
- trackEdit before any makeSnapshot: assert no-op early return
- rewind truncation guard: assert snapshot timeline is preserved when filesFailed > 0

* fix(rewind): clean up orphaned backups, surface no-client states, polish

- Per-eviction backup cleanup: when MAX_SNAPSHOTS overflow or rewind
  truncation drops snapshots, remove backup files no longer referenced
  by any surviving snapshot (best-effort, ENOENT-tolerant). Backup files
  are content-deduplicated across snapshots, so the live-set is computed
  from survivors before deletion.
- Surface no-client failure modes in handleRewindConfirm: 'conversation'
  mode now shows an error instead of silently returning; 'both' mode
  shows an info message after restore so the user knows the conversation
  half was skipped.
- i18n the previously hardcoded 'Conversation rewound...' message and
  add 3 new keys to all 9 locales.
- Tighten createBackup signature (drop unreachable null branch).
- Extract getMaxVersion helper to deduplicate identical loops in
  trackEdit and makeSnapshot.

Tests added: orphan-cleanup on overflow, dedupe preservation, rewind
truncation cleanup. All existing tests continue to pass (23 core, 71
AppContainer, 27 i18n).

* fix(rewind): use path separator constant in maybeShortenFilePath

The hardcoded '/' check meant Windows absolute paths (with '\') never
matched the cwd prefix, so the shortening was a no-op on Windows. The
new cleanup tests revealed this by asserting on the relative-path key:
on Windows the key was the full absolute path, so trackedFileBackups
lookups returned undefined.

Switching to the platform sep also makes Windows snapshots use the
relative key like POSIX, improving portability if cwd moves later.
restoreFromSnapshots re-runs maybeShortenFilePath on every key, so
existing on-disk sessions migrate transparently on resume.

* test(rewind): cover trackEdit best-effort guarantees and unchanged-file rewind

- edit.test.ts: assert tool still completes (file written, llmContent
  reflects the edit) when FileHistoryService.trackEdit rejects.
- write-file.test.ts: same for the write_file tool.
- fileHistoryService.test.ts: assert trackEdit swallows createBackup
  failures (forced via storageDir-replaced-with-file → ENOTDIR in
  recursive mkdir) without recording any backup.
- fileHistoryService.test.ts: assert applySnapshot leaves a file
  untouched (mtime unchanged, filesChanged empty) when its content
  already matches the target backup — covers the
  checkOriginFileChanged short-circuit.

* fix(rewind): align fileCheckpointing default + surface backup-missing on rewind

Two issues from a Codex review pass:

- Config: `fileCheckpointingEnabled` defaulted via `params.interactive !== false`,
  which resolves truthy when the caller omits `interactive` — but `this.interactive`
  itself defaults to `false`. Headless/programmatic callers that did not set
  `interactive` would silently start writing file-history backups under
  `~/.qwen/file-history/`. Use the same `?? false` default so the gate matches
  the resolved interactive value.

- checkOriginFileChanged: when the on-disk backup AND the working file have both
  been removed externally, the function returned `false` ("unchanged"), so
  `applySnapshot` skipped `restoreBackup` and rewind reported success even though
  the target snapshot expected the file to exist. Treat any failure to stat the
  backup as "changed" so callers attempt the restore: applySnapshot surfaces the
  missing backup via restoreBackup → filesFailed, makeSnapshot creates a fresh
  backup. Added a regression test for the both-missing path.

* fix(rewind): mark per-file backup failures so rewind surfaces them

Two related issues from a /review pass:

1. Silent data loss in makeSnapshot inheritance: when the per-file
   backup attempt threw inside makeSnapshot, the catch block left the
   path missing from `trackedFileBackups`, and the inheritance loop
   then copied the previous snapshot's backup into the new snapshot.
   A later rewind to that snapshot would restore older content while
   reporting success.

   Now the catch records `{ failed: true, ... }` for the path. The
   inheritance loop skips paths already present in trackedFileBackups,
   so failed paths are no longer paved over by stale carryover. Both
   applySnapshot and getDiffStats honor `failed` — rewind pushes the
   path to filesFailed and the diff preview omits it.

2. Marketing/scope mismatch: the rewind UI offers "Restore code" but
   the feature only tracks edits made via the `edit` and `write_file`
   tools — shell-mediated changes (`sed -i`, `cp`, `rm`, `mv`,
   `npm`, etc.) and out-of-tool manual edits are not captured.
   Added a class-level JSDoc on FileHistoryService spelling out the
   scope, and an inline footer in the restore-options panel:
   "Rewinding does not affect files edited manually or via shell
   commands." (matching the upstream claude-code MessageSelector
   wording). New i18n key in all 9 locales.

Test added: trackEdit/makeSnapshot per-file failure path. Asserts
the new snapshot has `failed: true`, and that rewind to that snapshot
reports the file as filesFailed instead of silently restoring the
inherited stale backup.

* fix(rewind): polish — i18n, type tightening, resumed-session UX hint

Several small wins from the latest /review pass plus a UX mitigation for
turns whose file-history snapshot is not present in memory (most often
because the conversation came from a resumed session, but also when a
turn has no captured edits):

- AppContainer: wrap the "Cannot rewind to a turn that was compressed"
  error in t(); add the new key to all 9 locales.
- RewindSelector: replace the inline `(+N -M in K file/files)` template
  literal with t() using two plural-aware keys; add to all 9 locales.
- DiffStats.filesChanged: tighten from optional to required to match
  reality (every code path that returns a DiffStats sets it). Drops the
  `!.filesChanged!` non-null cascade in RewindSelector.
- RewindSelector phase 2: when the option list does not contain
  code/both (i.e. no file-restore is actionable for this turn), show
  an explicit hint instead of leaving the user to guess why those
  options are missing. Same i18n key in all 9 locales.

The mitigation hint covers the resumed-session case Tan raised
(snapshots are not rehydrated by `/resume` today) without changing
behavior — `getRestoreOptions` already gracefully degrades to
conversation-only when `getDiffStats` returns undefined for a snapshot
that is not in memory; we just surface the "why" to the user.

* fix(rewind): unstick failed marker on the unchanged-file fast path

The `failed: true` marker added in d59838338 was sticky: once set, the
no-change optimization in `makeSnapshot` would copy the failed entry
forward into every subsequent snapshot for as long as the file stayed
unchanged. A single transient I/O error therefore poisoned `/rewind`
for that file until the user happened to modify the content again.

Add `!latestBackup.failed` to the no-change reuse guard so a failed
entry is never copied forward — the next snapshot retries the backup,
which either heals (when the underlying I/O has recovered) or honestly
records another failed entry.

New regression test (`does not carry a failed marker forward when the
file is unchanged`):

- Snapshot p1 with file content X
- Sabotage the storage dir → p2's per-file backup throws → p2 records
  failed: true
- Restore the storage dir; file still equals X
- p3 must NOT copy p2's failed entry; it must retry createBackup and
  produce a fresh non-failed entry that allows rewind to p3 to succeed
2026-05-16 22:16:01 +08:00
qqqys
0dde1ad704
feat(cli): add session-scoped /goal command with judge-driven turn continuation (#4123)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(cli): add session-scoped /goal command with judge-driven turn continuation

`/goal <condition>` pins a free-form objective for the rest of the session.
While a goal is active, an LLM judge runs at every Stop boundary and either
lets the turn end (condition met) or feeds the judge's reason back as the
next user prompt to keep the model working. Auto-clears on success;
`/goal clear` cancels early. Same primitive as Anthropic's Claude Code
2.1.140 `/goal`, built on qwen-code's existing Stop-hook + function-hook
plumbing — no new subsystem.

Core (packages/core/src/goals/):
  - activeGoalStore: per-session active goal + last-terminal cache, with a
    terminal-observer channel the CLI subscribes to so achieved/aborted
    cards land in history.
  - goalJudge: side-query against a fast model, transcript-grounded
    system prompt + json_schema response + disabled thinking. Tolerant
    JSON extraction with fallback so a flaky judge can't kill the loop;
    30s default timeout (vs. the 5s function-hook default that was
    silently killing real-world judge calls).
  - goalHook: function hook on Stop. Returns {decision:'block', reason}
    when not met (reusing client.ts's existing recursive continuation),
    {continue:true} when met. Self-clears active goal + notifies the
    terminal observer on met/aborted. MAX_GOAL_ITERATIONS=50 backstop.

CLI:
  - goalCommand: /goal | /goal <cond> | /goal clear|stop|off|reset|none|
    cancel. 4000-char cap, trust + disableAllHooks gates. Empty /goal
    shows running status, falls back to the last completed summary.
  - GoalPill: footer chip "◎ /goal active (12s)" — terse, claude-aligned.
  - GoalStatusMessage: set / checking / achieved / cleared / aborted
    history cards. "checking" replaces the generic stop_hook_loop chip
    for goal-driven iterations.
  - restoreGoal: on session resume, rehydrate the active goal hook +
    last-terminal cache from transcript so /goal survives /resume.

Cross-cutting fixes:
  - HookSystem.hasHooksForEvent(eventName, sessionId?): also consults
    SessionHooksManager. Previously SDK / programmatic Stop function
    hooks were silently gated out by client.ts's fast-path check, so
    they never fired.
  - client.ts: yield StopHookLoop on every continuation iteration (was
    iter > 1) — first not-met turn is now visible in the UI.
  - useGeminiStream: commit pending item + clear thoughtBuffer /
    geminiMessageBuffer on every Finished event. Fixes a UI bug where
    a Stop-hook continuation's text bled into the prior turn's pending
    history item (cumulative "te" / "tes" rendering), even though the
    persisted transcript was clean.

Co-authored-by: Qwen-Coder <noreply@qwen.ai>

* test(cli): fix footer goal pill mock

* fix(goal): persist terminal status on restore

* fix(goal): harden judge hook

* fix(goal): sanitize condition in instruction prompt and update matcher test

- goalCommand.ts: collapse newlines and downgrade embedded double-quotes in
  the condition before splicing into the instruction prompt so the wrapping
  quote structure stays intact.
- goalLoop.integration.test.ts: matcher assertion updated to '*' to match the
  current registerGoalHook contract (previously '').

Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>

* feat(goal): surface judge reason on terminal cards

Renders `Last check: <reason>` on the achieved / aborted history card
and on the empty-`/goal` summary so the final view records *why* the
judge ruled the goal complete. Uses a single inline-label Text instead
of the flex-row split used for `Goal:` — the reason is capped at 240
chars and almost always wraps; the flex-row variant hangs the
continuation at the value column's left edge (~12 cols of blank space,
easily mistaken for a stray empty line). Single Text + natural wrap
keeps the continuation flush.

Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>

* fix(goal): re-arm /goal on runtime /resume and /branch

Cold boot path in AppContainer already calls restoreGoalFromHistory after
loading session data, but the runtime /resume and /branch paths skipped
it entirely. After /new + /resume back to a session that had an active
/goal, the in-memory activeGoalStore entry still held the pre-/new
setAt and a hookId pointing to a hook that config.startNewSession() had
torn down — leaving the footer pill ticking from the original setAt
(observable as "几十秒" elapsed immediately after resume) while the
Stop hook was silently dead.

Wire restoreGoalFromHistory into both handlers right after the session
data lands so unregisterGoalHook clears the stale entry and
registerGoalHook re-arms with a fresh setAt / hookId and re-installs
the terminal observer.

Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>

* refactor(goal): reuse shared formatDuration utility

Drop the duplicated local formatDuration from goalCommand.ts and
GoalStatusMessage.tsx in favor of the shared formatters.ts version,
called with { hideTrailingZeros: true }. The shared util already has
its own test suite and matches Claude Code's ShellTimeDisplay style
(round values drop zero-unit tails: `5m 0s` → `5m`).

Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>

* fix(goal): abort judge API call on judge timeout

The judge-timeout path in judgeGoalWithTimeout only resolved a fallback
verdict; the underlying judgeGoal generateContent call kept running
because the hook context signal is never aborted by the timeout. Each
timeout leaked one in-flight request that accumulated across goal-loop
iterations. Link an AbortController into the judge signal and abort it
when the timeout fires.

Co-Authored-By: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(goal): harden judge continuation feedback

* test(goal): align loop integration with safe continuation

* fix(cli): harden goal resume lifecycle

* fix(cli): address goal review blockers

* fix(goal): guard stale same-condition callbacks

---------

Co-authored-by: Qwen-Coder <noreply@qwen.ai>
Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-16 18:14:13 +08:00
jinye
264ed82273
[codex] feat(serve): add capability registry protocol versions (#4191)
* feat(serve): add capability registry protocol versions

Introduce a serve capability registry and advertise protocolVersions from /capabilities while preserving the existing v1 envelope and Stage 1 feature aliases. Update SDK wire types, docs, and focused tests for old-daemon compatibility.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): clarify capability advertisement semantics

Address PR review feedback by preserving historical capability versions, separating registered and advertised feature helpers, testing protocol version metadata directly, and keeping runtime exports out of the serve types module.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-16 18:07:38 +08:00
qqqys
96b30ee427
feat(cli): add baseline /doctor memory diagnostics (#4180)
* feat(cli): add baseline doctor memory diagnostics

* fix(cli): address doctor memory review feedback

* feat(cli): add doctor memory assessment

* feat(cli): support doctor memory heap snapshots

* feat(cli): add doctor memory sampling

* fix(cli): harden doctor memory heap snapshots

* fix(cli): harden doctor memory heap snapshots

* fix(cli): harden memory heap snapshot diagnostics

* fix(cli): harden doctor memory snapshots

* fix(cli): stabilize heap snapshot cleanup ordering

* fix(cli): harden heap snapshot cleanup

* test(cli): cover memory snapshot fallbacks

* fix(cli): harden doctor memory abort and disk checks
2026-05-16 17:19:50 +08:00
qqqys
372acf1444
feat(cli): argument hint + --auto completion for /rename (#4048)
* feat(cli): argument hint + --auto completion for /rename

Closes #4047.

The /rename command supports a structured --auto flag (let the fast
model generate a sentence-case title from the conversation), but
unlike /model — which advertises --fast via argumentHint and a
completion entry — /rename's flag was undocumented inline. Users had
to either run the command incorrectly or check the docs to learn
about --auto.

- argumentHint: '[--auto] [<name>]' so the completion menu shows the
  shape when the user types `/rename` and tabs.
- completion: returns null on empty / free-text input (don't shadow
  the user typing a title) and surfaces --auto when the partial arg
  is a prefix of it ('-', '--', '--a', '--au', '--auto'). Same shape
  as /model's --fast handling.

Free-text titles intentionally don't auto-complete — there's nothing
meaningful to suggest, and offering --auto on every keystroke would
feel like noise on `/rename my-feature`.

Tests:
- pins argumentHint shape
- empty partial → null
- '-' / '--' / '--a' / '--au' / '--auto' all return the --auto suggestion
- 'my-feature' / 'fix bug' / '-x' return null (free-text path)

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(core): fall back to text JSON when generateJson gets no tool call

generateJson registers schemas as a respond_in_schema function
declaration and walks parts[].functionCall for the result. When no
tool_choice is set (the OpenAI-compatible converter never sets one) and
the system prompt explicitly asks for text JSON — e.g. session-title
generation's "Return ONLY a JSON object..." — some models honor the
prompt and emit the answer as a plain text part instead of calling the
tool. The answer is semantically correct; we just weren't reading it.

This bottoms out in /rename --auto as "The fast model returned no
usable title" on qwen3.6-max-preview, and likely affects every other
generateJson caller (next-speaker checker, edit corrector, etc.) on
the same class of model.

Add a tolerant fallback: when no function call comes back, parse
getResponseText(result) — which already skips thought parts — with a
JSON-object extractor that strips optional ```json fences and reads
the outermost {...} block. Strictly additive; the function-call path
stays primary.

Closes #4057.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* refactor(cli): unify /rename and /rename --auto pipelines

Bare /rename (no args) used to call a private generateKebabTitle path
that asked the fast model (or main-model fallback) for a 2-4 word
kebab-case name via a plain text call. /rename --auto used the
schema-enforced tryGenerateSessionTitle path for a 3-7 word sentence-
case title. Two code paths, two prompts, two failure-message formats,
two sanitizers — with the kebab path consistently lagging on history
filtering, surrogate handling, and error specificity.

Collapse to a single fast-model schema-enforced pipeline. Both bare
/rename and /rename --auto now call tryGenerateSessionTitle and both
record titleSource: 'auto' on success. The --auto flag stays as an
explicit user-intent marker (preserves the existing argumentHint /
completion / parseArgs surface) but no longer diverges semantically.

Bare /rename now also hard-requires fastModel; users who relied on
the main-model fallback need to either /model --fast <name> or pass
a name explicitly (/rename <name>). The new failure message points
at both options.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(cli): clarify rename title failure

* test(core): cover loose json fallback

---------

Co-authored-by: Qwen-Coder <noreply@qwen.ai>
2026-05-16 16:47:15 +08:00
jinye
435f711e33
feat(cli): warn users that rewind is disabled in IDE mode (#4122)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
2026-05-15 20:27:37 +08:00
易良
df32345d05
fix(vscode-ide-companion): use existing editor group for diff instead of forcing a new one (#4130)
* fix(vscode-ide-companion): use existing editor group for diff instead of forcing a new one

When the chat webview is in the leftmost group, opening a diff previously
called ensureLeftGroupOfChatWebview() which forcibly created a new editor
group. This was disruptive UX — there is often an existing empty group
to the right that could be reused.

Change the fallback chain from "left neighbor → force-create → Beside"
to "left neighbor → right neighbor → Beside". Also apply the same fix
to the readonly file opener in FileMessageHandler.

* fix(vscode-ide-companion): address review feedback — explicit Beside fallback, shared scan helper, comment accuracy

- Add ?? vscode.ViewColumn.Beside to targetViewColumn declaration so the
  fallback is explicit even if the downstream usage is reached without it
- Extract findNeighborGroup helper to de-duplicate the near-identical scan
  loops in findLeftGroupOfChatWebview and findRightGroupOfChatWebview
- Update stale comment in FileMessageHandler to reflect that the readonly
  document may open in the right group, not only the left

* fix(vscode-ide-companion): remove dead ensureLeftGroupOfChatWebview, fix param naming, add tests

- Delete ensureLeftGroupOfChatWebview and waitForTabGroupsCondition which
  are no longer called by any code path
- Remove now-unused openChatCommand import
- Rename _cur → cur in findNeighborGroup callbacks (param was used, the
  underscore prefix was misleading)
- Add editorGroupUtils.test.ts with 12 unit tests for findLeft/findRight
- Add createAndOpenTempFile viewColumn tests to FileMessageHandler.test.ts
  covering left-neighbor, right-neighbor, and Beside fallback cases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(vscode-ide-companion): align Beside fallback placement in FileMessageHandler with diff-manager

Move vscode.ViewColumn.Beside fallback to the targetViewColumn declaration
so both diff-manager and FileMessageHandler follow the same pattern:
left ?? right ?? Beside at declaration, plain viewColumn at usage.

* fix(vscode-ide-companion): fix TS2322 type error in FileMessageHandler.test.ts

vscodeMock.window.tabGroups.all was initialized as plain [] which
TypeScript infers as never[], causing assignment errors in CI.
Add explicit type annotation to match the objects assigned in tests.

* fix(vscode-ide-companion): clarify Beside fallback comment — covers missing webview too

The fallback chain left ?? right ?? Beside also falls through to Beside
when the chat webview group is not found (both helpers return undefined).
Update comments in both diff-manager and FileMessageHandler.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-15 17:56:14 +08:00
jinye
dcf7681d65
feat(core,cli): add generic atomicWriteFile, wire into Write/Edit tools, upgrade @types/node (#4096)
* feat(core): add generic atomicWriteFile and wire into Write/Edit tools

The Write and Edit tools used bare fs.writeFile, risking half-written
corrupt files on crash or power loss. Both tools' source code contained
explicit TODOs noting atomic write as the fix.

- Add atomicWriteFile() supporting string/Buffer with flush (fsync),
  permission preservation, symlink resolution, and EXDEV fallback
- Wire StandardFileSystemService.writeTextFile() through atomicWriteFile
- Refactor atomicWriteJSON to delegate to atomicWriteFile (adds fsync)
- Deduplicate renameWithRetry from runtimeStatus.ts
- Add flush:true to writeWithBackupSync for settings writes
- Upgrade @types/node to ^22.0.0 (flush option type support)

Closes the TODO in write-file.ts:371-385 and edit.ts:487-497.
Ref: #4095 (Phase 1)

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(core): address review comments on atomicWriteFile

- Fix permission window: separate existingMode from desiredMode so
  mode is set during writeFile (not just chmod after), eliminating
  the brief window where tmp file has overly permissive defaults
- Fix broken symlink handling: use lstat+readlink instead of realpath
  to correctly resolve symlinks whose targets don't exist yet,
  preventing the symlink from being replaced by rename
- Add test for writing through a broken symlink

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(core): address wenshao review on atomicWriteFile

- Fix Windows bug: use path.isAbsolute() instead of startsWith('/')
- Hoist path import to top-level static import
- Resolve full symlink chains via loop (handles A→B→C), with
  ELOOP guard at 40 hops matching POSIX SYMLOOP_MAX
- Mask stat.mode with 0o7777 to strip file-type bits
- Document EXDEV fallback atomicity loss in JSDoc
- Add tests for relative symlinks and multi-level symlink chains

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(test): fix CI failures from atomic write changes

- edit.test.ts: mock writeTextFile instead of chmod 444 for write error
  test — atomic write creates tmp file in same dir, so readonly target
  no longer triggers a write error
- atomicFileWrite.test.ts: skip permission tests on Windows — chmod is
  a no-op and stat.mode always returns 0o666

* fix(core): address deepseek review on atomicWriteFile

- Add try/catch around chmod calls to handle FAT/exFAT filesystems
  where POSIX permissions are not supported
- Add explicit type annotation to lstats variable

* fix: restore version numbers to 0.15.11 after rebase

* fix(core): resolve relative symlinks through directory symlinks

resolveSymlinkChain used path.dirname() to resolve relative symlink
targets, which is purely string-based. When intermediate path
components are themselves directory symlinks, the result would be
wrong (e.g. /a/link/file → ../target resolves to /a/target instead
of the kernel-resolved /b/target).

Use fs.realpath() on the parent directory to get the kernel-resolved
base for relative-target resolution.

* fix(test): normalize path separators in directory symlink test

Windows readlink returns native separators (backslashes), causing
the directory-symlink test to fail on Windows CI. Wrap both sides
of the symlink-target comparison with path.normalize.

* refactor(core): dedupe write/chmod logic in atomicWriteFile

- Extract writeOptions construction and tryChmod helper, removing
  duplication between the main write path and the EXDEV fallback
- Document atomicWriteJSON's symlink-preservation behavior

Addresses deepseek review on PR #4096.
2026-05-15 17:52:50 +08:00
ChiGao
41bcdae7d8
fix(core): refresh systemInstruction in setTools() so progressive MCP tools reach the model (#4166)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(core): refresh systemInstruction in setTools() so progressive MCP tools reach the model

Under PR #3994's progressive MCP path, Config.initialize() runs
startChat() BEFORE MCP discovery starts, then kicks discovery off in the
background and re-runs setTools() once it settles. But setTools() only
updated chat.generationConfig.tools — not systemInstruction — and MCP
tools are shouldDefer=true, so they were filtered out of declarations
anyway. The prompt's "Deferred Tools" listing was frozen at the
built-in-only snapshot from the initial startChat(), and the model had
no signal that any MCP tool existed. Headless --prompt runs silently
regressed to built-ins (issue #4163); interactive mode had the same gap
but was masked by retries.

setTools() now rebuilds the system instruction with the up-to-date
deferred summary and re-binds it to the live chat. The eager-reveal
guard for "ToolSearch unavailable + deferred tools present" moves with
it so a freshly-arrived MCP tool in `--exclude-tools tool_search`
sessions still lands in declarations instead of disappearing silently.
Shared with startChat() / refreshSystemInstruction() via a new private
resolveDeferredToolsForSystemPrompt() helper so the three paths cannot
drift apart again.

The legacy synchronous path (QWEN_CODE_LEGACY_MCP_BLOCKING=1) was
incidentally correct because discovery happened before startChat(); it
remains correct.

Test plan:
- packages/core/src/core/client.test.ts — three new cases covering
  newly-arrived MCP tools, already-revealed filtering, and the
  no-ToolSearch eager-reveal path.
- Full client.test.ts (107 tests) green.
- tool-search / skill-manager / agent / mcp-client-manager / AppContainer
  test suites green (callers of setTools()).
- CI integration: integration-tests/cli/simple-mcp-server.test.ts is
  expected to pass on first try without QWEN_CODE_LEGACY_MCP_BLOCKING.

Fixes #4163

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(core): lock in SessionStart preservation across setTools refresh

Adds the regression test chiga0 asked for in the PR #4166 review:
proves that setTools()'s setSystemInstruction-then-reapply pattern keeps
the SessionStart hook's additionalContext intact, so progressive-MCP
refreshes (AppContainer batch flush + the trailing setTools after
waitForMcpReady) don't silently strip hook context from the system
instruction.

Generated by claude-opus-4-7

Co-authored-by: Claude <claude-opus-4-7@anthropic.com>

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Claude <claude-opus-4-7@anthropic.com>
2026-05-15 17:26:22 +08:00
ChiGao
9d20536343
perf(cli): code-split lowlight to cut startup V8 parse cost (#4070)
* perf(cli): code-split lowlight to cut startup V8 parse cost

Move the syntax-highlight engine out of the synchronously-parsed cli.js
entry into a separately-emitted chunk and load it via dynamic import on
the first code-block render. Until the chunk arrives, code blocks render
as plain text; the next React commit of the surrounding subtree picks up
the highlighted version, so users never see incorrect highlighting –
just an imperceptibly later transition for the very first code block.

Mechanics:
- esbuild config: switch entry to outdir + splitting:true so that
  `await import('lowlight')` produces an actual on-disk chunk that's
  only parsed by V8 when first needed.
- esbuild-shims: rename injected __dirname/__filename to qwen-prefixed
  symbols + use `define` to redirect free references. Previous inject
  collided with vendored libraries (yargs) that ship their own
  `var __dirname` ESM-compat polyfill once splitting flattens chunks.
- prepare-package: include the new chunks/ directory in the published
  package's files list.
- CodeColorizer: keep the public colorize{Code,Line} signatures and HAST
  rendering identical; on first call when the chunk hasn't loaded it
  returns the plain line and fires the dynamic import via a tiny
  standalone loader module.
- lowlightLoader (new): isolates the lazy-load surface to a module with
  zero transitive imports (no themeManager, settings, or core). This
  lets test-setup prime the cache without dragging the whole UI module
  graph into every test file, which was observed to perturb theme and
  settings test outcomes when CodeColorizer was imported directly.
- test-setup: await loadLowlight() once via the standalone loader so
  synchronous snapshot tests see the highlighted output deterministically.

Measurements (real $HOME, n=15 interleaved A/B vs main HEAD, macOS):

| Metric             | Before (mean±sd ms) | After (mean±sd ms) | Δ        | t      | p        |
| ------------------ | ------------------- | ------------------ | -------- | ------ | -------- |
| firstByte (wall)   | 1633.5 ± 88.7       | 1475.8 ± 73.3      | -157.7   | 5.31   | 1.33e-5  |
| idle (wall)        | 2048.7 ± 93.6       | 1902.3 ± 80.2      | -146.3   | 4.60   | 8.71e-5  |
| cli.js size        | 25 MB               | 6.9 MB             | -18.1 MB | —      | —        |

Both metrics clear the +50ms-or-10% Welch's t-test bar by an order of
magnitude. cli.js drops 72%; total payload (cli.js + chunks/) is
similar but only cli.js is parsed at module-eval time, which is the
phase that dominates the user-visible startup gap.

How to validate:
  npm run bundle
  ls dist/                         # cli.js + chunks/lowlight-*.js
  node dist/cli.js -y              # interactive UI still renders

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): resolve chunk-relative sibling paths under esbuild splitting

With `splitting: true`, esbuild hoists modules with shared dependencies
into `dist/chunks/`. Three modules derived runtime paths from
`import.meta.url` assuming they were co-located with `cli.js`; once
hoisted, `path.dirname(fileURLToPath(import.meta.url))` resolved to
`dist/chunks/` and sibling-asset lookups silently missed:

- `skill-manager.ts`: bundledSkillsDir → `dist/chunks/bundled` (actual
  `dist/bundled/`). The `existsSync` guard swallowed the miss, dropping
  all four bundled skills (`/review`, `/qc-helper`, `/batch`, `/loop`)
  with no user-visible signal.
- `ripgrepUtils.ts`: `getBuiltinRipgrep()` → `dist/chunks/vendor/...`.
  Falls back to system rg if installed, otherwise null on minimal
  hosts — degrading grep to the slow internal scanner.
- `i18n/index.ts`: `getBuiltinLocalesDir()` → `dist/chunks/locales`.
  User-visible behavior survives via the static glob import in
  `tryImportBundledTranslations`, but the loose-on-disk override path
  is dead.

Each module now strips a trailing `chunks` segment when present, so
the lookup resolves under `dist/`. In source / transpiled modes the
basename is never `chunks`, so the fallback is a no-op.

Also:
- Add `chunks` to `DIST_REQUIRED_PATHS` in `create-standalone-package.js`
  so a regressed bundle that produces only `cli.js` fails the
  pre-packaging check instead of shipping a broken archive.
- Expand `esbuild-shims.js` header so future contributors understand
  that `__qwen_filename` / `__qwen_dirname` always resolve to the
  shim's chunk file (dist/chunks/) and that sibling-asset lookups
  must strip the `chunks` segment.

Reported by claude-opus-4-7 via Qwen Code /qreview on #4070.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* perf(cli): prefetch lowlight from AppContainer + harden loader

Three follow-ups to the lowlight code-split:

- AppContainer fires `loadLowlight()` from a mount effect so the dynamic
  import is already in flight before any code block needs colorizing.
  Without this, code blocks committed to ink's append-only `<Static>`
  region before the import resolves stay plain text for the rest of
  the session — Static can only be re-rendered via `refreshStatic`,
  which is not wired to lowlight load completion. Common reachable
  paths: short `--prompt -p` runs that finalize quickly, Ctrl+C-
  cancelled first turns, and the first-paint history replay on
  `--resume`. The startup parse-cost win is preserved (V8 still
  parses off the critical path).

- `lowlightLoader.ts` latches the first import failure so subsequent
  calls short-circuit to a rejected promise instead of re-attempting
  `import('lowlight')` on every keystroke. The colorizer already falls
  back to plain text on miss; recovery requires a fresh process anyway.

- `test-setup.ts` wraps the top-level `await loadLowlight()` in
  try/catch. A transient import failure no longer crashes the entire
  vitest run — tests that hit a code block render the plain-text
  fallback and surface a warning.

- `CodeColorizer.tsx` header comment updated to point at the
  AppContainer prefetch instead of claiming first-paint always sees
  a loaded instance.

Reported by DeepSeek/deepseek-v4-pro and claude-opus-4-7 via Qwen Code
/review and /qreview on #4070.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(bundle): extract resolveBundleDir helper, apply to extensions/new

Centralises the `chunks/` strip pattern that three sites
(`i18n/index.ts`, `skills/skill-manager.ts`, `utils/ripgrepUtils.ts`)
each duplicated after the round-3 fix in d581da04d. The implicit
coupling to `esbuild.config.js`'s `chunkNames: 'chunks/[name]-[hash]'`
now lives in a single helper (`packages/core/src/utils/bundlePaths.ts`),
so a future rename only needs updating in one place.

Also applies the same anchor to `commands/extensions/new.ts:EXAMPLES_PATH`.
That module is currently bundled into `cli.js` (so the strip is a no-op
today), but `qwen extensions new --help` always reads the examples
directory in its yargs `builder` — confirmed against the built bundle
that the lookup hits `dist/examples/` (sibling of `cli.js`). Using the
helper future-proofs against esbuild later hoisting the module into a
shared chunk, where the bare `__dirname`/`import.meta.url` lookup would
silently break the command for every end user.

While here, surface lowlight-load failures from `AppContainer`'s
prefetch effect to the debug channel (`debugLogger.warn`) instead of
swallowing them silently. The loader already latches failures
permanently, so this fires at most once per session; `CodeColorizer`
continues to fall back to plain text on miss, so user-visible behaviour
is unchanged.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(bundle): restore __filename shadow in ripgrepUtils; harden lowlight loader

Round-4 review (wenshao 2026-05-13 13:12) flagged five issues in the
recent code-split work. This commit addresses all of them.

CRITICAL — `packages/core/src/utils/ripgrepUtils.ts`: the round-3
`resolveBundleDir` refactor removed the local `__filename` declaration
but `getBuiltinRipgrep` still references bare `__filename` to decide
how many `..` segments to walk. In `npm run dev` (tsx, ESM) `__filename`
is undefined so the function throws `ReferenceError`. In the bundle
esbuild's `define` rewrites it to `__qwen_filename` (the shim chunk
path), which is the wrong string but happens to short-circuit to
`levelsUp = 0` — accidentally correct only because the chunk-path
string never contains `path.join('src', 'utils')`. Reproduced via tsx:
`__filename is not defined`; fixed by re-introducing the explicit
local shadow plus a comment explaining why centralising both helpers
into `resolveBundleDir` cannot replace the per-file shadow.

`packages/cli/src/ui/utils/lowlightLoader.ts`: the previous permanent
`lowlightFailed` latch left syntax highlighting dead for the entire
process lifetime on transient errors (EMFILE, antivirus locks,
slow-disk-after-wake). Replaced with a 30-second cooldown — within the
window subsequent calls return the cached rejection synchronously
(keeps the per-render short-circuit that protects against
permanently-broken installs); after the cooldown the next call retries
the dynamic import. Exposes `isLowlightCoolingDown()` so render-hot
callers can also skip duplicate failure logging.

`packages/cli/src/ui/utils/CodeColorizer.tsx`: hoisted
`loadLowlight()` + log out of the per-line render loop into a single
`ensureLowlightLoading()` call at the top of `colorizeCode`. In the
failure case this collapses hundreds of duplicate debug entries (one
per line) to one per block. The instance is now passed down to
`highlightAndRenderLine` as a parameter.

`packages/core/src/utils/bundlePaths.ts` + `esbuild.config.js`:
exposed `BUNDLE_CHUNK_DIR = 'chunks'` as a named constant and updated
`esbuild.config.js` to interpolate the same name into `chunkNames`
(plus an explicit "MUST stay in sync" comment). Renaming on one side
without the other now stands out at review time. Also expanded the
`define` comment with a contributor-facing warning describing exactly
why bare `__dirname` / `__filename` in source files becomes the shim
chunk path, and pointing future contributors at the
`fileURLToPath(import.meta.url)` shadow pattern (and
`resolveBundleDir` for sibling-asset lookups).

Verified:
- typecheck (all 4 workspaces): clean
- packages/core tests: 7747 passing (no regressions)
- packages/cli tests: only the pre-existing `useAtCompletion.test.ts`
  filesystem-order failures remain (confirmed against `git stash`)
- `npm run bundle` succeeds; `node dist/cli.js --version` returns
  `0.15.10`; `node dist/cli.js --help` renders normally
- `npx tsx <call getBuiltinRipgrep>` now returns the vendored path
  instead of throwing `ReferenceError`

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(bundle): validate lowlight API shape; sync doc-comment drift; add tests

- lowlightLoader: validate runtime shape of createLowlight() before the
  `as Lowlight` cast so an upstream API rename routes through the cooldown
  latch instead of silently degrading every code block to plain text.
- bundlePaths: correct doc comment — esbuild.config.js maintains its own
  `BUNDLE_CHUNK_DIR` constant rather than importing this one (it runs
  before any TS compile step).
- AppContainer: update prefetch-failure comment to reference the cooldown
  symbols (`LOWLIGHT_RETRY_COOLDOWN_MS` / `lowlightLastFailureAt`) that
  replaced the removed `lowlightFailed` latch.
- New unit tests covering the lowlightLoader state machine (success,
  in-flight dedup, shape mismatch, cooldown skip, post-cooldown retry)
  and `resolveBundleDir`'s strip-only-on-exact-match contract.

* test(bundlePaths): use path.resolve for Windows-compatible absolute paths

CI failure on Windows: the new `resolveBundleDir` tests built expected
values with `path.join(path.sep, ...)` (e.g. `\tmp\dist`), but
`pathToFileURL` resolves drive-less paths against the current drive
on Windows. The URL -> `fileURLToPath` round-trip returned `D:\tmp\dist`,
while the expectation stayed `\tmp\dist`, tripping all three new
assertions.

Switched both the URL source and the expected value to a single
`path.resolve(path.sep, ...)` anchor per test so both sides absorb
whatever the platform considers absolute. POSIX behaviour is unchanged
(`/tmp/dist` -> `/tmp/dist`).

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-15 17:26:18 +08:00
DennisYu07
57282ebb7d
feat(hooks): add prompt hook type with LLM evaluation support (#3388)
* implement prompt hook

* resolve comment

* resolve comment

* resolve comment

* resolve comment

* fix unit test
2026-05-15 17:13:05 +08:00