Skip to content

feat(core): Durable Managed Session journal and failover - #7

Open
doudouOUC wants to merge 21 commits into
cursor/upstream-main-3df3from
cursor/durable-session-failover-3df3
Open

doudouOUC wants to merge 21 commits into
cursor/upstream-main-3df3from
cursor/durable-session-failover-3df3

Conversation

@doudouOUC

Copy link
Copy Markdown
Owner

What this PR does

This is the durable Session and failover review block split out of preview QwenLM#12358, on current QwenLM main (90232f0eb, published here as cursor/upstream-main-3df3).

It adds the TypeScript session authority, local JSONL journal, HTTP store client, harness checkpoint, prompt inbox, activation fence, dispatch gate, and the tests that cover continuation and store conflicts. Transcript reading learns the managed execution engine, and session storage gains the managed resource root and title helpers. The shared contract fixture is included so the Java and TypeScript sides can be compared against the same bytes.

The Java session store, schema migrations, and writer-token routes are not in this diff. They ship with the Spring control plane, because the connector calls them.

Why it's needed

Issue QwenLM#12380 stage G is a durable session that can fail over without mixing that storage into the hosted tool loop or the WebShell panel. The preview kept the journal, the HTTP client, and the Java store in one tree. This block is the TypeScript half.

Reviewer Test Plan

How to verify

With workspace dependencies installed, run:

npx vitest run packages/core/src/managed-runtime/managed-session-authority.test.ts packages/core/src/managed-runtime/http-managed-session-store.test.ts packages/core/src/managed-runtime/managed-harness-checkpoint.test.ts packages/core/src/managed-runtime/managed-session-store-contract.test.ts packages/core/src/managed-runtime/local-jsonl-managed-session-journal-store.test.ts

This workspace has no node_modules, so those tests were not executed here.

Evidence (Before & After)

N/A. No runtime was started for this block.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Node dependencies are not installed in this workspace.

Risk & Scope

  • Main risk or tradeoff: http-managed-session-store.ts talks to /internal/managed-session-store/v1/. That Java route, in the Spring block, checks a client-supplied writer token and tenant header. It does not check a service credential, so a caller who can reach the port can attempt lease and journal operations. That gap stays on the Java review, not fixed here.
  • Not validated / out of scope: process failover (--session-failover, --inflight-failover, --continuation-failover) is implemented by scripts/run-managed-agent-server-e2e.ts in the Spring block and is not run in CI. This diff does not change the broker, the hosted profile, or WebShell. The tool digest helper is included because the journal and HTTP client hash requests with it; the tool executor is not.
  • Breaking changes / migration notes: transcript reads now recognize a managed execution engine record. Existing legacy transcripts stay on the legacy engine. This pull request is on doudouOUC/qwen-code and cannot merge into QwenLM/qwen-code directly.

Design notes: docs/design/2026-09-21-managed-session-durable-store.md and .zh-CN.md. The same pair is also in the Spring control-plane review.

Linked Issues

Related to QwenLM#12380. Split from QwenLM#12358. Does not close either.

中文说明

这是从预览 QwenLM#12358 拆出的 Durable Session 与 failover 评审块,基线是当前 QwenLM main(90232f0eb,在本仓库记为 cursor/upstream-main-3df3)。

它加入 TypeScript 的会话权威、本地 JSONL journal、HTTP 存储客户端、Harness checkpoint、prompt inbox、activation fence、dispatch gate,以及覆盖续跑和存储冲突的测试。转录读取会识别 managed 执行引擎,会话存储补上 managed 资源根目录和标题辅助函数。共享契约夹具也在里面,Java 和 TypeScript 可以对同一组字节。

Java 会话存储、schema 迁移和 writer token 路由不在本 diff。它们跟 Spring 控制面走,因为连接器会调用它们。

为什么需要

QwenLM#12380 的阶段 G 是可故障转移的持久会话,不应和 Hosted 工具闭环或 WebShell 面板混在一次评审里。预览把 journal、HTTP 客户端和 Java 存储放在同一棵树里。这一块是 TypeScript 这一半。

风险与范围

http-managed-session-store.ts 访问 /internal/managed-session-store/v1/。Spring 那一块里的 Java 路由只校验调用方自己带的 writer token 和租户头,没有服务级凭证。能打到该端口的调用方就可以尝试租约和 journal 操作。这个缺口留在 Java 评审里,本块没有改它。

进程级 failover(--session-failover、--inflight-failover、--continuation-failover)在 Spring 块的 scripts/run-managed-agent-server-e2e.ts 里,而且没有进 CI。本 diff 不改 Broker、Hosted profile 或 WebShell。工具 digest 辅助函数在这里,是因为 journal 和 HTTP 客户端用它给请求做哈希;工具执行器不在这里。

本环境没有 node_modules,这些测试没有跑。本 PR 在 doudouOUC/qwen-code,不能直接合并进 QwenLM/qwen-code。

关联 QwenLM#12380,从 QwenLM#12358 拆出,不关闭这两个单子。

Open in Web Open in Cursor 

wenshao and others added 3 commits September 25, 2026 10:10
…M#12681)

* feat(sdk-java): Add the W0a Managed Workspace binding contract

Add the first W0 slice of the Managed Agent proposal (QwenLM#12380): the
Workspace binding contract, as a framework-neutral Java module with a
TypeScript twin of its directory rule and digest. Nothing is wired yet.

- Workspace Registry record, a tenant-scoped read contract, and an
  immutable snapshot built from deployment configuration, with a
  successor check against dropped Workspaces, lower generations and
  storage swaps without a new generation.
- Actor access (NONE, READ, CREATE) and an explicit-grant policy.
- WorkspaceCatalog lists the Workspaces an actor can read, with a
  default that does not depend on the page, and resolves a Session's
  selection. Missing, foreign and unreadable Workspaces fail alike, and
  nothing falls back to a default or a launch directory.
- The lexical rule and normal form of cwdRelative. The drive-prefix
  check runs on the normal form, so ./C:x cannot turn into C:x.
- ContextBinding and its contextDigest: SHA-256 over length-prefixed
  UTF-8 items with a domain tag, with integers as decimal text.
- Shared fixtures, 70 path cases and 39 binding cases computed by an
  independent implementation, run by both languages.
- Run the new module in the SDK Java workflow's Java 21 jobs.

* refactor(sdk-java): Move the W0a Workspace binding into runtime-broker

QwenLM#12380 settled the placement: the binding is a separate package inside the
existing runtime-broker module, not a new Maven module. Move the code to
com.alibaba.qwen.code.runtimebroker.managedworkspace and drop the module's
pom, CI steps, workflow-guard tests and size-baseline change; the SDK Java
workflow already runs the runtime-broker tests and Checkstyle. The package
name keeps clear of the repository's unanchored `workspace/` ignore rule,
which would silently drop new files. The runtime-broker README and QWEN.md
gain a section on the package.

Tighten the contract where review found gaps:
- resolve raises workspace_not_found for missing, foreign, impossible and
  unreadable Workspaces from one site, and consults the access policy before
  the state of a default Workspace;
- the TypeScript encoder reads each binding field once and rejects a binding
  that is not an object;
- a snapshot rejects a defaults map holding two equal tenant keys.

Pin the rules in both languages with character probes over the whole Basic
Multilingual Plane and the astral blocks that mimic ASCII, and with fixtures
for lone-surrogate orders, dot segments with spaces and violations deep in
long paths. Java tests now also cover content and case-sensitive comparisons,
constructor visibility, significant spaces, locale-independent test data and
the listing's scan bounds. The design document records the decided placement
and moves the boot envelope to its own follow-up change.
…ntials (QwenLM#12680)

* perf(cli): keep running in place when env files only hold model credentials

The env-file relaunch rule from QwenLM#12602 fires for any value that `.env` files
or `settings.env` inject, including the API key that `/auth` stores and the
keys the docs recommend for `~/.qwen/.env`. Those users always paid the
second boot. Providers read credentials and endpoints per request, never
while a module loads, so `hasLoadedEnvironmentValues()` now ignores the
documented per-auth-type variables and the `envKey` of every configured
model provider. Any other injected value still forces a fresh image, and a
provider entry can never exempt a loader or boot-time key.

The DashScope proxy base URL was the one such variable read at module load;
it is now read at call time.

Reported by @wenshao in the QwenLM#12622 review (F2).

Claude-Session: https://claude.ai/code/session_01163b7X4Ntw1S2pr9VrtRrs

* feat(scripts): let the startup benchmark load credentials from a file

`--credentials settings|dotenv` moves the stub's key and URL from the shell
into the settings file's `env` block or `~/.qwen/.env`, so the env-file
relaunch rule can be measured. The default stays `shell`.

Claude-Session: https://claude.ai/code/session_01163b7X4Ntw1S2pr9VrtRrs
…QwenLM#12671)

* feat(cli): Declare the v2 execute/status/cancel Managed Runtime contract

Extend the owned managed-runtime route manifest with the three tool
operations and add their shared schema and conformance fixtures. The
wire shape fixes the evidence rules the recovery design demands: every
call is keyed by the original reference (sessionId, promptId, callId,
argsDigest) rather than any Broker-side identifier; status is read-only
and answers unknown with 200 instead of 404 or 500, because a missing
record is never evidence that a call did not run; a settled state is
the only one that may carry a result; and status additionally returns
the Runtime's own lastSequence cursor. Execute accepts up to 256 KiB
of request input, and every operation answers at most 1 MiB.

No handler is mounted yet: per the manifest rule the routes join in the
same line of slices as the execute extraction that implements them. The
TypeScript gate test proves the owned-route gate admits exactly these
paths, and the Java consumer pins the route contract, every outcome
classification, and the closed field sets, so the worker and transport
slices that follow share one reviewed target.

Related to QwenLM#12380.

* fix(runtime-broker): Accept unknown cancellation status

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

* feat(cli): Mount the v2 tool operations on the Managed Runtime worker

The attestation worker now serves execute, status, and cancel beside
attest. Its executor admits exactly the first-slice ordinary tools —
read_file, write_file, edit, and foreground run_shell_command — over a
real Config rooted at the attested workspace cwd, with checkpointing
disabled and no further approval gate, because admission happened on the
Harness side. An in-memory journal answers status and cancel by the
original reference; it is process-local by construction, since the worker
process is the Runtime generation and a restart is a new generation
rather than a continuation.

execute is idempotent by reference.callId: the same identity joins the
in-flight invocation or returns its settled result, and the same callId
with a different digest or payload is a 409 identity conflict. status is
read-only and answers unknown with 200 for a reference the Runtime never
saw. cancel settles a prepared invocation without touching the tool,
aborts an executing one, and a cancel the Runtime honored settles as
cancelled whether the tool surfaces the abort as an error or as an early
result. The worker's HTTP requestTimeout is disabled because an execute
call holds its connection until settlement.

Related to QwenLM#12380.

---------

Co-authored-by: jinye.djy <jinye.djy@U-Y37CF7G3-1943.local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <szujobs@gmail.com>
@doudouOUC
doudouOUC marked this pull request as ready for review September 25, 2026 11:05
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T11:12:05.868357Z ae96eee Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae96eeeb5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +842 to +845
export function isManagedSessionTranscriptSync(
filePath: string,
scratchBuffer?: Buffer,
): boolean {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block the legacy rename path for Managed sessions

Wire this Managed-session detector into SessionService.renameSessionInternal before it appends a legacy custom_title record. A repo-wide search shows that the new detector has no production caller, so renaming a local Managed session still appends that record after the Managed header; scanManagedSessionJournal then rejects the unknown post-header subtype, making the session impossible to reopen.

Useful? React with 👍 / 👎.

Comment on lines +802 to +809
export function managedSessionResourceRoot(
runtimeBaseDir: string,
sessionId: string,
): string {
if (!isSinglePathSegment(sessionId)) {
throw new Error('sessionId must be a single path segment.');
}
return path.join(runtimeBaseDir, 'resources', sessionId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove Managed resources when deleting a local session

Add this resource root to the existing SessionService.removeSession cleanup. Currently its only production consumer is LocalManagedSessionResourceStore; the deletion path removes the transcript and legacy sidecars but never removes runtime/resources/<sessionId>, so deleting a Managed session leaves all message, checkpoint, and metadata bodies—including potentially sensitive content—on disk indefinitely.

Useful? React with 👍 / 👎.

Comment on lines +1411 to +1416
} catch (cause) {
// Records may already be on disk, so the sequences this transaction
// claimed are spent whether or not the marker landed.
this.writeFailure =
cause instanceof Error ? cause : new Error(String(cause));
throw cause;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve retryability after an ambiguous remote commit

Do not permanently poison the authority for every appendTransaction error when the selected journal is the HTTP atomic store. If the server commits /transactions:commit but its response is lost, the client throws here and writeFailure prevents the same command from being retried, even though the HTTP protocol and receipt handling are explicitly idempotent for this ambiguity; the live Harness becomes unusable until takeover or restart.

Useful? React with 👍 / 👎.

Comment on lines +884 to +894
export function readManagedSessionTitleInfoSync(
filePath: string,
runtimeBaseDir: string,
scratchBuffer?: Buffer,
): { title?: string; source?: 'auto' | 'manual' } | undefined {
const found = readManagedDomainBodySync(
filePath,
runtimeBaseDir,
MANAGED_METADATA_MARKER,
scratchBuffer,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the Managed title reader in session listings

Route SessionService title reads through this helper for Managed transcripts. At this commit the only production caller is the restore projection, while SessionService.getSessionTitleInfo and session listing still call readSessionTitleInfoFromFileSync, which searches for legacy custom_title records that the Managed sink deliberately does not write; consequently a successfully committed Managed rename is absent from the session picker and title APIs.

Useful? React with 👍 / 👎.

Comment on lines +145 to +149
operation: 'commitMessage',
commandId: `recorder:${record.uuid}`,
sessionKey: this.authority.sessionHeader.sessionKey,
contentDigest: this.authority.sessionHeader.definitionRef.digest,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Digest each carried record for idempotency

Derive contentDigest from the actual record rather than the session definition. Every ordinary projected message currently uses the same digest, so if recorder:<uuid> is retried with changed content, LocalManagedSessionAuthority.commit treats it as an exact replay and silently returns the original receipt instead of raising the intended idempotency conflict; the title and other domain-record paths repeat the same pattern.

AGENTS.md reference: AGENTS.md:L261-L265

Useful? React with 👍 / 👎.

tanzhenxin and others added 8 commits September 25, 2026 11:24
…wenLM#12622)

* perf(cli): halve fresh-startup time to typeable and cut RSS by 60%

Measured with a clean-environment benchmark following the approach in
https://claude.dev/blog/how-we-made-claude-ai-faster/ (interleaved A/B,
p75 wall-clock backed by deterministic proxies). Against main, time to a
typeable prompt goes from 2.55 s to 1.18 s, `qwen -p` time to first
request from 2.15 s to 0.94 s, and RSS at the prompt from 574 MB to
231 MB, with no screen change after the prompt becomes usable.

- Skip the supervisor relaunch when it adds no Node flags; restarts
  re-exec in place and updates run in-process (POSIX, plain TUI and -p).
- The bin launcher runs the CLI in-process on POSIX.
- Enable Node's compile cache on the default route.
- Move startup side effects off the critical path: editor probing, the
  IDE process walk outside VS Code, and the npm-based update check.
- Resolve the git branch before the first frame so the footer does not
  reflow the screen after the prompt is usable.
- Bundle with minifyWhitespace; load the review command and non-common
  highlight grammars only when needed.

Experiments, tradeoffs and open decisions are in
docs/design/2026-09-24-fresh-startup-performance.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1XQv6wm9YJ5rSM53eMSRT

* fix(cli): keep global.gc in relaunched sessions

On POSIX the bin launcher now runs the CLI in-process and exposes gc at
runtime, so --expose-gc is no longer in process.execArgv. The supervised
relaunch built its child's flags from execArgv alone, so the session
process lost global.gc in --acp, stream-json, --json-fd, file input,
advanced.autoConfigureMemory, and on Node without process.execve.

Both relaunch paths now take their flags from one helper that adds
--expose-gc when gc is exposed but not on the command line.

* fix(cli): relaunch when env files injected values

QwenLM#12602 re-boots a one-shot run whenever .env or settings.env injected
values, because modules that read the environment at load time (and
Node itself, e.g. NODE_EXTRA_CA_CERTS) only see them in a fresh image.
The in-process path ran ahead of that check and skipped it for both
interactive and -p. Take the in-process path only when no env-file
value was injected; otherwise relaunch as main does.

* fix(cli): keep the spawned CLI under Bun

The Bun standalone flavor (the OpenTUI preview archives) runs the same
bin entry. Bun has no v8.setFlagsFromString, so the in-process path
crashed every non-fast-path command at startup on macOS and Linux.
Under Bun the entry spawns the CLI with --expose-gc, as on Windows.

Also correct the entry's header comment, the relaunch-flags helper's
doc comment and a displaced test comment, and narrow the design doc's
gc claim to the modes that were checked.

* fix(cli): install a deferred update after a clean exit

Without a supervising parent, the startup check fell back to "Run
/update to install the update." for standalone and update-command
installs. Record the request in-process instead: once the session ends
cleanly, install the update and exit with its code, so the launcher's
exit hook reopens the new version as the parent did on main. A second
quit during the install waits instead of cutting it short.

* docs(design): record the fresh-startup decisions

Resolve the open decisions, and describe the env-file relaunch rule,
the Bun launcher path, the restored update-on-exit and the independent
reproductions, in both language versions.

* docs(design): record decision 7 as a manual harness without a CI gate

The follow-up (QwenLM#12674) checks in the benchmark harness for manual runs.
The interactive startup counts stop at the typeable prompt, and
timer-driven work before it shifts with runner speed, so a CI gate
would be flaky.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
…P mode (QwenLM#9531)

* fix(cli): make the Agent Team teammate tab transcript scrollable in VP mode

In Virtualized History mode (ui.useTerminalBuffer, on by default) the
app owns the whole screen, so there is no terminal-native scrollback.
The main conversation view renders its transcript in a ScrollableList
virtual viewport, but AgentChatContent rendered the teammate tab
transcript through ink's <Static> unconditionally — content that
scrolled off screen could not be reached by Page Up, Page Down, arrow
keys, or the mouse wheel (QwenLM#9507).

Route the teammate transcript through the same ScrollableList viewport
in VP mode: header + committed + live items + spinner live in one
viewport pinned to the tail, with Page Up/Page Down, Shift+arrows,
Ctrl+Home/End and wheel/scrollbar support. The committed/live split is
preserved (items from an executing/confirming tool group onward stay
on the interactive render path so approval dialogs keep receiving
input). The legacy <Static> path for useTerminalBuffer: false is
unchanged — native terminal scrollback already works there.

* fix(cli): preserve teammate transcript selection

* fix(cli): re-measure the agent footer when its height changes

AppContainer's controls-height measure effect tracked only
main-conversation state, so on the agent tab the Completed/Failed
status row, queued messages and input growth never triggered a
re-measure: controlsHeight stayed stale-high and the transcript
viewport pushed the composer and tab bar past the terminal bottom.

AgentComposer now derives a layout key from its height-shifting state
(streaming state, status row, queued messages, input text) and syncs
it to AgentViewContext; the measure effect depends on that key,
mirroring liveAgentPanelLayoutKey from QwenLM#5798.

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

* fix(cli): use renamed RespondingSpinner in agent transcript viewport

Main renamed GeminiRespondingSpinner to RespondingSpinner; the merge
kept the old name at the viewport's spinner item and broke typecheck.

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

* fix(cli): follow RespondingSpinner rename after merging main

Main renamed GeminiRespondingSpinner to RespondingSpinner; update the
teammate-tab spinner usage and its test mock so the merged tree compiles.

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

* test(cli): guard the AgentViewContext layout-key propagation deps (QwenLM#9507)

R3-1: the provider link of the QwenLM#9507 layout-key chain was the only link
without a source guard. Pin agentComposerLayoutKey in the state memo deps
and setAgentComposerLayoutKey in the actions memo deps of
AgentViewContext.tsx by extending app-container-controls-dep.test.ts in
the PR's established guard style. Mutation-verified: dropping either
entry from its deps array (while the object literal keeps it) turns
exactly the matching new test red, and every pre-existing test stays
green.

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

* fix(cli): restore full-detail toggle and mouse actions on teammate VP path

The virtualized render path added for the teammate tab dropped two things
the main conversation view has:

- `fullDetail` was not forwarded to either HistoryItemDisplay site, so the
  app-wide Ctrl+O/Alt+T toggle did nothing for tool groups on this tab
  while truncated rows kept advertising "... +N chars (ctrl+o)".
- ScrollableList arms SGR mouse capture (bypassVpGate), but
  ContentMouseController was never mounted. With tracking armed the
  terminal stops handling the mouse itself, so OSC 8 link clicks and the
  right-click context menu silently did nothing.

Forward `fullDetail` on both VP branches (pending and committed) and mount
ContentMouseController next to TextSelectionController, mirroring
MainContent's VP path. The menu itself is already painted by the shared
ContextMenuOverlay in DefaultAppLayout, which sits above this tab.

Tests pin both: HistoryItemDisplay receives the toggled `fullDetail` on
both branches, and ContentMouseController mounts in VP mode only.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmto4amd1ku

* fix(cli): quiet the teammate VP viewport while the context menu is open

Mounting ContentMouseController made the right-click menu openable on the
teammate tab, but this branch never consulted useContextMenu(), so scroll
keys and wheel ticks kept reaching the viewport underneath the pinned menu
and a press or drag on a menu item anchored or cleared a transcript
selection under the overlay.

Mirror MainContent's VP path: gate ScrollableList's hasFocus on
contextMenuOpen === null, and pass TextSelectionController
eventsPaused={contextMenuOpen !== null} — paused rather than deactivated,
since deactivating clears the selection the menu's Copy Selection offers.
ContentMouseController stays ungated: it owns the open menu's pointer
interaction and its deactivate effect calls closeMenu().

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtq0vr74ni

* fix(cli): clear the painted text selection when its controller unmounts

The highlight range is painted onto the per-stdout frame controller, which
lives outside React, so it outlived the controller that painted it: ink
re-reads it on every serialization pass and stamps the background onto
whatever cells now occupy those coordinates. A selection made in a teammate
transcript therefore survived the tab switch and was re-stamped over rows of
the replacing view the user never selected, and no successor could repair it
-- clearSelection() early-returns while its own selection is empty, which is
what every freshly mounted controller starts with, and the frame-invalidation
callback early-returns on the same condition.

The teardown is unconditional (it goes straight to setSelection(null) rather
than through clearSelection()) and unmount-only: it must never fire on
eventsPaused, which exists to keep the selection that the open context menu's
"Copy Selection" item offers. getBuffer is a useCallback keyed on [stdout], so
the effect is stable and its cleanup runs exactly once, on unmount.

This closes the whole class rather than one route: teammate -> main,
main -> teammate, and agent -> agent (AgentChatView keys its boundary on
agentId) all unmount the surface that owns the selection.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtqfvvmto2

* fix(cli): gate the teammate tab's key consumers on the open context menu (QwenLM#9531)

Mounting ContentMouseController made the right-click menu reachable on the
Agent Team teammate tab, but the tab's other key consumers did not gate on it:
KeypressContext broadcasts to every subscriber and discards return values, so
a key aimed at the menu was also delivered to the composer and the approval
dialog. The composer now goes quiet on up/down/return/escape while the menu is
up (and closes it on any other key, then falls through), and the approval
dialog folds the open menu into the same `isFocused` flag it already gates its
keypress handler and RadioButtonSelect on. Both mirror the main view's
InputPrompt gate.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtyw8o2w11

* fix(cli): keep teammate approvals answerable and gate the last key consumers (QwenLM#9531)

R13-1: on the VP path a teammate's pending ToolConfirmationMessage is a tail item inside the windowed list, and dialogsVisible is derived from main-app dialog state only, so one Page Up unmounted the dialog together with its key handler and left the round blocked with nothing on screen. Pull the tail back into view when an approval arrives, then take the scroll keys away - the gate alone would trap a user who had already scrolled up, with no way back.

R13-2: KeypressContext broadcasts to every subscriber and discards return values, so an open right-click menu cannot consume a key for anyone. Gate the three remaining consumers on the teammate tab (tab bar arrows, the Shift+Tab approval-mode cycler, the Ctrl+F embedded-shell toggle), matching the two already gated in b479224.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtzfj4451s

* fix(cli): keep the deps guard's identifier out of comment prose

The source guard in app-container-controls-dep.test.ts substring-matches
the raw deps-array text, comments included. Naming liveAgentPanelLayoutKey
in this comment meant deleting the real dep entry left the guard green.
Reword so the identifier appears only as the dep entry itself; verified
by mutation (deleting the dep entry now fails the guard).

* fix(cli): drop a queued scroll flush on focus loss and gate the nested answer input

Two review Criticals on the teammate tab's approval surface:

- ScrollableList left a wheel/drag flush armed across a focus loss, so the
  timer fired after whatever took focus had pulled the tail into view and
  scrolled a pending approval dialog back out of the render window, where
  its keys and this list's were already dead.
- AskUserQuestionDialog hard-wired its nested custom-answer TextInput to
  isActive={true}, so an Enter aimed at an open right-click menu still
  submitted the half-typed draft as the answer to the pending question.

* fix(cli): block stale key handlers while context menus open

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

* fix(cli): preserve keys after input dismisses context menu

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…wenLM#12673)

* fix(cli): restore ripgrep exec bit on managed npm update activation

npm/pnpm pack preserves the exec bit only for `bin` entries, so the
published tarball ships every vendor/ripgrep/*/rg as 0644, and the
published manifest intentionally carries no postinstall to restore it
(QwenLM#6164 removed that surface). The managed self-update path installed,
validated, and activated such a staging tree without ever touching the
vendored binary modes, so every self-update landed a ripgrep that fails
to spawn with EACCES and silently falls back (QwenLM#12668).

Chmod the staged vendor/ripgrep/*/rg to 0755 after validation and smoke
test, before the activation rename. Best-effort like the source-tree
postinstall: platform dirs without a unix rg are skipped, and any other
chmod failure only logs — the runtime probe still falls back to a system
ripgrep or the JS grep tool, so an update must never fail over this.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-issue-patrol/jmugdo4w20u

* fix(cli): heal the ripgrep payload that activation actually adopts

Review round 1 follow-ups on the managed npm update heal:

- Guard the exec-bit assertion with `process.platform !== 'win32'` instead of
  calling it vacuous there: libuv synthesizes the mode from the read-only
  attribute alone, so a writable regular file stats as 0o666 and the assertion
  cannot pass on Windows. Only the assertion is guarded, so the case still runs
  there and still covers the win32 `rg.exe` ENOENT swallow.
- Heal the version directory that survives the rename collision, and the higher
  version an existing pointer keeps, not only the staged tree. Both exits of
  `activateManagedNpmUpdate` previously left `active.json` naming a payload this
  process never healed, and the collision branch discards the healed one first.
  The heal stays metadata-only, so a live older session importing that directory
  is unaffected.
- Extend the fixture with the `COPYING` entry the published tree really carries
  beside the platform directories, and add a non-ENOENT chmod failure (ELOOP via
  a self-referential symlink) so the skip guard, the ENOENT discrimination and
  the "must not block the update" contract are all pinned.
- Attribute the 0644 normalization to `pnpm pack`, which is what the release
  publishes with (`.github/scripts/run-release-step.sh` has no `npm publish`).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmugktc5f3t

* refactor(cli): drop unobservable chmod-failure reporting from the ripgrep heal

The `code !== 'ENOENT'` discrimination existed only to decide whether to call
`debugLogger.warn`, and that call cannot reach a sink in the one process that
runs it: `runCliEntry` awaits `installManagedNpmUpdate` and returns before
route dispatch (cli.ts:520-530), so no `Config` is constructed there and
`setDebugLogSession` is never called (config.ts:3104, :5524); `warn` returns
early without a session (debugLogger.ts:296-300).

Collapse it to a bare catch that states why nothing is logged. The
`!entry.isDirectory()` guard goes with it: `readdir` names are all the loop
needs now, and `Dirent.isDirectory()` reports a symlinked platform directory
as false, so the guard would skip a layout the chmod would have healed.

Also drops the test's `vi.hoisted`/`vi.mock` debugLogger spy and its two
`warn` assertions, which asserted the mock rather than product behaviour. The
COPYING fixture entry stays: the real tree carries it, and the heal now
genuinely attempts and swallows `COPYING/rg`.

232 -> 204 added lines. Mutation panel re-run one mutant at a time at this
head, zero survivors: dropping the staging heal reds 2 cases, the
collision-branch heal 1, the activeDir heal 1; making the chmod catch rethrow
reds 4 and the readdir catch rethrow reds 9. 19/19 tests pass, prettier and
eslint clean, no type errors in the changed files.

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

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
… fails (QwenLM#12666)

* fix(cli): notify when the Linux clipboard tool is found but its query fails

On Linux, getLinuxClipboardTool() only probes `command -v wl-paste/xclip`; it never touches the display server. With a stale or dead X server (or a compositor socket that went away) the probe passes while the actual query — xclip -t TARGETS -o or wl-paste --list-types — fails, and the paste was swallowed silently: non-zero exit, spawn error/throw and timeout all collapsed into a bare false, indistinguishable from "no image".

Thread onUnavailable into checkClipboardForImage() and getWlPasteImageTypes() and invoke it at each failure exit (non-zero exit, spawn error, spawn throw, timeout), so the caller's unavailability warning fires. A successful query that finds no image still never notifies, so copying text stays quiet, and the no-tool path is unchanged.

Refs QwenLM#12505 (finding 1 of 3).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-issue-patrol/jmufyo0er0o

* fix(cli): keep an empty Linux clipboard from claiming the native module is broken

Linux clipboard tools use one non-zero exit code for both "the display
server is dead" and "nothing is on the clipboard", so keying the
unavailability notification on the exit code made the image-paste binding
report that the native clipboard module could not be loaded whenever the
clipboard was simply empty.

Capture fd 2 in checkClipboardForImage() and getWlPasteImageTypes(), the
same way saveFromCommand() already does so a non-zero exit can be
diagnosed rather than merely reported, and skip the notification when
stderr identifies the exit as a benign empty clipboard:

- xclip: "There is no owner for the ... selection" (xcprint.c
  errconvsel(), exit(EXIT_FAILURE) when XGetSelectionOwner() is None)
- wl-paste 2.x: "Nothing is copied" (bail() is fprintf(stderr) + exit(1))
- wl-paste 1.x: "No selection" (same bail() path)

An unclassified non-zero exit still notifies, and the spawn-throw,
spawn-error and timeout notifications are left untouched.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmug5t7mp33

* test(cli): make the Linux clipboard failure paths diagnosable

Review follow-up to the three Suggestions on this PR:

- clipboardUtils.ts: log the exit code (and args) unconditionally when a
  clipboard query exits non-zero, and log both new timeout paths, mirroring
  saveFromCommand in the same file. A tool that fails without writing to
  stderr (xclip exits EXIT_FAILURE silently) notified the user while leaving
  nothing in the debug log to explain why.
- clipboardUtils.ts: bind and log the error in the new catch around
  spawn('wl-paste'). saveFileWithWlPaste calls getWlPasteImageTypes without
  an onUnavailable callback, so a synchronous spawn throw was invisible.
- clipboardUtils.test.ts: give the two inline BMP fixtures the stderr stream
  production now reads. Without it, child.stderr.on threw inside the promise
  executor and both tests passed on saveClipboardImage's catch-all null with
  nothing past the type query executed. They now assert the unlink of the
  derived .bmp path (and .png before .bmp), and a createDebugLogger spy pins
  each new log line.

Logging only: no notify condition, return value or cache semantics change.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmugedurp3j

* fix(cli): match released xclip empty-clipboard wording, latch the notify

Two defects in the Linux clipboard failure classifier, both reported
against d4439fc.

The only xclip marker was 'no owner for the', which comes from errconvsel()
in unreleased xclip git master (xcprint.c:136, called from xclip.c:778).
Released 0.13 -- the version Debian, Ubuntu and Fedora package -- has no
errconvsel() at all: a -t TARGETS -o query on a selection nobody owns takes
the XCLIB_XCOUT_BAD_TARGET branch that has no fallback left, prints "Error:
target TARGETS not available" and returns EXIT_FAILURE (xclip.c:468). No
marker matched that, so a routine empty clipboard fell through to
onUnavailable and rendered the "native clipboard module could not be
loaded / reinstall Qwen Code" notice, which cannot be true on the Linux
branch because it never loads that module. main returned false silently on
this exact input, so it was a regression, and it spent
clipboardUnavailableShownRef -- latched once per session and never reset --
so the genuine dead-X-server failure this PR exists to report could not be
reported again for the rest of that run. Both spellings are now markers,
and the doc comment says which xclip each one belongs to.

The unconditional exit-code log copied saveFromCommand's line but not the
`if (resolved) return;` latch that makes that line safe there. Node delivers
`close` after a spawn `error` (carrying the negated errno) and after a
timeout `kill()` (with code === null), so the close handler re-entered its
non-zero branch for a query that had already been reported: it recorded
`exited with code -2` for a binary that never started, `exited with code
null` immediately after the timeout line, and fired onUnavailable a second
time for one failure. Added the same settle latch to checkClipboardForImage
and to getWlPasteImageTypes, and logged the spawn errno in both error
handlers, since with the fabricated exit code suppressed that is otherwise
the only trace a spawn failure leaves. The failure paths still do not cache:
the latch returns before `cachedWlPasteImageTypes = types`.

The two fixtures now reproduce Node's real event order -- createSpawnErrorChild
emits close(-2) after error, and createHangingChild's kill emits
close(null, 'SIGTERM') -- so the existing toHaveBeenCalledOnce assertions
witness the latch instead of a sequence production never produces.

61 tests pass (60 before, +1 for the released xclip wording). Every new or
strengthened assertion was mutation-checked on its own: removing the marker
reds the new test with "expected spy to not be called at all, but actually
been called 1 times"; removing either function's latch reds exactly its
three witnesses ("expected spy to be called once, but got 2 times" plus the
'exited with code' negative) while the genuine exit-code, empty-clipboard and
dead-display tests stay green. cli typecheck reports no clipboardUtils error.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmugktc5f3t

* test(cli): fold the duplicated timeout cases, correct the marker provenance

Cleanup for the two round-2 Suggestions still open at 99289af. Net -21 lines;
no production behavior change beyond comments.

R2-2: the two `records the timeout when the query never answers` cases re-ran
the exact scenario the two `notifies when the query times out` cases already
cover -- same env stubbing, same probe result, same createHangingChild(), same
call resolving to false -- and differed only in which callback they asserted
on. Each spent a real five seconds against the module-private
PROCESS_TIMEOUT_MS, so the pair cost ten seconds on every local run and every
CI shard that picks up this file. Their witnesses are folded into the two
existing cases, which now assert a superset: the timeout log line, the
`exited with code` negative that catches the fabricated exit code arriving from
kill()'s close(null, 'SIGTERM'), and notify-once. Test count 61 -> 59,
five-second cases 4 -> 2, file duration 22.9s -> 18.0s.

Driving the remaining two with fake timers would remove the last ten seconds,
but that is test infrastructure rather than a defect, so it is left as
follow-up.

R2-3: the BMP witness comment read as confirming the python3 conversion path is
now pinned. It is not. fs.open is unmocked while mkdir is a no-op, so
saveFromCommand fails at its O_EXCL open before spawning, python3 never runs,
and the unlink counted there is the save-failure tail -- which also leaves the
fixture's callCount 2/3 arms unreachable. The comment now says so. Covering the
conversion catch and deleting those dead arms are pre-existing on the base
commit, so they stay follow-up rather than widening this PR.

Also corrects the wl-paste provenance: "Nothing is copied" arrives in
wl-clipboard v2.2.0 (src/wl-paste.c:252), not at the 2.x boundary -- v2.0.0
and v2.1.0 still print "No selection" (src/wl-paste.c:187). Both markers were
already load-bearing, just for a different version reason than stated, which is
the same master-versus-released confusion R2-1 was about. The citation now
lives once on EMPTY_CLIPBOARD_STDERR_MARKERS instead of being restated
verbatim in the test file; the duplication is what let the two copies drift.

Each folded witness was re-mutation-checked individually after the move:
removing the released-xclip marker reds its case with "expected spy to not be
called at all, but actually been called 1 times", and removing either
function's close latch or timer latch reds exactly its case with "expected spy
to be called once, but got 2 times". 59 tests pass; eslint, prettier --check and
cli typecheck report nothing on either changed file.

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

* test(clipboard): pin the four diagnostic logs R3-1 named as unasserted

clipboardUtils.ts logs four diagnostics on the Linux clipboard failure paths
that the new tests executed but never asserted, so reverting
child.on('error', (err) => { to the pre-PR child.on('error', () => { dropped
the binding and the log together and still shipped green.

Those logs are load bearing. The settle latch suppresses the close handler's
fabricated exit code, so the errno is a spawn failure's only trace, and the two
stderr echoes are the only record of the wording isEmptyClipboardError()
classified -- without them a failing query reads just "xclip exited with code 1.
Args: ..." and the classification decision is no longer auditable. Until QwenLM#12504
fixes the onUnavailable text, the debug log is also the only place the real
failure reason is recorded anywhere.

One assertion per site, added to the tests that already reach them, following
the toHaveBeenCalledWith convention the other three logs in this PR already use.
The wl-paste async-error assertion targets .debug, not .error, because the
synchronous-throw arm logs the byte-identical message at the other level and is
pinned separately; the two stderr expectations omit the fixtures' trailing
newline because both production sites call .trim().

Each assertion was mutation-checked individually: deleting or reverting its own
site now reds exactly one test (1 failed | 58) with no collateral, and the
production file is restored byte-identical afterwards. Test-only change -- no
log statement was removed, reworded or consolidated.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmugtdz9149

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…k aggregation (QwenLM#12689)

When multiple PreToolUse hooks match one tool call, the documented
hookSpecificOutput.permissionDecision fell into the generic
"later values win" merge branch, so the last hook in config order
overrode any earlier deny and the tool ran. Rank the field during
aggregation (deny > ask > allow) with the reason following the
winning rank (same-rank reasons concatenate), leaving top-level
decision OR-logic and last-wins for other hookSpecificOutput fields
unchanged.

Fixes QwenLM#12683


Patrol-Run: qwen-issue-patrol/jmugqj3jg10

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Add the contract for W0a-2, the second W0 slice of QwenLM#12380: how a Runtime
Broker and a Managed Runtime worker agree on managed-context/1, boot
document v2 and ready record v2, attestation v3, and the context
installation request with its receipt. Nothing is wired yet; W0c mounts
the routes and writes boot v2.

- Boot v2 carries the Workspace binding and the mount root but no
  Session context, since a Workspace-isolated Runtime is shared. Every
  field that a response repeats is bounded, so every record fits within
  its 16 KiB limit.
- A Session installs its ContextBinding with an idempotent request. The
  worker checks the shape, the binding's digest, the Workspace part, and
  then operation and Session reuse. A refused request records nothing.
- A shared JSON schema and fixture file pins every closed key set, field
  rule, route, error classification and the check order. An
  implementation independent of both languages generated the expected
  values.
- managed-context-envelope.ts validates boot documents, ready records,
  attestation requests and installation requests, and builds ready
  records, attestation responses and receipts. It reuses the W0a checks
  and digest, which managed-workspace-binding.ts now exports.
- A Java conformance test in runtime-broker pins the key sets, routes,
  constants and error table, and recomputes every installation digest
  with ContextBinding.
- A bilingual design document. The W0a document and the runtime-broker
  README and QWEN.md now point to it.

Refs QwenLM#12380
* feat(sdk-java): project Runtime tool replies onto the broker contract

HttpRuntimeTransport now implements RuntimeTransport. Status and cancel
answers keep only state and a settled result, and session verbs that the
v2 tool contract does not define fail closed.

Co-authored-by: jinye <doudouOUC@users.noreply.github.com>

* feat(sdk-java): adopt local Runtime processes by ready record

The local-process provisioner now reports a durable kind, boots from the
persisted seed, and reconciles a restored lease by attesting it again. A
process that no longer answers is not found instead of guessed ready.

Co-authored-by: jinye <doudouOUC@users.noreply.github.com>

* feat(serve): add the hosted harness profile and broker tool path

qwen serve --profile hosted-harness now requires a loopback bind, a
harness token, and the Runtime Broker origin, token, and capability
digest. The broker client speaks the merged execution routes, and the
local-process provisioner adopts workers by ready record.

Co-authored-by: jinye <doudouOUC@users.noreply.github.com>

* fix(serve): guard incomplete hosted runtime foundations

* fix(serve): close hosted runtime review gaps

* test(core): make exited PID assertion deterministic

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: jinye <doudouOUC@users.noreply.github.com>
Co-authored-by: jinye.djy <jinye.djy@U-Y37CF7G3-1943.local>
Co-authored-by: Shaojin Wen <szujobs@gmail.com>
@doudouOUC
doudouOUC force-pushed the cursor/durable-session-failover-3df3 branch from 3fad955 to 9fdf1e3 Compare September 25, 2026 15:47
doudouOUC and others added 10 commits September 25, 2026 16:30
…nLM#12692)

* feat(managed-agent): add Spring control plane and dual-path WebShell

Bring the standalone managed-agent server, its embedded broker listener,
and the WebShell Java panel onto the current broker contract. Legacy
daemon sessions stay available beside the new path.

Co-authored-by: jinye <doudouOUC@users.noreply.github.com>

* fix(managed-agent): address review defects and wire server CI

* fix(managed-agent): preserve case-sensitive store identities

* fix(managed-agent): bind locally and return 404 for missing routes

* codex: address PR review feedback (QwenLM#12692)

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: jinye <doudouOUC@users.noreply.github.com>
Co-authored-by: jinye.djy <jinye.djy@U-Y37CF7G3-1943.local>
QwenLM#12308)

* feat(serve): support model and reasoning selection at session creation

* fix(serve): allow model-only session startup configuration

* test(serve): include startup capability in registry expectations

* fix(serve): roll back rejected standalone startup

* fix(serve): make definite startup rejections retryable and honest

- Map the ACP internalError wire shape ({code: -32603, data.details})
  produced when switchModel refuses an unregistered model to
  startup_config_rejected on the startup-config call path, so the
  ordinary route answers 422 instead of 500 and the standalone path
  takes its definite rollback branch.
- Roll back the persisted recording of a caller-supplied sessionId on
  POST /session after a definite startup rejection, so a retry of the
  same id is no longer answered with 409 session_id_conflict; uncertain
  failures keep the recording.
- Publish auto in the OpenAPI approvalMode enum and pin it against
  APPROVAL_MODES; narrow the 422/protocol/design cleanup wording to
  match the implemented best-effort semantics; restore the sessionId in
  the new rollback warning.

* fix(serve): keep uncertain startup faults uncertain and roll back ACP recordings (QwenLM#12308)

- session-startup-config: drop the -32603 arm so child-side internal
  errors (auth, credential, timeout, transport) stay uncertain instead
  of a false definite startup_config_rejected; the ACP child now maps
  only its two caller-caused switchModel refusals (unknown model for the
  auth type, media-only primary) to invalid params, so definite
  rejections ride -32602 alone.
- dispatch: roll back the persisted recording of a caller-supplied
  sessionId on the ACP session/new transports after a definite startup
  rejection, mirroring the REST route; uncertain outcomes keep it.
- routes/session: warn once when the recording rollback is refused or
  fails instead of silently discarding the boolean outcome.
- standalone-session-service: extract the shared rollback sequence into
  one helper used by both the startup-config and model_selection_failed
  branches.
- openapi: qualify the 422 close/rollback as best-effort and replace the
  unconfirmable status-view pointer with the self-certifying retry check.

* fix(serve): name the rejected session in startup rollback and align ACP error codes (QwenLM#12308)

- SessionStartupConfigError carries an optional sessionId so both
  rollback gates (REST route, ACP dispatcher) roll back the rejected
  session's recording even when the caller never supplied an id or the
  child did not honor it; a daemon-generated id no longer leaves a
  listed, resumable phantom after a definite 422.
- A refused ACP recording rollback now logs the same inconclusive
  warning the REST route emits instead of dropping the cleanup result.
- startup_config_rejected maps to JSON-RPC -32602 (caller input), with
  data.httpStatus keeping the REST-equivalent 422; SDK transports key
  on httpStatus, so client behavior is unchanged.
- SDK throws DaemonStartupConfigError (TypeError with code
  'invalid_startup_config') for pre-transport startup rejections.
- Docs: modelApplied is documented as spawn-scoped again (an attach
  omits it or reports the spawn owner's outcome).
- Tests: daemon-generated-id rollback pins on both transports, a
  refused-rollback log pin, a service-boundary invalid-config guard
  pin, full reasoning-selection acceptance coverage, whitespace trim
  pins, and a real-ModelsConfig classifier coupling test.

* test(sdk): keep the message assertion beside the stable code on startup rejections (QwenLM#12308)

* fix(serve): fail closed on control-character startup model ids (QwenLM#12308)

A modelServiceId carrying a newline slipped past parseSessionStartupConfig
and produced a core refusal message the single-line classifier regexes
could not match, so a definite caller-caused rejection degraded to an
internal error and skipped the recording rollback. Reject control
characters at the parse boundary and make isCallerCausedModelRefusal
newline-tolerant; align the SDK and integration-guide modelApplied
presence rule with the spawn-scoped wording the bridge and OpenAPI
already publish.

* docs(sdk): name both transport error-code locations in DaemonStartupConfigError JSDoc (QwenLM#12308)

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

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Bring the session authority, JSONL journal, HTTP store client, checkpoint,
and continuation tests over as their own review block. The Java store they
call stays with the Spring control plane.

Co-authored-by: jinye <doudouOUC@users.noreply.github.com>
@doudouOUC
doudouOUC force-pushed the cursor/durable-session-failover-3df3 branch from c2844e6 to 1443597 Compare September 25, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants