mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-24 05:26:21 +00:00
2823 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f4e01a409e |
fix(auth): address PR #4287 review (critical + suggestion)
vscode AuthMessageHandler (Critical): - Add the missing protocol-selection step so custom-provider users can pick Anthropic/Gemini instead of being silently locked to OpenAI. - Validate free-form base URL with the same /^https?:\/\// check the CLI uses; reject file:/javascript: schemes. vscode AuthMessageHandler (Suggestion): - Stop filtering separator entries from the provider QuickPick so groups (Alibaba Cloud / Third Party / Custom) actually show as headers instead of a flat list. - Treat a null authInteractiveHandler as an error: surface an authError + cancellation notification instead of silently dropping the user's input. - Call notifyAuthCancelled when validateApiKey rejects so the webview state resets and the user can retry. core/providers/presets/openrouter.ts (Critical): - Replace the substring includes() in ownsModel with a URL-hostname match so paths like https://api.example.com/openrouter.ai/v1 stop being misidentified as OpenRouter models (and getting removed on re-install). vscode/services/settingsWriter.ts (Critical): - stripTrailingCommas() so JSONC files with trailing commas (VSCode's default style) parse instead of silently returning {} and then overwriting the entire settings file. - readSettings() distinguishes ENOENT (return {}) from parse errors (log + rethrow) so a malformed file never gets clobbered. - writeSettings() writes through a temp file + fs.renameSync atomic rename, eliminating the half-written file window on EACCES / disk-full / crash. - setValue() refuses to overwrite a scalar at an intermediate path segment (would have silently destroyed e.g. {"env": "legacy-string"}). core/providers/install.ts (Suggestion): - Move settings.backup?.() inside the try block so a backup failure still triggers the env-rollback path in catch. cli/config/loadedSettingsAdapter.ts (Suggestion): - Add the same UNSAFE_KEY_PARTS guard the vscode adapter has, so __proto__/constructor/prototype segments are rejected before reaching the underlying setNestedPropertySafe walker. Defense in depth: not exploitable today but the utility has no built-in guard. vscode/webview/providers/WebViewProvider.ts (Suggestion): - Hoist buildInstallPlan / applyProviderInstallPlanToFile to static imports (both modules already top-level imported); drops two per-call await import() round-trips. cli/utils/doctorChecks.ts (Suggestion): - Whitespace nit before the comma in the qwen-code-core import. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
18b35b48ec |
i18n(cli): translate "Connect an LLM provider" in all locales
Strict-parity locales (zh, zh-TW) require every built-in command description to be translated; the renamed /auth description was falling back to English and breaking the must-translate test. Add translations for zh / zh-TW (required) and refresh the other seven locales (en, ru, de, ja, fr, ca, pt) so the old "Configure authentication information for login" key is removed everywhere rather than left as a dangling dictionary entry. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
1b0b7f93a8 |
refactor(cli): rename /auth description to "Connect an LLM provider"
The old description ("Configure authentication information for login")
implied a Qwen-account login. After the /auth refactor it's really
about picking an LLM provider and entering credentials, so the menu
entry should say that.
Also add 'connect' as an alt-name alongside the existing 'login' so
users can type /connect when 'auth' feels wrong. Keep 'login' for
muscle-memory compatibility.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
||
|
|
ed08f7296b |
fix(auth): show base URL default as placeholder, not prefilled value
In Custom Provider Step 2/6 (and on protocol switch), the base URL input started with the protocol's default URL pre-filled. Users who wanted a non-default endpoint had to manually clear the field first. Switch to placeholder semantics: the input starts empty, the default URL is shown as a hint, and submitting blank falls back to that default (then writes it back to baseUrl so downstream steps see a real value). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
0d8fe738f5 |
fix(auth): default Audio modality to off in provider advanced config
In the /auth Custom Provider advanced-config step, "Enable modality" should default to Image + Video only. Audio was on by default, which implied the model accepts audio input even though most providers people configure here don't. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
efcfce5b6b |
Merge remote-tracking branch 'origin/main' into refactor/unify-provider-config-to-core
# Conflicts: # packages/cli/src/ui/components/ManageModelsDialog.test.tsx # packages/cli/src/ui/components/ManageModelsDialog.tsx # packages/cli/src/utils/apiPreconnect.ts # packages/core/src/providers/all-providers.ts |
||
|
|
6804a18f01 |
refactor(auth): unify applyProviderInstallPlan in core, drop cli/auth
CLI and vscode now share core's applyProviderInstallPlan instead of keeping two parallel implementations. The CLI-only env rollback (snapshot process.env, restore on error) is folded into the core version so vscode also benefits from it. CLI ships a LoadedSettingsAdapter that maps LoadedSettings to core's ProviderSettingsAdapter contract. Backup/restore is layered: write a .orig file, structuredClone settings + originalSettings, then recomputeMerged() on restore — same guarantees as before, just routed through the adapter. Tests for the install logic are migrated to core and rewritten against the adapter mock (more focused than the previous LoadedSettings/Config mocks). packages/cli/src/auth/ is gone entirely. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
423e453069 |
refactor(cli): drop OpenRouter OAuth + /manage-models, simplify /auth
OpenRouter now uses the standard API-key flow under "Third-party Providers" (issue #4108). The whole OpenRouter OAuth implementation (PKCE, callback server, model auto-install) and the /manage-models command (only OpenRouter was wired in; /auth Step 2 already covers model selection) are removed. /auth is renamed around the "Connect a Provider" mental model: - Dialog title is now "Connect a Provider"; the OAuth main entry is gone - handleAuthSelect (mixed close + auth trigger) is split into a single-purpose closeAuthDialog; legacy wrappers (handleSubscriptionPlanSubmit, handleApiKeyProviderSubmit, handleCustomApiKeySubmit, ...) are dropped in favor of the unified handleProviderSubmit Core: openRouterProvider switches to authMethod='input', uiGroup='third-party', ships with two recommended free models, and is reordered to the end of the third-party list to keep DeepSeek as the default highlight. Net diff: 34 files, +124 / -3835. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
33b2e0dccc
|
fix(serve): normalize Windows path separators in workspace file read responses (#4279)
`workspaceRelative` returned the platform-native separator from `path.relative`, leaking backslashes into `/file`, `/stat`, `/list`, and `/glob` response paths on Windows. Surfaced as a Windows-only CI failure in the `GET /glob > scopes glob matches to cwd` test (`['sub\\inside.ts']` vs expected `['sub/inside.ts']`). Always emit POSIX-style separators so SDK consumers see the same shape across platforms. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) |
||
|
|
b208f34455
|
feat(serve): add /demo debug page for qwen serve daemon (#4132)
* feat(serve): add /demo debug page for qwen serve daemon Add a self-contained HTML debug page at GET /demo that provides a browser-based UI for exercising all daemon routes: session create/attach, prompt send/cancel, SSE event streaming, model switching, permission voting, and health/capabilities checks. Also add a same-origin exemption middleware (before the CORS deny layer) so browser fetch calls from the demo page pass through while external Origins remain blocked. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(serve): address CR feedback for /demo page security and robustness - Fix XSS: build permission buttons with DOM APIs instead of innerHTML - Fix SSE: move currentEvent outside read loop for cross-chunk frames - Fix SSE: handle stream end (flush trailing buffer, update UI status) - Security: move /demo route behind hostAllowlist and bearerAuth guards - Security: add host.docker.internal to same-origin Origin allowlist - Add Auth Token input and include Authorization header in API/SSE calls - Add try/catch to /demo route handler with writeStderrLine logging - Check API result before removing permission card from UI - Add 7 tests for /demo route and Origin-stripping middleware * fix(serve): move /demo before bearerAuth so browsers can reach it Browsers cannot attach Authorization headers on address-bar navigation, so /demo behind bearerAuth was unreachable when --token was set. Move the /demo route after CORS + Host allowlist but before bearerAuth. The static HTML shell contains no secrets; all API/SSE routes remain bearer-protected and the in-page token input authenticates them. * feat(serve): show 401 token hint on demo page When an API call returns 401 Unauthorized, highlight the Auth Token input field with a yellow border and display a hint message guiding the user to enter their bearer token. Applies to both API calls and SSE connections. The hint auto-dismisses after 6 seconds. * fix(serve): address round-2 CR feedback for /demo security - Loopback-gate /demo like /health: pre-auth on loopback, post-auth on non-loopback. Prevents unauthenticated access on public interfaces. - Add X-Frame-Options: DENY + CSP frame-ancestors to /demo response to prevent clickjacking via cross-origin iframe embedding. - Cache selfOrigins Set in Origin-stripping middleware (rebuild only when port changes) instead of allocating per-request. - Clear pendingPerms + reset currentAssistantBubble on session switch to prevent stale permission cards from the previous session. - Update tests: loopback vs non-loopback /demo auth, anti-clickjacking headers, rename CORS test, add localhost and [::1] origin coverage. * fix(serve/demo): address CR round-2 feedback for demo page UX - Replace innerHTML with DOM APIs in addLog() to prevent XSS - Add MAX_LOG_ENTRIES=500 pruning to prevent unbounded DOM growth - Add concurrent-send guard (promptInFlight) to prevent double submits - Show error feedback in Chat tab when sendPrompt or createSession fails - Disable prompt controls on SSE failure (both catch and non-OK paths) - Add toolCall context detail (command/input/path/diff) to permission cards --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
52d2850c7f
|
feat(serve): safe workspace file read routes (#4175 PR 19) (#4269)
* refactor(serve/fs): glob audit hashes workspace + emits pattern Closes PR #4250 follow-up #4. Hashing the per-call cwd for glob audit produced a different pathHash for every subdirectory glob without giving operators any actionable difference (raw paths are privacy-gated). Replace the hash basis with the bound workspace itself and surface the literal pattern on a new schema field, so every glob row carries a stable workspace marker and a per-call pattern. The pattern field also fires on parse_error denials (path-escape patterns, non-relative patterns) so audit consumers debugging a production glob rejection can see the exact rejected pattern without needing QWEN_AUDIT_RAW_PATHS=1. * feat(serve): safe workspace file read routes (#4175 PR 19) Add four read-only HTTP routes that consume PR 18's per-request WorkspaceFileSystem boundary: - GET /file?path=... text content + meta (encoding/BOM/lineEnding) - GET /list?path=... directory entries (name/kind/ignored) - GET /glob?pattern=... workspace-relative match paths - GET /stat?path=... file/directory metadata The routes share one error envelope (sendFsError) that maps FsError.kind through the boundary's existing DEFAULT_STATUS_BY_KIND table to a typed JSON response. All four 200 responses set Cache-Control: no-store and X-Content-Type-Options: nosniff so a browser-adjacent client cannot cache or sniff source content. Routes are advertised under a single workspace_file_read capability tag — the four endpoints share the same backing boundary and the same failure shape, so per-route tags would force four simultaneous registry entries with no operator-meaningful difference between them. Mutating routes will ship in PR 20 under their own workspace_file_write tag. Trust gate is unchanged: read intents pass on untrusted workspaces per PR 18's policy.ts. Auth follows the global bearer flow only; read routes never run mutate(), since none of them mutate state. * feat(serve): runQwenServe injects fsFactory + emit pipeline Closes PR #4250 follow-up #2. runQwenServe now constructs a WorkspaceFileSystem factory from the bound workspace, threads its emit hook through to the read routes, and exposes the trust snapshot via deps.trustedWorkspace. Test additions pin the wiring contract: - audit events emitted on success / denial flow back through the test-supplied fsAuditEmit hook - deps.fsFactory override is honored (built-in default does not silently shadow injection) - trust snapshot defaults to true (operator-chosen workspace) - trust=false routes through to the boundary and trips untrusted_workspace on write intents Default emit stays a stderr warning so a wiring regression that drops events remains visible. PR 21's SSE fan-out will replace the default with a workspace-scoped event channel. * fixup(serve): address PR #4269 round-1 review feedback Closes 8 findings from Copilot inline review + Codex review on PR #4269 (5 P0, 3 P1): P0 (correctness / privacy / operations) - runQwenServe.ts: throttle the default fsAuditEmit by reusing the exported `createDefaultFsAuditEmit` from server.ts. The earlier per-event `writeStderrLine` would print one line for every /file/list/glob/stat audit event under normal traffic. Now warns once + every 100th drop with payload context, so a wiring regression is still visible without flooding logs. (Copilot runQwenServe.ts:316; Codex runQwenServe.ts:305) - routes/workspaceFileRead.ts: probe glob with maxResults+1 and trim, so `truncated` reflects whether the boundary actually had more matches. Earlier `length === maxResults` heuristic false-positived when the workspace happened to hold exactly N matches. (Copilot workspaceFileRead.ts:399) - routes/workspaceFileRead.ts: glob `relMatches` now flows through the shared `workspaceRelative` helper. Root match (`pattern=.`) renders as "." rather than the empty string `path.relative` returns; helper also covers the boundWorkspace-undefined edge case so the route no longer carries its own fallback branch. (Copilot workspaceFileRead.ts:388; review summary HIGH-1) - fs/audit.ts: `pattern` field now rides on the same privacy gate as `relPath` / `message`. Glob patterns commonly carry workspace-relative or absolute path fragments (`src/secrets/*.env`, rejected `/Users/alice/ws/**`), so emitting them in privacy mode bypassed the same redaction the other path-bearing fields honor. Operators wanting full forensic context opt in via QWEN_AUDIT_RAW_PATHS=1. (Codex audit.ts:249) - routes/workspaceFileRead.ts: cwd resolves with intent='list' rather than 'glob'. The orchestrator's `recordAndWrap` auto-derives `data.pattern` from `intent === 'glob'`, which turned cwd-resolution failures into rows where the cwd string masqueraded as the glob pattern (`?cwd=../outside` → `pattern: ../outside` in audit). Switching to 'list' is the correct semantic shape (cwd is a directory we intend to walk) with identical trust + path-resolution behavior. (Codex workspaceFileSystem.ts:941) P1 (cosmetic / comment accuracy) - server.test.ts: `honors deps.fsFactory override` test comment rewritten to match the actual failure mode (a regression would 404 on a.txt, not 200 against package.json). (Copilot server.test.ts:3219) - routes/workspaceFileRead.ts: `limit` error message uses the MAX_LIST_ENTRIES constant instead of the literal 2000. (review summary MEDIUM) - fs/audit.ts: expanded the JSDoc explaining why the AuditPublisher request types Omit four fields and pass `pattern` through. (review summary MEDIUM) Test additions / adjustments - audit.test.ts: split the existing pattern tests into raw-paths and privacy-default cases; added two new privacy-mode assertions that strip pattern under default config. - workspaceFileSystem.test.ts: harness accepts `includeRawPaths`; glob audit suite runs with raw paths to observe `pattern`; new `glob audit privacy default` suite asserts pattern + relPath are stripped without the env opt-in. - workspaceFileRead.test.ts: new GET /glob cases for the truncated edge case (count == maxResults → false; count > maxResults → true) and root-match normalization. Not adopted (with rationale) - review summary HIGH-2 (glob pathHash uses boundWorkspace): this is the deliberate follow-up #4 contract from PR 18; pattern is the per-call signal, pathHash is the workspace marker. - review summary MEDIUM-1 (parseIntInRange three-state return): matches `parseMaxQueuedQuery` in server.ts; consistency wins. - review summary LOW-1/2/3 (capabilities comment length, CSP header, reverse truncated:false assertion): rationale already documented in code, CSP belongs in a hardening PR, the reverse assertion already exists. 518/518 serve tests pass; typecheck + eslint clean within src/serve/. * fix(serve): address workspace file read review Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): tighten workspace file read review followups Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
8f9b979ec0
|
test: reduce wait-dependent UI test delays (#3987)
* test: reduce wait-dependent UI test delays * test: address UI test review feedback * test: address remaining UI test review feedback - Replace runPendingRetryTimer's polling loop with vi.advanceTimersToNextTimerAsync to decouple from connect()'s internal async structure. - Document that newSessionWithRetry's 300ms auth-delay timer is unreachable under the current mocks so future tests advance it via the same helper instead of restoring shouldAdvanceTime. - Stop swallowing runPendingRetryTimer errors behind connectPromise.catch(e => e); attach a noop catch only to mute the unhandled-rejection guard and assert directly on the original promise. - Comment why flush() yields twice (Ink 7 + React 19 split a render across two microtask ticks) so it is not "optimized" to a single yield. - Move the trailing unmount() into the finally block in the shell-history placeholder test so an assertion failure does not leak the Ink instance. |
||
|
|
103090669e
|
feat(serve): workspace memory and agents CRUD (#4175 Wave 4 PR 16) (#4249)
* feat(serve): workspace memory and agents CRUD (#4175 Wave 4 PR 16)
Adds the first Wave 4 mutation route surface: workspace-scoped memory
and subagent CRUD over HTTP. Remote clients (TUI / channels / web /
IDE adapters) can now list, read, create, update, and delete subagent
definitions and read / append / replace QWEN.md without disturbing
session state.
Routes:
- GET /workspace/memory (read-only snapshot)
- POST /workspace/memory (append/replace, strict-gated)
- GET /workspace/agents (list project + user + builtin)
- POST /workspace/agents (create-only; 409 on collision)
- GET /workspace/agents/:agentType (full detail incl. systemPrompt)
- POST /workspace/agents/:agentType (update; 403 read-only on builtin)
- DELETE /workspace/agents/:agentType (idempotent for SDK callers)
Mutation paths use mutate({ strict: true }) from PR 15 so they refuse
unauthenticated requests even on no-token loopback defaults. Workspace
mutations validate X-Qwen-Client-Id against bridge.knownClientIds() and
stamp originatorClientId on emitted events.
Capability tags added: workspace_memory, workspace_agents.
New typed events fanned out via bridge.publishWorkspaceEvent (best-
effort to every active session bus; read-after-write is the contract):
- memory_changed { scope, filePath, mode, bytesWritten }
- agent_changed { change, name, level }
writeContextFile.ts is the new core helper that resolves
QWEN.md placement (workspace vs ~/.qwen) and append-vs-replace
semantics. Whitespace-only appends short-circuit before fs.writeFile,
so a no-op POST does not bump mtime or fan out a misleading event.
SubagentManager is wrapped with a CRUD-scoped Config stub via Proxy:
only getSdkMode / getProjectRoot / getActiveExtensions are stubbed
(verified against subagent-manager.ts; getToolRegistry is execution-
path only). Any future Config method touched on a CRUD path throws
immediately so dependency creep is visible.
Auto-memory CRUD, persistent audit log, and the EACCES → NOT_FOUND
unlink mapping in core SubagentManager.deleteSubagent are explicit
follow-ups (PR 16.5 / PR 24 / separate fix).
Validation:
- typecheck: cli + sdk-typescript clean
- vitest: serve 348/348, writeContextFile 10/10, SDK 335/335
- eslint: clean
* fix(serve): address Codex P2 review on PR 16 (#4175 Wave 4 PR 16 follow-up)
Three correctness issues Codex flagged on the just-shipped workspace
memory + agents CRUD surface:
1. Concurrent POST /workspace/memory append no longer loses writes.
Two simultaneous appends would each read the same existing file,
compose new content in JS memory, then race the fs.writeFile —
the later write silently overwrote the earlier appended entry.
Add a per-resolved-path Mutex map (mirroring jsonl-utils.ts's
fileLocks pattern) and wrap the entire read-compose-write
sequence in runExclusive.
2. GET /workspace/agents now reflects out-of-band file changes.
SubagentManager.listSubagents() default served the in-memory cache;
developer / IDE adapter edits to .qwen/agents/*.md never appeared
even though GET /workspace/agents/:agentType always reads disk.
Pass { force: true } so the LIST route walks disk every call,
matching the detail route's "filesystem is the source of truth"
contract.
3. Reject builtin agent names on POST /workspace/agents to prevent
undeleteable shadow files. A client could write a project-level
agent named "general-purpose" — list/load resolved the shadow
first, but SubagentManager.deleteSubagent's name-based builtin
guard (subagent-manager.ts:302) rejected DELETE forever. Add a
BuiltinAgentRegistry.isBuiltinAgent check in parseAgentConfig
so the conflict surfaces at create time instead of trapping the
file beyond the API. The check is case-insensitive, matching the
resolver's case-insensitive cascade.
New tests:
- writeContextFile.test.ts: 10 parallel appends, all 10 entries
must survive in the final file (would fail without the mutex).
- workspaceAgents.test.ts: GET /workspace/agents observes a
freshly-written agent file on the second call (force-refresh
proof); POST with name="general-purpose" returns 422 + the
case-insensitive variant "explore" too.
Validation:
- typecheck: cli + sdk-typescript clean
- vitest: serve 351/351 (was 348, +3 new), writeContextFile 11/11
- eslint: clean
* fix(serve): apply round-1 review fold-in 2a (HIGH + CodeQL) on PR 16
Round-1 inline review (#4249) flagged ~28 items across Copilot,
wenshao, and CodeQL. This commit lands the HIGH-severity correctness
fixes plus the two CodeQL polynomial-regex warnings.
Validation tighten — `parseAgentConfig` + `parseAgentUpdates`:
- Trim leading/trailing whitespace on `name` before passing to
SubagentManager. `" tester "` would otherwise create a frontmatter
name with spaces that case-insensitive lookups can never find.
- Fail-closed (422 invalid_config) on present-but-wrong-type optional
scalars: `model`, `color`, `approvalMode`, `background`. Previously
malformed values silently dropped through validation, masking
client-serialization bugs.
- Validate `approvalMode` against the `APPROVAL_MODES` enum on both
create and update; an unknown value used to 201 with the field
silently omitted from the saved file.
- `runConfig` is now whitelist-sanitized to `{ max_time_minutes,
max_turns }` only; unknown keys are dropped, malformed values
return 422. Previously the whole input object was persisted
verbatim into YAML frontmatter.
- `?scope=` query is fail-closed for repeated values
(`?scope=workspace&scope=global`) — Express parses these as arrays
which the previous `typeof === 'string'` check silently treated as
absent, broadening DELETE/UPDATE semantics from one level to both.
- Empty update body returns 400 invalid_config (previously rewrote
the file + emitted a misleading `agent_changed` event).
- No-op updates (every supplied field already matches `existing`)
return 200 + `changed: false` and SKIP the file rewrite + event
fan-out.
Memory write helper — `writeContextFile.ts`:
- Move whitespace-only no-op detection BEFORE `fs.mkdir`. Without
this, an empty POST still created the parent directory and bumped
its mtime even though `changed: false` was reported.
- Replace two polynomial regex patterns flagged by CodeQL
(`/^\s+|\s+$/g` and `/^\n+|\n+$/g`) with hand-rolled `while` loops.
Same pattern auth.ts:120-125 already uses for the same CodeQL rule.
SDK — `DaemonClient.ts` + `types.ts`:
- `DaemonWriteMemoryResult` gains optional `changed?: boolean` so
typed callers can suppress redundant cache invalidation on no-op
appends. Optional for forward-compat with daemons that predate the
field — undefined treats as "changed: true" (legacy contract).
- `deleteWorkspaceAgent` only swallows 404 when the body's `code`
is `agent_not_found`. A bare 404 (older daemon, misrouted proxy,
generic gateway page) now throws — previously the SDK silently
reported success even when the request never reached a route that
understands workspace agents.
- `updateWorkspaceAgent` adds an optional `scope` parameter
mirroring `deleteWorkspaceAgent`, so callers can target the user-
level definition when a project-level agent shadows it.
Validation:
- typecheck: cli + sdk-typescript clean
- vitest: serve 357/357 + writeContextFile 12/12 = 369/369 passing
(was 362; +7 new)
- eslint: clean
Explicitly NOT applying (out of scope per issue #4175 PR 16
review-resolution policy):
- Copilot's "strict gate after body parser" finding — already
documented as PR 15 review-resolved tradeoff at auth.ts:256-269.
* fix(serve): apply round-1 review fold-in 2b (MEDIUM + tests) on PR 16
MEDIUM hardening:
- Fix the JSDoc on `collectWorkspaceMemoryStatus` to match the
workspace-root-only discovery the implementation actually does
today. The 32-iteration upward walk is reserved for a future
hierarchical mode but breaks after iteration 1 in v1.
- Lower the depth limit on `walkWorkspaceForMemory` from 32 → 12.
Realistic project depth sits well below 8; 12 leaves headroom
without amplifying blast radius from symlink cycles.
- Daemon `Config` Proxy now defines a `has` trap symmetric to the
existing `get` trap. Without it, a future SubagentManager path
doing `'someMethod' in this.config` would silently get `false` and
bypass the safety net the throw-on-unknown-property design
installed.
- Preflight `manager.loadSubagent(name, level)` before
`manager.createSubagent`. The default-path collision check inside
SubagentManager would otherwise miss same-frontmatter-name +
different-filename collisions; the preflight makes 409
agent_already_exists deterministic.
- Multi-level DELETE now emits one `agent_changed` event per level
that actually had a file removed. Previously an unscoped DELETE
removing both project and user shadows would publish only one
event with one level — misleading subscribers using event metadata
for toasts / audit / echo-suppression.
Test additions (covers the new event types + bridge fan-out + SDK
helpers):
- `daemonEvents.test.ts`: predicate narrowing for `memory_changed` /
`agent_changed` (rejects malformed scope/mode/level), reducer
records `lastWorkspaceMutation` + `lastWorkspaceMutationType` with
latest-event-wins semantics and stays non-terminal.
- `httpAcpBridge.test.ts`: `publishWorkspaceEvent` fans out to every
active session bus; `knownClientIds()` aggregates clientIds across
sessions and the returned set is a snapshot (mutating it does not
affect future calls).
- `workspaceAgents.test.ts`: success-path test stamping
`originatorClientId` on the create / update / delete events for a
known client.
- `DaemonClient.test.ts`: 7 round-trip tests for the new SDK helpers
(workspaceMemory, writeWorkspaceMemory, listWorkspaceAgents,
getWorkspaceAgent, createWorkspaceAgent, updateWorkspaceAgent with
scope query, deleteWorkspaceAgent: 204 / structured 404 / bare 404
triage).
- `writeContextFile.test.ts`: replace the 30ms-mtime test with a
`vi.spyOn(fs, 'writeFile')` assertion that the no-op path never
invokes writeFile. Deterministic on every filesystem.
Validation:
- typecheck: cli + sdk-typescript clean
- vitest: serve 363/363 + writeContextFile 12/12 + SDK 347/347
- eslint: clean
Reviewer guide: combined with fold-in 2a (commit
|
||
|
|
495d11f016
|
refactor(serve): add FileSystemService boundary (#4175 Wave 4 PR 18) (#4250)
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
* refactor(serve): add FileSystemService boundary (#4175 PR 18) Introduce a per-request workspace filesystem boundary inside the `qwen serve` daemon. The boundary centralizes path canonicalization, symlink-aware boundary checks, ignore/trust policy, size/binary limits, and audit hooks behind a single typed surface — preparing PR 19 (read-only file routes) and PR 20 (write/edit routes) to share a guarded chokepoint instead of re-implementing path safety per route. Wave 4 PR 18 of #4175 — pure refactor, no new HTTP routes; depends on PR 12 (#4241) and PR 15 (#4236), both merged. New module under `packages/cli/src/serve/fs/`: - `paths.ts` extracts `canonicalizeWorkspace` from `httpAcpBridge.ts` (re-exported there for backward compatibility) and adds: - `ResolvedPath` brand and `Intent` union (read/write/edit/list/ glob/stat) with exhaustiveness checks at the trust gate - `hasSuspiciousPathPattern` — detects NTFS ADS, 8.3 short names, long-path prefixes, UNC paths, trailing dots, DOS device names, and three-or-more-dot path components (claude-code-style) - `findExistingAncestor` with explicit ENOTDIR rejection so a regular file in a path component throws `parse_error` rather than passing boundary inspection and 500-ing later - `resolveWithinWorkspace` running a chain-aware realpath check with ENOENT-tolerant ancestor walk for write/stat intents - `errors.ts` defines `FsError` / `FsErrorKind` plus `wrapAsFsError`, which categorizes raw `fs.promises` errnos (EACCES → permission_ denied, ELOOP → symlink_escape, ENOTDIR → parse_error, etc.) so body-level failures emit audit events instead of escaping uncategorized - `policy.ts` carries `MAX_READ_BYTES` (256 KiB), `MAX_WRITE_BYTES` (5 MiB), `BINARY_PROBE_BYTES` (4 KiB), `shouldIgnore` (file/ directory aware), and `assertTrustedForIntent` with an exhaustive switch over `Intent` - `audit.ts` emits typed `fs.access` / `fs.denied` `BridgeEvent` frames with SHA-256-hashed paths, optional raw-path passthrough via `QWEN_AUDIT_RAW_PATHS=1`, and discriminator `kind` fields so SDK consumers can exhaustively narrow `event.data` - `workspaceFileSystem.ts` — `WorkspaceFileSystem` interface + `createWorkspaceFileSystemFactory` with eight methods (resolve, stat, readText, readBytes, list, glob, writeText, edit). Every body method funnels failures through `recordAndWrap`, which wraps raw fs errors and always emits an `fs.denied` audit event before rethrowing. `readText` enforces `MAX_READ_BYTES` *before* delegating to the slurping core service so unbounded requests against multi-gigabyte files can no longer OOM the daemon. `glob` realpath-checks each hit against the canonical workspace and reports filtered escapes via a single aggregated `fs.denied` event with the dropped count - `index.ts` is the barrel re-export PR 19/20 will import from Modified files: - `packages/cli/src/serve/httpAcpBridge.ts` — extracted `canonicalizeWorkspace` to `fs/paths.ts`; the bridge re-exports it so existing callers in `server.ts` and `runQwenServe.ts` keep working - `packages/cli/src/serve/server.ts` — added `fsFactory?: WorkspaceFileSystemFactory` to `ServeAppDeps`; `createServeApp` builds a strict default (`trusted: false`, warn-once no-op `emit`) when none is injected so a future refactor that forgets `fsFactory` injection cannot silently allow writes against an untrusted workspace; factory parked on `app.locals` for PR 19/20 route handlers - `packages/core/src/index.ts` — re-exports `Ignore`, `loadIgnoreRules`, and `LoadIgnoreRulesOptions` from `utils/filesearch/ignore.js` for cli consumption 411 serve tests pass; typecheck clean. Engineering principles checklist: - [x] Independently mergeable (no new routes, no new capability tag) - [x] Backward compatible (no removed routes / event fields / CLI behavior) - [x] Default off (no public surface change; PR 19/20 will activate routes) - [x] qwen serve Stage 1 routes preserved - [x] Gradual migration (PR 19/20 will adopt the boundary) - [x] Reversible (single PR rollback) - [x] Tests-first (101 unit tests across the new module + contract test) 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(serve/fs): address PR review feedback (#4250) Codex + Copilot review found 8 substantive issues against |
||
|
|
96219924a0
|
feat(serve): MCP client guardrails (#4175 Wave 3 PR 14) (#4247)
* feat(serve): MCP client guardrails (#4175 Wave 3 PR 14) Adds an in-process MCP client counter, slot-reservation enforcement at all 3 spawn sites (discoverAllMcpTools / discoverAllMcpToolsIncremental / readResource), new `--mcp-client-budget=N` + `--mcp-budget-mode={enforce,warn,off}` CLI flags forwarded to the ACP child via env, and additive `clientCount` / `clientBudget` / `budgetMode` / `budgets[]` fields plus `disabledReason: 'budget'` tagging on `GET /workspace/mcp`. Always-on capability tag `mcp_guardrails` with `modes: ['warn', 'enforce']` so SDK clients can pre-flight refusal semantics. Typed SSE push events (`mcp_budget_warning` / `mcp_child_refused_batch`) intentionally deferred to a small follow-up PR — the snapshot already exposes `budgets[0].status: 'warning'|'error'` + `refusedCount` so operator visibility isn't blocked. * fixup(serve): address PR 14 review (#4247) findings 1-7 Addresses Codex + Copilot review feedback on #4247. Seven functional and forward-compat fixes; (8) `tcp` transport mapper vs createTransport deferred pending @wenshao direction (separate core/protocol decision). 1. **Single-server rediscovery bypass** — add `tryReserveSlot` at the top of `discoverMcpToolsForServerInternal`. Pre-fix a server refused at startup could be brought online later via `/mcp reconnect <name>` and exceed the cap in enforce mode. 2. **Empty `budgets[]` when mode=off** — early `return []` in `buildBudgetCells` when mode is `off`. Protocol docs / SDK types promise empty array; pre-fix emitted a synthetic noisy cell. 3. **runQwenServe validation + env leakage** — mirror CLI budget validation in `runQwenServe` (the embedded entry point); explicitly delete `QWEN_SERVE_MCP_*` env vars when options are undefined so multiple daemons in one process don't leak prior budget config to subsequent ACP children. 4. **Disabled-vs-refused precedence + stale refusal log** — config-disable wins over budget refusal in the per-server cell; `removeServer` + `disconnectServer` drop the entry from `lastRefusedServerNames` so operator action immediately clears the budget tag. 5. **Incremental remove-before-reserve ordering** — process config-removed servers FIRST in `discoverAllMcpToolsIncremental` so freed slots are visible to subsequent `tryReserveSlot` calls. Pre-fix scenario {a,b}→{a,c} with budget=2 wasted a slot. 6. **`scope` forward-compat type widening** — `'workspace' | (string & {})` on both `ServeMcpBudgetStatusCell` and `DaemonMcpBudgetStatusCell` so SDK consumers don't break when PR 23 adds `scope: 'pool'` per the documented no-schema-bump contract. 7. **Test comment alignment** — fix "With budget=1" comment to match `clientBudget: 2` code. Plus 4 new core regression tests covering #1/#2/#4/#5, and 4 new serve tests covering #3 (boot rejection + env cleanup). 237/237 pass across the affected files (36 core mcp-client-manager + 50 acpAgent + 151 serve). * docs(serve): clarify v1 snapshot-based budget warning detection (#4247) Address github-actions review-summary finding (I) on PR #4247: v1 operators have no SSE push event for budget pressure yet (deferred to PR 14b), so the protocol doc should explicitly say how to detect warning / error states from the snapshot. Adds the three-way mapping `budgets[0].status` ↔ live/refused counts. * fixup(serve): address PR 14 review round 2 (#4247 wenshao) Addresses @wenshao review on PR #4247. Three critical safety fixes + four suggestion-level improvements. Critical (zombie slot leaks — would break `enforce` mode for the rest of the daemon's lifetime): - C2: `discoverAllMcpTools` connect() catch now releases reservedSlots + clients entry. Pre-fix one failed connect permanently consumed a budget slot. - C3: `readResource` wraps client.connect() in try/catch; on throw the slot + client entry are cleaned up before re-raising. Tracked `weReservedSlot` so the cleanup only fires for newly-created lazy spawns (reused already-CONNECTED clients are untouched). - (wenshao C1 was the rediscovery-bypass also caught by Codex + Copilot — already addressed in fixup |
||
|
|
d07c958bb5
|
feat(tui): add daemon adapter spike (#4202)
* docs(tui): draft daemon adapter plan * feat(tui): add daemon adapter spike * fix(tui): harden daemon adapter event handling * fix(tui): report daemon prompt failures * fix(tui): surface daemon terminal failures * fix(tui): harden daemon adapter state handling * fix(tui): harden daemon adapter lifecycle * fix(tui): harden daemon adapter follow-ups --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> |
||
|
|
f44ed09412
|
feat(serve): preflight and env diagnostics routes (#4175 Wave 3 PR 13) (#4251)
* feat(serve): introduce ServeErrorKind and BridgeTimeoutError (#4175 Wave 3 PR 13) Lay the type foundation for `/workspace/preflight` and `/workspace/env` (and the eventual MCP guardrails route) so cells emitted by all three share a closed `errorKind` taxonomy: - `SERVE_ERROR_KINDS` literal-list + `ServeErrorKind` union — the seven values from #4175 (`missing_binary`, `blocked_egress`, `auth_env_error`, `init_timeout`, `protocol_error`, `missing_file`, `parse_error`). - `BridgeTimeoutError` typed class — `withTimeout` now rejects with this rather than a plain `Error`, letting `mapDomainErrorToErrorKind` recognize init / heartbeat / extMethod timeouts via `instanceof` instead of regex-matching message strings. Message format is preserved bit-for-bit. - `mapDomainErrorToErrorKind` helper — one place to classify `BridgeTimeoutError`, `SkillError`, fs ENOENT/EACCES/EPERM, ModelConfigError subclasses (recognized by `name` field — they aren't on the public surface of `@qwen-code/qwen-code-core`), `SyntaxError`, plus message-regex fallbacks for legacy throw sites (`agent channel closed`, missing CLI entry path). - `ServeStatusCell.errorKind` tightened from open `string` to the closed `ServeErrorKind` union. Backward compatible — PR 12 never assigned the field. - SDK mirrors: `DAEMON_ERROR_KINDS` const + `DaemonErrorKind` type; `DaemonStatusCell.errorKind` tightened. Tests: 11 new unit tests in `status.test.ts` covering each mapping rule plus the BridgeTimeoutError shape. No route changes; no behavior changes for any existing path. * feat(serve): add buildEnvStatusFromProcess helper (#4175 Wave 3 PR 13) Pure helper that constructs the `/workspace/env` payload from `process.*` state. No I/O, no ACP roundtrip, no globals beyond `process.env`. The route itself lands in the next commit. - `ServeEnvKind` discriminant: `runtime | platform | sandbox | proxy | env_var` - `ServeEnvCell extends ServeStatusCell` with `name` + optional `present` / `value`. Cells with `kind: 'env_var'` are presence-only — `value` is ALWAYS omitted to keep secret env vars off the wire even by accident. - `ServeWorkspaceEnvStatus` envelope: `{ v, workspaceCwd, initialized: true, acpChannelLive, cells, errors? }`. `initialized` is structurally `true` because env answers from the daemon process directly; `acpChannelLive` reports whether a child is up but does not change the payload shape. Whitelist policy: - Auth/secret keys (presence-only): OPENAI/ANTHROPIC/GEMINI/GOOGLE/DASHSCOPE/ OPENROUTER `_API_KEY`, `QWEN_SERVER_TOKEN`. - Non-secret keys (also presence-only for shape uniformity): base URLs, locale, TZ, NODE_EXTRA_CA_CERTS, QWEN_CLI_ENTRY. - Proxy vars (`HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`/`ALL_PROXY` + lowercase variants): credentials stripped via `redactProxyCredentials`, then `URL().host` so the wire only carries `host:port`. NO_PROXY is a host list rather than a URL so we pass the redacted form verbatim. SDK mirrors: `DaemonEnvKind`, `DaemonEnvCell`, `DaemonWorkspaceEnvStatus`. Tests: 9 unit tests covering the proxy-credential redaction, lowercase env fallback, NO_PROXY pass-through, presence-only `env_var` invariant (`'value' in cell === false`), whitelist enforcement, runtime tag detection, and envelope shape. * feat(serve): add GET /workspace/env route (#4175 Wave 3 PR 13) Wire `buildEnvStatusFromProcess` from the previous commit through the bridge, server, and SDK so remote clients can pre-flight the daemon's runtime environment without spawning an ACP child. - `workspace_env` capability tag (always advertised on a current daemon). - `bridge.getWorkspaceEnvStatus()` answers entirely from `process.*` — the route never consults ACP. `acpChannelLive` reflects whether a child exists but does not change the payload, so an idle daemon and a busy one return the same env shape. - `app.get('/workspace/env', ...)` mirrors PR 12's one-liner pattern. - SDK: `DaemonClient.workspaceEnv()` returning `DaemonWorkspaceEnvStatus`. - Docs: bullet in `docs/users/qwen-serve.md` calling out the presence-only redaction policy and the no-ACP-spawn guarantee. Tests: server-level (env returned + `'value' in env_var === false` assertion), bridge-level (idle and live both answer locally without hitting ACP extMethod), SDK-level (recording-fetch round-trip on `/workspace/env`). The `workspace_env` tag is added to the `EXPECTED_STAGE1_FEATURES` capability list assertion. * feat(serve): add /workspace/preflight daemon-cells path (#4175 Wave 3 PR 13) Wire the preflight route. Daemon-level cells are populated unconditionally from `process.*` and `node:fs`; ACP-level cells fall back to `not_started` placeholders when no child is alive so a poll never spawns one. - `workspace_preflight` capability tag. - `ServePreflightKind` discriminant (12 values: node_version, cli_entry, workspace_dir, ripgrep, git, npm — daemon-level — plus auth, mcp_discovery, skills, providers, tool_registry, egress — ACP-level). - `ServePreflightCell extends ServeStatusCell` with `locality: 'daemon' | 'acp'` + free-form `detail`. `ServeWorkspacePreflightStatus` envelope. - `createIdleAcpPreflightCells()` factory: emits the six ACP-level cells with `status: 'not_started'` + a uniform `hint` so the bridge can stitch them in alongside daemon cells without ever calling ACP. - `bridge.getWorkspacePreflightStatus()`: - Daemon cells via `buildDaemonPreflightCells` (Promise.all over Node-version, CLI-entry resolution mirroring `defaultSpawnChannelFactory`, `fs.stat` on `boundWorkspace` with ENOENT/EACCES/EPERM mapped to `missing_file`, best-effort `canUseRipgrep` / `getGitVersion` / `getNpmVersion` warnings). - ACP cells via `requestWorkspaceStatus` — idle factory returns the `not_started` placeholders; live path delegates to ACP via the `qwen/status/workspace/preflight` ext method (handler lands in next commit). Bridge-side timeout / channel-close while consulting ACP folds into envelope `errors[]` with `mapDomainErrorToErrorKind` classification; daemon cells still render. - `app.get('/workspace/preflight', ...)` route + JSDoc bullet. - SDK: `DaemonPreflightKind` / `DaemonPreflightCell` / `DaemonWorkspacePreflightStatus` mirrors; `DaemonClient.workspacePreflight()`. Tests: server-level (route returns the bridge payload), bridge-level (idle returns 6 daemon + 6 ACP `not_started` cells without spawning a channel), SDK-level (`workspacePreflight()` round-trip). Capability test updated. * feat(serve): wire ACP-side preflight cells (#4175 Wave 3 PR 13) Populate the six ACP-level preflight cells inside the ACP child so `/workspace/preflight` returns real values for live sessions. - `extMethod(qwen/status/workspace/preflight, ...)` dispatches to a new `buildAcpPreflightCells(config)` private method. - Five cell builders, each returning a `ServePreflightCell` with `locality: 'acp'`: - `auth`: `validateAuthMethod(authType, config)` returning non-null string → `auth_env_error`. Missing auth method → warning. Throws classified via `mapDomainErrorToErrorKind` with `auth_env_error` fallback. - `mcp_discovery`: rolls up `getMCPDiscoveryState()` + per-server `getMCPServerStatus(name)` counts. `connecting > 0` or in-progress discovery → warning + `init_timeout`; `disconnected > 0` post-discovery → error + `protocol_error`. - `skills`: `SkillManager.listSkills()`; SkillError throws are mapped via the helper (`PARSE_ERROR` → `parse_error`, `FILE_ERROR` → `missing_file`). - `providers`: `getAllConfiguredModels()`; empty list with a configured `authType` → warning + `auth_env_error`. ModelConfigError throws map to `auth_env_error`. - `tool_registry`: null registry → error + `protocol_error`. Otherwise surfaces tool count. - `egress`: stays `not_started`. PR 14 plugs in the real probe. - `errorCell` private helper extended with optional `errorKind` parameter; defaults to `mapDomainErrorToErrorKind(error)` so existing call sites (`mcp` / `skills` / `providers` envelope errors) automatically gain classification. Tests: 2 new acpAgent tests — preflight returns the six expected ACP cells with correct locality + statuses; preflight surfaces a `SkillError` (`PARSE_ERROR`) on the `skills` cell as `errorKind: 'parse_error'`. The core `vi.mock` block adds a SkillError class for `instanceof` matching inside `mapDomainErrorToErrorKind`. * docs(serve): preflight and env protocol section (#4175 Wave 3 PR 13) Document `/workspace/env` and `/workspace/preflight` end-to-end: - Common-cell shape: tighten `errorKind` from open `string` to the closed `DaemonErrorKind` enum (seven literals from #4175). Add an explicit redaction-policy paragraph covering env-var presence-only, proxy host:port reduction, and the whitelisted-secrets list. - Capability-tag list: add `workspace_env` and `workspace_preflight`. - New `### GET /workspace/env` section with sample payload, `DaemonEnvKind` / `DaemonEnvCell` types, and the redaction-policy paragraph spelling out which secret env vars are enumerated and how proxy URLs are reduced to `host:port`. - New `### GET /workspace/preflight` section with idle sample payload, `DaemonPreflightKind` / `DaemonPreflightCell` types, the seven-value `errorKind` semantics table, and the bridge-error fallback contract (mid-request ACP channel close → cells drop to `not_started` + envelope carries one `errors[]` entry). - Source-layout table: extend the `status.ts` row to mention the new `ServeErrorKind` / `BridgeTimeoutError` / `mapDomainErrorToErrorKind` surface; add a new `envSnapshot.ts` row. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
9abd704e09
|
fix(cli): record mid-turn queued user prompts (#4215) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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) |
||
|
|
a5e4839e07
|
fix(cli): restore ACP prompt counter on resume (#4233) | ||
|
|
c25e22b575
|
feat(serve): add session-scoped permission route (#4232)
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> |
||
|
|
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> |
||
|
|
605e5eea16
|
fix(cli): include skill base dir in slash commands (#4224) | ||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
b9590283c0
|
fix(cli): pass rewind selector test props (#4211)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
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) |
||
|
|
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
|
||
|
|
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`
(
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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
|