Commit Graph
52 Commits
Author SHA1 Message Date
jinyeandQwen-Coder 32d22f950c feat(managed-agent): support durable structured user questions (#16433)
Connect Hosted question actions, structured answers, durable continuation, and the WebShell question form. Cover canonical response replay and pending-question recovery across Harness and Java context restarts.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 09:23:44 +00:00
bc19325983 feat(web-shell): add host mount entries and align managed empty-state layout (#15928)
* feat(web-shell): 增加 managed-host 构建入口与托管会话页

为产品侧集成提供独立的 managed-host 产物(--mode managed-host)与托管会话页:
不挂 daemon workspace/session context,托管会话由 provider 直接渲染列表、会话与输入框。

包含:vite lib 构建模式、managed-host 渲染入口及测试、ManagedSessionsPage 托管分支与
测试、provider 类型调整、README 与中英文设计说明。本次不涉及宿主注入能力。

* feat(web-shell): 托管宿主可注入侧栏导航 props

ManagedAgentWebShell 增加可选导航 props 并原样透传给 ManagedSessionsPage:
navigationTarget / archivedNavigationTarget / onArchivedSessionsStatusChange /
navigationActive / navigationSearch。

宿主据此决定是否把官方会话列表 portal 到自己的侧栏(navigationTarget 为 null 时
仍走 home 布局但不 portal),并同步搜索词与归档计数。类型复用 ManagedSessionsPage
现有 props,不新增重复定义;新增 2 个用例覆盖原样透传与未传时保持 undefined。

* feat(web-shell): add full-page app-host entry and align managed empty-state layout

- Add app-host mount entry (renderWebShellApp) so a non-React host can render the
  complete WebShell (sidebar + welcome + composer) by injecting baseUrl/provider;
  no shell is hand-drawn and no backend address is hardcoded.
- Center the managed empty state like the standalone chat (content shrinks so the
  chatPane centers it) instead of pinning the welcome to the top and composer to
  the bottom; gate the welcome header off when a failed session is selected.
- Surface session failure and unconfirmed-submission notices together above the
  composer, dedupe the same message across channels, and style them as callouts.
- A pending prompt owned by another session now offers a redirect to the owning
  conversation instead of retry/discard from a foreign session; reconcile tests.
- Build app-host with global (unscoped) CSS so root-level state classes match.

* test(web-shell): align managed status smoke assertions

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

* test(web-shell): reach the owning conversation before retrying a foreign pending

A pending prompt owned by another conversation no longer offers retry in the
foreign session: it renders "Open the original conversation" instead. The
spec still clicked "Retry the same request" straight from the foreign session,
so the two foreign variants timed out waiting for a button that is deliberately
absent from this head while the owner variant passed.

Follow the redirect first, then retry. Owner variant unchanged.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvwo8rnm58

* docs(web-shell): document the app-host entry and drop ignored e2e links

The review on head 932a61d8dc read `app-host` as undocumented: no README
section, no mention in either design doc. Document it the same way
`managed-host` is documented -- entry API, build mode, served directory --
and record the unscoped-CSS behaviour as a deliberate decision narrowed to
that bundle, naming the entries that keep scoped CSS. Also state that neither
host mode is part of `npm run build`, which is what the wiring actually does.

Both design docs linked `.qwen/e2e-tests/managed-host-webshell.md`, which
`.gitignore` excludes (`.qwen/*`), so the links are dropped and the
statement they supported stays.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvxdyp0v69

* fix(web-shell): revert breaking changes to existing consumers

- Revert `isChatEmptyState` to `!managedMode &&` (was: `!connection.sessionId && !(managedAgentProvider !== undefined && managedSessionId)`), restoring the original behavior where managed mode is never empty state. The head condition broke existing managed consumers by making them hit empty-state layout when no session was selected.

- Revert `sidebarRailEnabled` to drop `managedAgentProvider === undefined &&`, restoring rail for existing managed consumers. The condition was app-host-specific and should not affect hosts that already pass a provider.

- Revert `ManagedSessionsPage` inline `flex: 0 0 auto` (was unconditional in empty state), restoring `flex: 1 1 auto` for the managed entry. The shrink was only correct when paired with `.appChatEmpty .chatPane { justify-content: center }`, which only exists in the full-page `renderWebShellApp` entry. In `renderManagedAgentWebShell` the class is absent, so the shrink alone produced a 136px content block pinned to the top with 550px dead space below.

- Remove `skipScope` from `vite.lib.config.ts`: the justification ("scoped selectors use [data-web-shell-root] as an ancestor anchor") is factually incorrect. `COMPONENT_SCOPE` includes `:where([data-web-shell-root][data-web-shell-shadcn], ...)` which matches the element itself, so scoped bundles already apply state classes on the root element. Keep scoping for all modes.

- Update README and both design docs to reflect the corrected scoping behavior.

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

* chore(web-shell): remove app-host entry

The app-host entry (renderWebShellApp) had no build wiring (absent from
build script, exports, files, and no CI job invoked --mode app-host), no
test coverage, and hardcoded a specific host's sidebar/footer configuration.
Remove it from this PR; it can be reintroduced in a follow-up with proper
packaging and tests once a named consumer exists.

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

* fix(web-shell): drop unused mode parameter from injectCssModules

The mode parameter became unused after the skipScope removal. Remove it to
fix the ESLint no-unused-vars error.

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

* docs(web-shell): scope the managed-host entry and correct stale layout notes

- README: say plainly that `managed-host` is source-only (no `build`,
  `exports` or `files` wiring, so it is not in the npm package), and record
  that one shell per shadow root is the contract.
- package.json: keep the orphan `dist/types/managed-host.d.ts` out of the
  published tree so the package does not ship a type with no implementation.
- ManagedSessionsPage: point the `summary.failure` dedupe comment at the
  current failure block above the composer, and record why a session switch
  resets failure and discard notices.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy1jgjo76

* docs(web-shell): state the real consequence of the session-switch reset

The comment claimed the list error is re-derived on a session switch. It is
not: the list effect's deps exclude sessionId, so a catalog-load error is
hidden until provider/listRevision changes. Say what actually happens.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy1jgjo76

* fix(web-shell): keep managed sessions visible when workspaces collapse

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

* fix(web-shell): address doudouOUC review feedback

- Remove shadow-DOM style adoption from renderManagedAgentWebShell: the
  shared CSSStyleSheet cache caused cross-contamination when two shells
  shared one shadow root. Hosts using shadow DOM should adopt styles
  themselves or use a plain container.
- Scope the error/discard reset effect to hostSessionOnly only: the
  unconditional clear on [provider, sessionId] hid the catalog-load error
  for existing managed consumers on every session switch without retrying.
- Update the managed-host test to match the removed shadow-DOM behavior.
- Drop the stale shadow-root note from README.

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

* fix(web-shell): preserve styles for managed hosts in shared shadow roots

Restore the documented managed-host ShadowRoot style lifecycle and keep the shared package sheet until its last owner unmounts. Cover both fallback and constructable sheet behavior.

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

* fix(web-shell): center the managed host empty conversation as one group

The host mount entry has no .appChatEmpty ancestor, so the managed empty conversation kept its full-height content box and pinned the composer to the bottom. Give host-mounted empty conversations the same shrink-to-content centering, mount the entry through the managedHome fill wrapper, and drop the empty trailing status row below the composer.

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

* fix(web-shell): let a host-mode session clear through without an echo channel

hostSessionOnly alone marks the shell controlled, so the page's clear
signal after deleting or archiving the current session was dropped when
the host passed no onSessionChange (the README-documented mount). The
deleted conversation stayed on screen and sendable.

Allow the internal write for a clear (undefined) when no echo channel
exists; new selections stay host-owned, preserving the existing
'unrequested selection is suppressed' contract. The bot's suggested
controlled-expression rewrite would have broken that contract, so the
gate lives in the handler instead.

Witness: the new no-echo test fails at 5683eed0 without this change
(sessionId stays 's1') and passes with it; the echo-channel test pins
that the fix does not leak internal writes into echo-owned hosts.

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 16:50:24 +08:00
易良andyiliang114 4700dd2368 fix(web-shell): put the list-failure notice under the tabs, not above them (#16104)
* fix(web-shell): put the list-failure notice under the tabs, not above them

#16082 merged with the notice as the nav's first child, so a failed session load
rendered above the Sessions/Archived tab bar and pushed the list's own controls
down - an error about the list appearing before the list's navigation. It now
renders after the tabs and the title filter, in the region the sessions occupy.

Also stops break-all from cutting the request id mid-token ('req' /
'uest-list-500'); break-words keeps it whole and wraps at a boundary.

Observed in a real browser at 1280x800 against a 500 from /sessions/query:
  before: nav children = [alert y=52, tabs y=180, search y=212]
  after:  nav children = [tabs y=52, search y=84, alert y=128]

The file also carried two prettier violations from the squash merge; prettier
--write normalises them, which is why the diff touches two unrelated indents.

* style(web-shell): keep the diff to the move and the word break

The previous commit ran a newer prettier than this repo pins, which reindented two
unrelated call sites and does not satisfy the repo's own --check. Redone from the
pristine file with the pinned prettier (3.6.1): the net diff against main is now
only the block move and break-all -> break-words.

---------

Co-authored-by: yiliang114 <[email protected]>
2026-11-02 12:50:12 +08:00
9dde2f657d fix(web-shell): surface Managed session list failures (#16082)
* fix(web-shell): surface Managed session list failures

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

* test(web-shell): match the list-failure sentences, not the whole alert

The list-failure alert wraps its message and its Retry button in the same
role=alert div, so textContent reads 'Managed sessions could not be loaded.Retry
loading sessions'. Two assertions compared that concatenation against the
sentence alone, which made four cases fail with a message that looked identical
on both sides.

Assert the sentence is present, the way the sibling HTTP-500 case already does,
and keep the alert count so the 'once' property is still pinned.

Not verified locally: this checkout's core dist predates
managed-runtime/managed-permission-rules.js, so the suite cannot load here. CI
is the first real run.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-02 10:41:58 +08:00
jinyeandQwen-Coder 4afe1a28fd feat(managed): add independent worktrees for main sessions (#15745)
* feat(managed): add independent worktrees for main sessions

* fix(managed): reconcile session worktree contract guards

Join worktree descriptors into session pages while preserving the existing query budgets. Keep schema, CSI inventory, surface captures and lock-order witnesses consistent with the new feature, and memoize cleanup blockers and maintenance operation kinds.

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

* fix(managed): require initial worktree checkout completion

Record checkout completion only after Git succeeds and refuse reentry or verification without the matching durable receipt. Preserve incomplete directories and user edits.

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

* fix(managed): allow shared local project imports

Keep registration metadata private to each local installation so existing projects can be imported without taking over the parent marker. Refuse operations that require exclusive authority over shared storage.

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

* feat(web-shell): streamline managed worktree controls

Show the actual Git branch and use a one-click worktree choice that creates isolation on the first message. Keep ready worktree metadata in session details and match Local sidebar markers while preserving frozen retries and preparation feedback.

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

* fix(managed): address worktree lifecycle regressions

Keep cancellation available before worktree readiness, bind persistence to the Managed catalog, use maintenance-safe cleanup IDs and isolate Git scan scheduling. Refuse new checkouts nested under their source repository while retaining existing cleanup, and repair the affected CI fixtures.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-29 00:55:09 +00:00
jinye 72834cb425 fix(managed-agent)!: simplify Web Shell controls and retire cwd submissions (#15616) 2026-10-26 19:07:39 +08:00
jinye 06d82d786a feat(managed-agent): support Workspace file uploads (#15586) 2026-10-26 01:10:18 +00:00
c1d37384b5 fix(managed): persist session Todo and project committed task lists (#15155)
* feat(serve): give managed workspace sessions web_fetch and todo_write (#15076)

A Managed Workspace Session offered six tools, so a session that needed to
read a changelog or an issue page had to run curl through
run_shell_command and ask for approval every time, and a long multi-step
task had no plan of record. qwen serve sessions already had both tools;
the Managed path never declared them.

The two tools join through new hosted-workspace-files/3 and
hosted-workspace-shell/3 profiles rather than widened /1 or /2 ones.
Admission rests on what a Session's own profile offered, so widening /2
would make a saved Session expose tools its earlier turns never had; this
follows the glob precedent, which added /2 for the same reason.

web_fetch runs in the Worker on the same outbound channel
run_shell_command already uses, and its managed mode keeps the fetch to
one transfer: no response cache, binary payloads refused instead of
written to the Runtime's disk, and no fast-model side query, which is a
second model call the Session's own accounting never sees. todo_write is
Worker-local bookkeeping the model reads back, with no Session Store
schema change and no client projection yet.

Admission validates both tools' arguments before the Runtime is acquired,
so a malformed call costs no durable work. Each approval mode pre-approves
todo_write, which only writes its own list, and asks about web_fetch,
which reaches a URL nobody in the Session chose; the Java action preview
now carries both tools' input, so the URL is visible before the operator
answers.

Resolves #15076

* fix(managed-agent): narrow Hosted tool profiles to Todo

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

* fix(serve): narrow the managed tool profiles to todo_write (#15076)

A maintainer ruling on the issue defers native web fetch, so this branch
delivers the task-list half and drops the fetch half rather than carrying
a contested network-policy decision inside a tool-profile change.

Dropping the fetch is also the technical answer for a files-profile
Session: it has no shell, so a fetch there would open an outbound path
instead of reusing one, and nothing in the Hosted path constrains the
destination — admission checks the scheme, credentials and length but not
the host, and under yolo neither the Worker nor the Harness asks, which
would let a Session reach link-local and metadata endpoints unattended.
The core web-fetch source is back to its pre-PR bytes.

Three review findings are fixed in the surviving half. The Worker now
opts in to todoWriteEnabled explicitly, which core defaults off and the
operator-facing path gates registration on, instead of registering a tool
whose enablement flag says no. The plan-mode read set for the /3 profiles
is gone: it was unreachable because the constructor admits public plan
mode only for files/1, shell/1 and shell/2. And the task list no longer
claims durability it does not have — the Worker's tool set is keyed by a
runtime session id the Harness derives per turn, so the list is scoped to
one turn, and the reminder a local Session shows is injected by the model
loop in the Harness, which never sees the Worker's Config.

The design docs record the delivered slice, why the fetch half is held,
and the remaining work: a destination policy before fetch can return, a
Harness-supplied stable session identity for a list that survives a turn,
Java profile predicates for /3, and a client projection.

Refs #15076

* feat(managed): persist Todo state in the Hosted Session

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

* fix(managed): pass the bare hex contentDigest for the Todo command

writeHostedTodos built `sha256:${managedToolDigest(input)}`, but
assertManagedSessionDigest accepts only /^[0-9a-f]{64}$/, so every
todo_write commit was refused as managed_session_invalid_record and the
turn surfaced it as "Hosted tool turn requires recovery; its original
work was not released." The `sha256:` prefix belongs to the argsDigest
family; every other commitDomainRecord caller passes bare hex.

Verified locally on this head: hosted-workspace-tool-turn.test.ts went
from 21 failed / 377 passed to 20 failed / 378 passed, eliminating
exactly "persists Todo without a Worker and restores the latest state
after reopen" and adding nothing. The remaining 20 all carry the single
message "(0 , unescape) is not a function", this box's known minimatch
baseline. No CI lane covers this branch, because ci.yml's pull_request
filter is main/release/**/omni-experiment and the base here is
feat/15076-managed-web-fetch-todo-write.

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

* test(managed): reconcile recovery coverage for Todo profiles

* fix(serve): keep todo_write out of the provider worker lane

The previous commit registered `todo_write` in both Worker tool sets. The
provider lane is not the lane the hosted profiles use: it serves the
resident and CSI sessions, whose manifests are a closed set of read, write
and edit (plus shell), and the `/2` change that added `glob` left it
untouched for exactly that reason. Registering the tool there widened
every one of those sessions and broke 32 assertions across the resident
manifest and the provider worker suites.

Only the executor lane keeps the tool, which is the lane `createManagedToolSet`
builds for a hosted `/3` session. The provider worker returns to its
pre-change bytes, and both design documents now say which lane opts in.

Refs #15076

* chore(serve): tighten the todo_write slice after an audit round

No behaviour change. Four audit passes over the PR diff found seven wording
and precision defects, each fixed here:

- The `/3` profile comment still said the version adds `web_fetch` and
  `todo_write`; the fetch half is not delivered.
- The approval comment implied a fetch tool exists in a profile and merely
  asks, rather than being absent.
- The admission comment named "a blocked item's own state" as a deferred
  rule. The core tool has no such rule, and it does defer duplicate ids and
  duplicate `blockedBy` references, which the comment now states.
- A single-case `it.each` became the plain test it is.
- The approval test name read "never under yolo alone", which describes the
  opposite of what it asserts.
- My Worker test block had been inserted between an unrelated comment and
  the `relativizeGlobText` describe it documents.
- Both design documents described the removed fetch admission in the present
  tense, and neither said the `todoWriteEnabled` opt-in has no reader inside
  the Worker.

Mutation checks on the fixed tree: forcing `declaresTodoTool()` false fails
11 cases, dropping the admission branch fails 9, dropping the pre-approval
fails 1, and dropping `TodoWriteTool` from `ADMITTED_TOOL_NAMES` fails 1.
Dropping `todoWriteEnabled` fails nothing, which is why the documents now
record that the flag states intent rather than switching behaviour.

Refs #15076

* fix(managed): close the two Critical gaps the audit round found on the Todo slice

R1-2 -- packages/core/src/managed-runtime/managed-session-records.ts

`todo_state` was added to MANAGED_SESSION_ENABLED_DOMAINS but not to
MANAGED_SESSION_ENVELOPE_DOMAINS, and no Stage H body is registered for it
(MANAGED_EXTENSION_RECORD_BODIES has no `todo_state` key). That broke the
partition invariant: managed-extension-projection.test.ts > "partitions the
enabled domains between the bodies and the envelope list" filters the enabled
names by "has no body" and compares the result with toEqual against the
envelope list, so the extra name made it deterministically red.

`todo_state` commits through the envelope path -- hosted-todo-session.ts:38
calls session.authority.commitDomainRecord with { domain: 'todo_state' } --
which is exactly what that list documents, so the name belongs there. It is
inserted at index 1 (after `goal_state`) rather than appended because the
witness compares against MANAGED_SESSION_ENABLED_DOMAINS order, where
`todo_state` follows `goal_state`.

The envelope entry is only valid while no body is registered for the domain:
managed-extension-projection.ts refuses to load over an envelope/body
collision.

Reproduced red before the change (1 failed | 169 skipped of 170,
AssertionError: expected [ 'goal_state', 'todo_state', ...(3) ] to deeply equal
[ 'goal_state', ...(3) ]), green after (170 passed of 170), and removing the
entry reproduces the same AssertionError.

R1-1 -- packages/cli/src/serve/hosted-workspace-tool-turn.ts

The `/3` widening reached hostedApprovalForTurn and both branch gates in
registerHostedHarnessSessionRoutes, including the one that admits a
`planMode` selection, but not the HostedWorkspaceToolTurn constructor, which
still listed only files/1, shell/1 and shell/2. A `hosted-workspace-files/3`
Session with a Plan-mode selection therefore passed the boundary and then
threw Error('Public Plan mode is unavailable for this Session.') at turn
setup, where before the widening the same selection was refused cleanly with
400 invalid_hosted_approval_selection. The constructor now admits the same
five profiles the boundary does.

Widening alone would ship a contradictory round: declarations() filters the
advertised set to exit_plan_mode plus HOSTED_PUBLIC_PLAN_READS
(read_file/glob/skill) while planning, so todo_write is withheld, but
todoInstructions() was gated only on declaresTodoTool() and would still inject
"Keep it current with todo_write". Because the batch-wide refusal converts
every call in a batch to an error response once any request carries a
validationError, that instruction invites a call whose refusal also discards
the legitimate reads issued beside it. todoInstructions() is now gated on
!this.planning as well.

HOSTED_PUBLIC_PLAN_READS is unchanged: todo_write writes, so it does not join
the public plan-mode read set.

The new test constructs a files/3 and a shell/3 turn with publicPlanMode, and
asserts the constructor does not throw, todo_write is absent from
declarations(), todoInstructions() is empty over seeded durable state, a
todo_write call returns the Plan refusal, no Broker work is spent and the
seeded list is still the latest afterwards. Both halves are pinned by a
mutant: removing the added profiles reproduces the constructor throw (2
failed), and removing the planning gate reproduces the contradictory
instruction (2 failed, "expected '# Current Session todo list...' to be ''").

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvk1uqs6qi

* fix(web-shell): drop the dead execution_status guard and pin the flat blockedBy fallback

Three review-driven fixes plus one merge regression caught by the core build:

R1-18: the execution_status pre-filter in getLatestSuccessfulTodos was unreachable (tool.status already derives from the same field), so it could never be the deciding reject; delete it and let the completed/cancelled gate stand alone.

R1-17: session-workflow.spec.ts claimed top-level blockedBy silently produces no edges, but todos.ts:43 now falls back to it; rewrite the comment to state _meta.qwenTodo wins when both are present.

R1-16: every blockedBy fixture nested under _meta.qwenTodo, so the only path that renders Hosted dependency edges was untested; add flat-{blockedBy} and _meta-wins cases.

The PR's move of the blockedBy-preservation block into preserveTodoDependencies also dropped the execute-scope oldTodosMap declaration, which core's TodoCompleted and TodoResultApplication hooks still read; restore it.

* test(cli): drop unused hostedTodoInstructions import and reformat

The R1-18 fix deleted the dead execution_status guard but left the now
unused symbol in the import list, which fails tsc (TS6133) during the CI
prepare step and therefore every TypeScript lane. Also rewrap the two
assertions the same commit left over the 80-column limit.

---------

Co-authored-by: wenshao <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-25 21:43:34 +08:00
jinye a4ce90270e feat(managed): add durable mid-turn user message queue (#15449) 2026-10-25 17:53:56 +08:00
jinyeandQwen-Coder 08a06a46bd fix(web-shell): Prevent duplicate Managed composers (#15496)
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-25 14:08:39 +08:00
jinye 0d7518f0a2 feat(managed): add session permission rules and auto approval (#15448) 2026-10-24 14:14:42 +00:00
jinyeandQwen-Coder ab4b4920ee fix(web-shell): align Managed sidebar with daemon navigation (#15331)
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-23 08:12:42 +00:00
ytahdnand钉萁 69df02bb9a fix(web-shell): preserve draft attachments through refresh and allocation (#15303)
Fix two correctness issues left by PR #15025 after its merge into main:
Managed summary reloads could delete unsent attachment archives, and
session allocation could retain an already accepted draft after removing
a submitted attachment or normalizing touch references.

Keep same-session capability knowledge during reload, reuse the existing
removal-tolerant acceptance predicate for migration, and preserve reference
identity when there is nothing to hoist. Keep strict host continuation
checks and protection for newer input.

Add focused regression coverage and synchronize the bilingual design.

Co-authored-by: 钉萁 <[email protected]>
2026-10-23 14:13:03 +08:00
jinyeandQwen-Coder f0a47611ab refactor(managed-agent): Remove pre-release compatibility paths (#15263)
* refactor(managed-agent): Simplify first-release contracts

Remove unreleased compatibility paths across the managed runtime, Java control plane and WebShell. Keep one fresh V1 baseline and document that schema versions stay at V1 until the first formal release.

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

* test(managed-agent): Align remaining tool fixture with current contract

Restore schema rationale comments while retaining the V1 baseline.

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

* fix(managed-agent): Align CSI proof with current checkpoint encoding

Require the three current nullable checkpoint fields in the native proof, keep malformed and missing fields rejected, and synchronize retained fixture bytes and digest chains. Update tests that still expected retired pre-release state and replay compatibility.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-23 02:09:21 +00:00
ytahdnand钉萁 f18decc7cc fix(web-shell): preserve composer draft attachments across views (#15025)
* fix(web-shell): preserve composer draft attachments across views

* fix(web-shell): preserve draft edits during restore and acceptance

* test: stabilize durable commit and standalone runtime checks

* revert: drop unrelated core extension test change

---------

Co-authored-by: 钉萁 <[email protected]>
2026-10-23 10:06:10 +08:00
jinyeandQwen-Coder 49f1f623c2 feat(managed-agent): Enable public subagent delegation by default (#15171)
Expose qualified public child Sessions with immutable launch policy, aggregated approvals, prompt-scoped stopping and recovery, and WebShell task and transcript views.

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-22 15:32:36 +00:00
jinye 4ee2bb4e47 feat(managed-agent): manage MCP servers without recreating sessions (#15119)
* feat(managed-agent): compose Shell Sessions with pinned MCP servers

* feat(managed): manage MCP servers within existing sessions
2026-10-22 14:07:46 +00:00
9a83f1080e feat(managed-agent): add filtered session title queries (#15157)
* feat(managed-agent): Add session lifecycle UI and reading exports

Expose durable lifecycle actions and archive navigation in Managed Web Shell, with identity-scoped browser recovery and immutable HTML/Markdown reading exports.

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

* fix(managed-agent): Resolve lifecycle review and smoke regressions

Use the host React DOM server renderer, announce global errors once, and advance the OpenAPI version. Keep smoke selectors specific to session navigation and align the Workspace expectation with its existing Managed label.

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

* test(managed-agent): Preserve schema bindings in channel race fixture

Reuse the production schema-aware JDBC template so repeated H2 queries keep binary identity parameters. Preserve the existing ownership race and its assertions.

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

* fix(managed-agent): Correct lifecycle retry and export handling

Keep failed unadmitted archive intents idle until explicit retry, scope approval capability to each session profile, and preserve public export semantics and embedded downloads.

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

* fix(managed-agent): Clarify Markdown export format boundaries

Explain original Markdown source and offline HTML in the shared localized export menu and synchronized designs. Preserve raw Markdown syntax in the export contract regression.

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

* fix(core): Remove duplicate history helper import

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

* feat(managed-agent): add filtered session title queries

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

* feat(web-shell): complete managed session title search

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

* test(ci): Observe lock announcement before blocking

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

* fix(managed-agent): align title query whitespace semantics

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

* test(web-shell): Disambiguate recovery session navigation

Anchor recovery session selectors so lifecycle action buttons do not match the session row.

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

* chore(api): preserve source layout in merged contract

Keep unchanged OpenAPI values byte-for-byte from the source branches without changing the merged contract.

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

---------

Co-authored-by: jinye.djy <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: jinye <[email protected]>
2026-10-22 20:46:33 +08:00
jinyeandQwen-Coder 7773fa313a feat(managed-agent): Page checkpoint history and improve session feedback (#15170)
* feat(managed): Page durable checkpoint history

Preserve complete logical receipts through bounded immutable history pages and shared SQL/native recovery validation.

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

* fix(managed): Restore workspace history maintenance

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

* fix(managed): Explain model API turn failures

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

* fix(web-shell): Show running status for managed sessions

Reuse daemon sidebar indicators and retain confirmed session summaries across navigation and delayed catalog responses. Refresh known running background sessions until they settle.

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

* fix(web-shell): Remove persistent managed composer hints

Keep loading, error and confirmation feedback while hiding routine model and approval explanations below the composer.

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

* docs(managed-agent): Clarify paging rollout and sidebar evidence

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

* test(managed-agent): Align packaged model failure expectations

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-22 12:07:40 +00:00
jinyeandQwen-Coder 51f1d2c8bb fix(managed): Align checkpoint limits and expose recovery blocks (#15094)
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-22 11:37:11 +00:00
jinyeandQwen-Coder 605fd5ee17 feat(managed-agent): Add session lifecycle UI and reading exports (#15100)
* feat(managed-agent): Add session lifecycle UI and reading exports

Expose durable lifecycle actions and archive navigation in Managed Web Shell, with identity-scoped browser recovery and immutable HTML/Markdown reading exports.

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

* fix(managed-agent): Resolve lifecycle review and smoke regressions

Use the host React DOM server renderer, announce global errors once, and advance the OpenAPI version. Keep smoke selectors specific to session navigation and align the Workspace expectation with its existing Managed label.

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

* test(managed-agent): Preserve schema bindings in channel race fixture

Reuse the production schema-aware JDBC template so repeated H2 queries keep binary identity parameters. Preserve the existing ownership race and its assertions.

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

* fix(managed-agent): Correct lifecycle retry and export handling

Keep failed unadmitted archive intents idle until explicit retry, scope approval capability to each session profile, and preserve public export semantics and embedded downloads.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-22 16:37:26 +08:00
jinyeandQwen-Coder 59d21e1c69 feat(managed-agent): Safely settle recovery and continue sessions (#15131)
* feat(managed-agent): Safely settle recovery and continue sessions

Require original-input settlement, continuation readiness and completed cleanup before reopening a Managed Session. Add conservative recovery queries and rechecks, original-target cancellation, reload-safe operation tracking and editable bounded handoff drafts.

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

* test(managed-agent): Avoid initial fixture output backpressure

Capture the native generator output in a JUnit temporary file so waiting for its exit cannot fill an unread pipe. Preserve the 30-second deadline and all recovery assertions, and reap the generator when an assertion or interruption ends the wait.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-22 06:06:57 +00:00
jinyeandQwen-Coder d03b3116da feat(managed-agent): Add durable manual context compression (#15104)
* feat(managed-agent): Add durable manual context compression

Add idle-only public and WebShell compression operations with fenced native
maintenance, atomic journal results and compressed model-history recovery.
Preserve the raw transcript, pending input and tool results across retries.

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

* test(managed-agent): Align assertions and pin compression Hook recovery

Match the permission tooltip and the existing Managed Workspace label while
preserving authorization and registration assertions. Verify that an unknown
Hook outcome retains the durable generating phase and recovery barrier.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-22 08:09:13 +08:00
jinyeandQwen-Coder bca0cf80b9 feat(web-shell): unify daemon and managed workspace navigation (#15061)
* feat(web-shell): unify daemon and managed workspace navigation

Extract shared workspace navigation, task target selection and execution choice
from the stacked integration so hosts can use them with the existing Managed
provider on main.

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

* fix(web-shell): keep workspace directories stable during refresh

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-21 14:23:48 +00:00
jinyeandQwen-Coder ed0d824e8c feat(web-shell): support managed session rename (#14987)
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-21 14:22:32 +08:00
jinyeandqwen-code-dev-bot d2f2895fab feat(managed-agent): import enabled skills at session creation (#14930)
* feat(managed-agent): import enabled Skills when creating sessions

* fix(managed-agent): bump contract version for session skills

* codex: address PR review feedback (#14930)

* codex: address PR review feedback (#14930)

* codex: address PR review feedback (#14930)

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-10-21 04:16:25 +00:00
易良andQwen-Coder 827bf1b76e fix(web-shell): align Managed Session titles, file access and the Workspace row (#14924)
* fix(web-shell): derive the title on Workspace-bound Session create

The bound create path omitted the title, so the Session list and header fell back to a UUID prefix while the standard shell shows a prompt-derived title. Send the same bounded, control-character-stripped title the unbound create already uses.

* fix(web-shell): treat unsupported Workspace file access as a capability

A 409 workspace_files_unsupported from the mount-time probe is a deployment capability, not a transient failure: the composer simply has no file-reference actions. Stop storing the broker's English message and rendering it as a permanent alert; other probe failures still surface.

* fix(web-shell): compact the bound Workspace row

The home layout printed the workspace id and relative directory twice. Keep one summary line (id truncated in place with the full value in the title, then cwd, model and runtime) and leave only the cwd switch and the execution-unavailable notice in the body. The standalone layout keeps its labeled lines.

* test(web-shell): pin the compact Workspace row

* fix(web-shell): surface file-access refusals after the mount probe

Only the mount probe treats workspace_files_unsupported as an absent capability; the same code also covers a broker that failed mid-request, so a refusal from a later read must still report. Clear the probe's failure instead of guarding the shared sink.

* fix(web-shell): slice the Managed Session title cap by code point

titleFor capped at 80 UTF-16 code units, so a prompt whose astral
character started at code-unit index 76 was cut between its surrogates.
Sending a title from the bound create made that cap reachable for bound
Sessions: the server accepts a lone surrogate (validTitle rejects only
length > 256 and control characters) and no store column keeps it, so
the prompt tail was persisted as '?'.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvhb9cysmc

* refactor(web-shell): reuse object() in the file-access guard

isFileAccessUnsupported hand-rolled the unknown-to-record narrowing that
object() already provides in this module, and dropped its array
exclusion. Both forms answer identically for every value the provider
can throw, so this only removes the second idiom.

Also pin that the suppression keys on the error code rather than the 409
status. The same mount probe answers 409 workspace_file_unavailable for a
fenced or draining Workspace, and that must still surface; nothing
distinguished the two, because the only negative case varied status and
code together.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvhb9cysmc

* test(web-shell): pin that the compact summary stays out of standalone

The standalone-layout case asserted only what that layout gains, so
removing the home && guard on the summary kept the suite green while
duplicating the compact row above the two labeled lines, inside a plain
div. The home-layout case already asserts the summary exists.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvhb9cysmc

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-21 11:06:33 +08:00
jinyeandQwen-Coder 26aaa35afe feat(managed-agent): Support approval changes during a turn (#14920)
* fix(web-shell): Select a named model for managed tasks

Remove the redundant server-default entry when model presets are available and submit the displayed preset Agent.

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

* feat(managed-agent): Support approval changes during a turn

Read the durable session policy at subsequent permission boundaries and release eligible pending actions through the existing decision journal. Keep Plan controls, admission snapshots, writer fencing, and legacy provider behavior intact.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-20 23:28:00 +00:00
jinye 888d80815b feat(managed-agent): expose session MCP catalog in WebShell (#14916) 2026-10-20 17:09:12 +00:00
jinye d55aa8a3e8 fix(managed-agent): load saved tool details on expansion (#14604)
* fix(managed-agent): show bounded tool details in session history

* fix(managed-agent): load saved tool details on expansion

* fix(managed-agent): advance tool detail contract version

* fix(managed-agent): remove tool detail feature switch

* fix(managed-agent): preserve saved structured shell results
2026-10-19 09:26:02 +00:00
易良andQwen-Coder fb8e9a4512 perf(web-shell): incrementally project managed session messages (#14562)
* perf(web-shell): incrementally project managed session messages

Reuse trusted append prefixes while rebuilding replacement, prepend and sibling histories and preserving independent output snapshots.

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

* test(web-shell): pin the projector's unregistered-array rebuild

The nextEpoch !== undefined guard in createManagedMessageProjector keeps
two consecutive raw transcript arrays (equal length, no merge lineage)
from being treated as one append chain: without it the projector reuses
the first projection and shows stale text. use-managed-session's
snapshot path stores transcript.events directly, so the second read is
exactly such an unregistered array. No existing test failed when the
guard was removed, so this pins it with the smallest failing-first
regression: two raw arrays, same length, different text, the second
content must win.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-19 17:20:04 +08:00
jinye 084078b0d2 feat(managed-agent): raise attachment input budget to 8 MiB (#14628) 2026-10-19 06:43:38 +00:00
jinye 3e9d22f14b feat(managed-agent): support workspace file references (#14639) 2026-10-19 05:11:32 +00:00
jinye 0df419d898 feat(managed-agent): load pinned Skills from the composer (#14602) 2026-10-19 02:29:38 +00:00
jinye 97ebffaa70 feat(managed-agent): support Plan mode and inline attachments (#14531) 2026-10-18 16:58:07 +08:00
jinyeandQwen-Coder ea6d67390a feat(managed-agent): Improve workspace selection and tool progress (#14405)
* feat(managed-agent): Improve workspace selection and tool progress

Allow selecting existing authorized Linux project directories, expose safe tool lifecycle progress, honor the configured model-round limit, and reuse daemon WebShell presentation.

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

* fix(managed): Allow late model rounds to persist and recover

Remove obsolete round ceilings from persistence and continuation validation. Cover late rounds with recovery regressions and complete real-model prompt evidence.

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

* test(managed-agent): Order workspace contract imports

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

* fix(managed-agent): Correct directory truncation reporting

Count an omitted eligible directory before marking the page truncated while preserving the raw-entry scan bound. Pin the unsafe-selection refusal codes and add directory limit regression coverage.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-17 10:35:25 +08:00
jinyeandQwen-Coder af9bd8ed05 feat(managed-agent): add Shell Full Access and session approval switching (#14323)
* feat(web-shell): integrate Managed session workflow

* docs(managed-agent): align model-switch acceptance translations

* fix(managed-agent): reconcile WebShell integration checks

* fix(managed-agent): address WebShell review feedback

* fix(managed-agent): align schema projections and CI probes

* feat(managed-agent): add Shell Full Access and approval switching

Freeze approval selection per Turn, preserve legacy retry identity, and allow owner-authorized idle policy changes with durable WebShell confirmation.

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

* feat(web-shell): integrate Managed session workflow

* docs(managed-agent): align model-switch acceptance translations

* fix(managed-agent): reconcile WebShell integration checks

* fix(managed-agent): address WebShell review feedback

* fix(managed-agent): align schema projections and CI probes

* feat(web-shell): integrate Managed session workflow

* docs(managed-agent): align model-switch acceptance translations

* fix(managed-agent): reconcile WebShell integration checks

* fix(managed-agent): address WebShell review feedback

* fix(managed-agent): align schema projections and CI probes

* fix(web-shell): scope Local approval overlays to the active backend

* test(web-shell): refresh transcript size evidence after main sync

* feat(web-shell): integrate Managed session workflow

* docs(managed-agent): align model-switch acceptance translations

* fix(managed-agent): reconcile WebShell integration checks

* fix(managed-agent): address WebShell review feedback

* fix(managed-agent): align schema projections and CI probes

* fix(web-shell): scope Local approval overlays to the active backend

* test(web-shell): refresh transcript size evidence after main sync

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-16 09:21:53 +00:00
jinye f9df3df1ef feat(web-shell): integrate Managed sessions into the home workflow (#14316)
* feat(web-shell): integrate Managed session workflow

* docs(managed-agent): align model-switch acceptance translations

* fix(managed-agent): reconcile WebShell integration checks

* fix(managed-agent): address WebShell review feedback

* fix(managed-agent): align schema projections and CI probes
2026-10-16 14:12:04 +08:00
72316e6608 feat(web-shell): Reuse standard UI for Managed Agents (#13973)
* feat(web-shell): Reuse standard UI for Managed Agents

Reuse native chat, model and Workspace controls for Java Managed Agent sessions while preserving admission retries and scoped drafts.

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

* fix(web-shell): Clear accepted drafts after editor remount

Keep accepted input ownership across session switches and response-loss
retries. Snapshot original editor text in the local pending record so a
reload can clear matching owner storage without an in-memory callback.
Preserve original commits, foreign drafts and newer different drafts.

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

* fix(web-shell): Clarify abandoned first-input admission

Warn that first input may already be running and a new request can repeat
its effects. Refresh the owning Session list after local abandonment and
show uncertain recovery controls only once the active request settles.
Add the focused review regressions for composer input and standalone UI.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-10-13 07:30:12 +00:00
jinyeandQwen-Coder b675c97461 feat(web-shell): add Managed child Agent task panel (#13936)
* feat(web-shell): add Managed child Agent task panel

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

* docs(web-shell): record child task panel acceptance evidence

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

* fix(web-shell): keep task panel copy out of transcript exports

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

* fix(web-shell): synchronize task cancellation settlement

Publish validated task snapshots before clearing cancellation recovery, keep known terminal state against delayed reads, and clear obsolete admission contention once the task ends. Keep background task polling quiet and align detail timestamps and labels with the panel language.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-12 13:01:51 +00:00
jinyeandQwen-Coder 6936c77e60 feat(managed-agent): support WebShell session cwd changes (#13760)
* feat(web-shell): support managed session cwd changes

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

* feat(managed-agent): advertise WebShell cwd change capability

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

* chore(managed-agent): consolidate W2 WebShell delivery

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

* fix(web-shell): compose abort signals without AbortSignal.any below Chrome 116

refreshSummary composed the caller signal with the hook lifetime via an
unconditional AbortSignal.any, which is Chrome 116 / Safari 17.4 while the
package floor is Chrome 111 / Safari 16.4. On those browsers a successful
cwd change threw TypeError on the completion readback, was booked as
unconfirmed, and left the composer locked for the lifetime of the tab.

Mirror the composeAbortSignals feature-detect from acpTransportUtils and
rebase onto main: the cwdChange capability becomes OpenAPI v1.36 (v1.34 is
reserved for actor roles, v1.35 landed first).

* refactor(managed-agent): address triage nits

- managed.cwd.unconfirmedForbidden now tells the user sending stays
  blocked until the server settles the operation or a new tab is opened.
- cwdShaped collects to HashSet explicitly since retainAll mutates it;
  Collectors.toSet makes no mutability promise.
- Drop the never-read cwd field from ManagedSessionCwdControl basis state.

* fix(web-shell): repair CI after rebase

Prettier rejected the conflict-resolved OpenAPI JSON (the closing brace
stayed glued to the description line), and the transcript entry grew past
its byte ceiling: main measures 1,320,579 and this branch 1,323,120
against a 1,323,000 gate. The growth is the cwd slice's shared i18n
strings; no interactive cwd code enters the transcript graph. Raise the
ceiling to 1,324,000 with the measurements recorded, matching the gate's
documented ratchet practice.

* chore(managed-agent): renumber cwd capability to OpenAPI v1.37

Main took v1.36 for the replay-floor pass (#13621) while this branch was
in review, so the cwdChange capability moves to v1.37 and the design docs
follow. Also drop the pre-rebase SHA from the size-gate docblock; the
measurement is unchanged at 1,323,120 on the rebased branch.

* feat(managed-agent): converge cwd capability on the #13545 role model

Main's #13545 moved cwd admission from creator identity to
atLeast(OPERATOR) while this branch was in review. The capability
predicate now mirrors exactly that: one role grant read covers the
workspaces of the cwd- and Turn-shaped candidate union, the
execution-facts read covers the union of both id sets, and the facts
batch is skipped when the caller holds no OPERATOR grant — the same
zero-query shape the submit path has.

The OpenAPI entry becomes v1.38 (main took v1.37 for the roles slice).
Capability tests move to the role vocabulary: a non-creator OPERATOR now
sees cwdChange while a READER does not, and the no-facts-read cases run
under a READER-only caller. Design docs record the convergence.

* fix(managed-agent): drop duplicate registry-facts interface declarations

Textual auto-merge with #13545 concatenated this branch's fail-closed
default methods with main's abstract declarations of the same two
AgentStateStore methods, breaking compilation. Main's abstract
declarations win: ManagedAgentStore is the only implementation and
overrides both. The design docs' fail-closed-default note goes with the
removed methods.

* fix(web-shell): release a parked cwd intent on definitive replay refusals

A lost-ACK intent has no operationId, so Confirm is the only way out —
but definitive was gated on initialSubmission, so a confirm replay could
never classify a refusal as final and the composer stayed locked for the
tab's lifetime. The store's actor-scoped idempotency lookup runs before
the role, revision and busy gates, so a 4xx behind it proves the original
submit was never recorded. Drop the initialSubmission conjunct and
release with the real refusal code; 401 and the pre-lookup read-visibility
404 stay non-definitive (they prove nothing about recording), as do
transient 408/429. The query path starts accepted=true, so transient
mid-poll refusals still retain an admitted operation's intent.

R1-12.

* docs(managed-agent): align the cwdChange contract text with the role model

The field description still said the caller must be the Session creator,
contradicting the role-based predicate shipped in the same diff; the
WebShell design docs still recorded #13545 as open with a pre-rebase
baseline. Both now match the converged role model, and the generated
client is refreshed. The composeSignals fallback test also pins the
hook-lifetime half of the composition, not just the caller signal.

R1-2, R1-3, R1-22.

* chore(managed-agent): renumber cwd capability to OpenAPI v1.39

Main took v1.38 for the automation runtime (#13598); the cwdChange entry
moves to v1.39 and the design docs follow. Transcript entry measures
1,323,162 against the 1,324,000 gate on the rebased branch.

* codex: address PR review feedback (#13760)

* codex: address PR review feedback (#13760)

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-11 04:55:03 +00:00
e9582993bf fix(managed-agent): harden the managed panel failure lifecycle and worker path containment (#13179)
* fix(managed-agent): harden commit retry, worker containment, panel polling

Managed sessions fail-closed on journal write errors, but the plain
transactions commit tolerated no uncertainty: one dropped receipt
(timeout or 5xx) parked the session authority in a permanent write
failure. The broker already dedupes commits by command key and replays
their receipts, so retrying uncertain failures is safe; do it on both
commit paths with one shared predicate.

In the managed runtime worker, a relative file path was resolved against
the session directory without verifying containment, while the shell
directory argument already required the same admitted-workspace check.
Reject paths that resolve outside the registered workspace.

The managed panel ran three independent fixed-3000ms loops (bootstrap
snapshot, event-stream reconnect, summary poll) with no ceiling, no
jitter, and no stop for definite 4xx answers. Give them a shared backoff
(3s floor, 30s cap, jittered) and stop on non-retryable client errors.

Merging streamed events rebuilt and re-sorted the whole transcript per
delta; append strictly-newer tails directly and keep the general
dedupe-and-sort path only for overlapping or disordered input.

* fix(managed-agent): defer the plain-commit retry until fault-gate drivers allow it

The hosted fault-gate drivers pin a single commit attempt per targeted
fault (hosted-store-failure-driver 'Harness must not retry the failed
write', hosted-shell-output-driver injections counters), so the retry
added for plain /transactions:commit must land together with the driver
restatement of bounded, byte-identical, effect-once retry semantics.
Tracked in the retry-terminal-states issue; revert it here to keep this
PR small and green.

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

* fix(web-shell): reset managed stream backoff on delivered events

When an SSE connection dies by proxy idle timeout it surfaces as a
status-less throw, so the stream loop's end-of-iteration reset never ran
and the shared failure counter climbed to the 30s cap for the life of
the panel even though every reconnect succeeded and delivered events.
Reset on progress inside the for-await body, matching the bootstrap and
summary loops' notion of success.

Also pin previously unwitnessed review invariants: the empty-incoming
merge case (a load-bearing crash guard, not an optimization), the exact
exponential backoff ladder under a deterministic random, the stream
loop's non-retryable exit and its unchanged resubscribe cursor, and
comments documenting the exact-3s first-failure floor and the merge
fast path's sorted-input contract.

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

* fix(web-shell): track managed stream, snapshot, and poller health separately

A connection delivering only replayed duplicates, or delivering nothing
while simply staying alive, now also resets the reconnect ladder: the
old progress-only reset treated every proxy idle-timeout death as a
failure and let the delay climb to the cap on healthy quiet sessions.
Gap-recovery snapshots climb their own failure ladder instead of being
charged to (and constantly zeroed by) counter that stream delivery owns,
and a terminal non-retryable stop is now recorded in a sticky
stoppedReason that per-event updates cannot erase — previously a 4xx on
the summary poll left the composer frozen with no banner after the next
stream event cleared the transient error.

Tests pin each merged-event guard clause (ascending input, strictly-newer
boundary, and the empty-current short-circuit) with its own mutation
witness, and the new mock generators declare the full subscribe options
shape so they typecheck outside the repo's blind tsconfig gates.

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

* fix(web-shell): keep managed streams alive across definite snapshot answers

A transcript-only 4xx from the gap-snapshot read no longer tears down a
live event stream: the stop is recorded but the loop keeps resubscribing
behind the snapshot ladder. The sticky terminal reason now retires when
a later authoritative read succeeds instead of staying painted over a
session that demonstrably recovered, and wins the alert chain over a
newer transient error so a surviving loop cannot mask the hook's
permanent decision. The four fail-classify-stop sites share one closure
instead of four verbatim copies, and the hook specs mount through the
shared reactHarness primitives with one deterministic-backoff helper.

New pins: bootstrap and stream ladder growth across consecutive
undelivered rejections, gap-snapshot ladder reset after a healed outage,
gap-snapshot 4xx recorded while the stream stays subscribed, stop-reason
precedence and persistence at the only production render site, and
retirement of the reason by a later successful read.

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

* fix(web-shell): render managed action errors beside the terminal stop

A sticky terminal stop in the alert chain silently swallowed every
page-level action error: with the panel still interactive, a rejected
submit, cancel, or list load replaced nothing, so the user saw the stale
stop reason above a visibly live transcript with no trace of the fresh
failure. The page's own error now renders as its sibling element — the
terminal reason keeps the first positional slot that existing alerts
read through, and the hook's transient error stays behind it.

An expired credential is no longer terminal anywhere either: this client
re-derives its headers on every request, so a short-lived-token lapse is
answered fresh by the very next attempt once the host refreshes — the
loops back off and recover instead of wedging behind a banner until a
manual Refresh. Definite 4xx answers for session existence stay terminal
and still self-retire when a later authoritative read succeeds.

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

* chore(vscode-ide-companion): restore main's NOTICES.txt

The merge commit absorbed a NOTICES regeneration produced from my local
worktree, whose pnpm store resolves a few transitive versions differently
from CI's canonical frozen-lockfile environment (is-fullwidth-code-point,
fdir, picomatch, get-east-asian-width). The lane regenerates from a clean
install and therefore disagreed with the checked-in file.

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

* fix(web-shell): retire managed stream stops through their own authority

R6's record-and-continue could park a terminal banner that nothing ever
retired: a single definite 4xx on the summary poll stayed red over a
visibly healthy stream for the life of the mount, and a session-leg stop
during bootstrap left a live summary beside a permanently frozen stream
with no warning. Every verdict now carries the authority that issued it,
every successful read retires only its own leg's sticky reason (plus the
transient error, so the page cannot display the same text twice), and a
session-leg success after a terminal bootstrap stop re-arms the effect so
subscribing restarts instead of silently staying dead. A transcript-leg
verdict survives any number of session-leg successes — a live stream
cannot certify history.

Specs pin both shapes and the origin match itself; the positional page
alert test's unmocked jitter is pinned to the maximum rung so the
recovering poll lands after both positional reads.

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

* fix(web-shell): record managed health signals per answer authority

The failure model kept one sticky slot plus one transient error, so a
terminal verdict was retired by any successful read and a page-fetch
failure could be swallowed once other signals arrived. Two review
Criticals converged on the same root: health is per-authority, not
global.

- Keep one signal per answer authority (session / transcript / stream),
  each carrying its message, finality, and a sequence number. The
  exported stoppedReason/stoppedLeg derive from the newest final signal
  and the transient error from the newest non-final one, so a terminal
  verdict is cleared only by a later success of the same leg, and a
  newer transient can no longer hide a sticky verdict (or vice versa).
- A failed older-page fetch now lands in the transcript leg's slot
  instead of a parallel error field that derivation would have hidden;
  the next successful page or gap resync retires it. The state-level
  error field is gone — the records are the single source.
- The stream's own failures (subscribe rejections, stall verdicts) live
  in the stream leg; a resync that advances the cursor retires them.
- Snapshot reads settle with Promise.allSettled and reject through a
  typed SnapshotLegError that forwards status/code, so the session
  leg's answer always decides bootstrap termination regardless of
  settle order, without mutating foreign error objects.

Verification: managed suite 299/299 across 15 files; five mutants
(status/code forwarding, classification order, stream catch leg, stream
retirement on advance, page-failure leg) each redden exactly their
witness spec; eslint, prettier, and tsc --build clean.

* fix(web-shell): let the managed stream retire its own verdict on delivery

Answering the remaining R8-1 gap the per-authority records left open: a
definite stream answer is now retired when the stream itself recovers,
and a standing verdict can no longer be displaced by a blip on a less
specific authority.

- A genuinely new delivered frame retires the stream leg's records —
  its own recovery retires its own verdict. Replays and gap frames are
  not recovery evidence and do not count.
- The displayed sticky verdict is picked by authority precedence
  (stream > transcript > session) instead of newest-final-wins, so a
  definite 404 blip on the summary poll no longer forces a standing
  "history pruned" banner out of view for the blip's whole interval.

The four R8-1 entrances are pinned one spec each, every one confirmed
by its own mutation: a stream transient failure now stays visible
through healthy summary polls (M: poll also retiring the stream leg
reds it), a definite stream verdict clears when the reconnect delivers
new data (M: no delivery retirement), a transcript verdict stands
through a session-leg blip and after its recovery (M: poll also
retiring the transcript leg), and a failed "Older history" click stays
visible through later poll cadences (M: same), with a fifth spec
pinning the precedence rule itself (M: newest-final-wins).

Verification: managed suite 304/304 across 15 files; all nine mutants
red exactly their witness spec and the restored tree is green; eslint,
prettier, and tsc --build clean.

* fix(web-shell): guard managed verdict finality and clear loading on every recorded failure

Two R9 Criticals in the per-authority signal layer:

- The fail-to-record rewrite dropped the loading:false the old helper
  carried, so no failure path cleared the bootstrap loading flag once a
  snapshot could never succeed: a terminal 404 (or a transcript leg
  retrying forever) left the transcript skeleton, the "Loading..."
  status, and the suppressed progress line up for the life of the
  mount. Every recorded failure now ends any in-flight read at the one
  shared writer.
- The write layer had no monotonicity guard on final: a weaker later
  failure on the same leg (a network TypeError, 401/403, a failed
  "Older history" click, a stall assertion) overwrote a standing
  terminal verdict with a transient record, and retire() — the only
  clearing path — never re-asserted it. Verdicts are now monotonic at
  the write: a final entry cannot be downgraded by a non-final failure,
  and retire() stays the only downgrade path. The loadOlder catch,
  which lives outside the effect closure, applies the same
  classification and guard in its mirrored write instead of hardcoding
  final: false, and the stall assertion routes through the same
  classification helper as every other stream-leg failure.

Pins: loading:false assertions in the three bootstrap-failure scenarios
the review named plus the transcript-leg-only scenario where no success
path ever runs (the named transcript-verdict spec keeps its assertion,
but its success-first shape means it cannot red the mutant — the
red-able transcript-leg pin is the new one); two monotonicity witnesses
(a definite stream verdict surviving later transient failures; a resync
verdict surviving a failed page fetch). Four mutants — unguarded
record(), unguarded/unclassified loadOlder catch, no loading clear,
session-leg page failure — each red every one of their witness specs.

Verification: managed suite 306/306 across 15 files; eslint, prettier,
and tsc --build clean.

* fix(web-shell): let every managed read authority retire its own verdict on its own success evidence

Two R10 Criticals against the per-authority signal layer:

- A failed "Older history" click now stays transient even on a definite
  4xx. Classifying it terminal had no heal path: the transcript leg
  retires only on a snapshot success or a later click, so a one-second
  store failover under a live panel produced a permanent "session not
  found" alert that also masked later genuine session verdicts by
  precedence. A click is one-off evidence with no ladder asking again;
  the poll certifies a genuinely gone session on its own leg. The
  monotonicity guard is untouched.
- Verdicts expired on evidence narrower than "this leg answered
  successfully", so a terminal record outlived its failure. The
  retirement contract now separates the leg's success from the delivery
  of new data: a fulfilled transcript read retires its stale verdict
  even when the session leg of the same snapshot rejected (the read was
  previously discarded before either retirement site); a subscription
  attempt that stays error-free for the backoff-certifying duration
  expires a terminal stream verdict (replays and parked-idle
  connections count as answers; failed reconnects never do, and
  transient records are never expired); and an error-free clean close
  does the same for reconnect patterns that never idle long enough.

Pins: a 404 page click now surfaces as a transient error only (mutant:
classification restored reds it); a stale resync verdict clears on the
next resync whose transcript leg fulfills (mutant: discarding the
fulfilled read reds it); a terminal stream verdict expires on a
replay-only reconnect past the proof-of-life point, and on an
error-free clean close (mutants: no expiry on either path reds each).
The final-monotonicity, loading-clear, and page-leg mutants from the
prior round were re-run against the moved sites and still red their
witnesses. Constraints held: non-final stream records survive replay
parks, terminal verdicts survive later failed reconnects, and the
session-first classification order plus the replay/not-new frame rule
are unchanged.

Verification: managed suite 310/310 across 15 files; eslint, prettier,
and tsc --build clean.

* fix(web-shell): retire managed verdicts on any answered read and restore them when the answering attempt fails

Two review Criticals against the signal layer's retirement contract, plus
the round-4 real-stack F1:

- A leg's verdict still expired on evidence narrower than that leg
  answering. The transcript leg had no recurring success evidence at all:
  retire('transcript') ran only inside snapshot(), so once a gap resync
  took a definite 4xx and the stream healed by delivering new frames
  without ever gapping again, the verdict stood for the life of the mount
  and masked a later genuine session verdict by precedence. A genuinely
  new frame now retires the transcript leg too, which also clears a
  failed "Older history" click on the next delivery (the merge base's
  behavior). And a resync that answers but cannot advance the cursor is
  the stream leg answering: the stall branch expires a standing terminal
  stream verdict first, or the monotonicity guard would block the stall
  warning itself.
- The proof-of-life timer measured from attempt start, so a reconnect
  that spent three seconds failing expired a standing terminal verdict on
  evidence the contract excludes ("failed reconnects never count"), and
  the removal disarmed the monotonicity guard that would have blocked the
  weaker transient write when the throw finally landed. The timer now
  captures the standing verdict from a synchronous mirror ref at fire
  time (a state updater runs at React's flush and can lose that race) and
  the stream catch restores it before recording the failure; a genuinely
  new frame in between drops the capture, since live data refutes the
  verdict for good.
- Proof-of-life and error-free clean closes now expire the stream leg's
  transient records as well as terminal ones (F1: a recovered 502/401
  line no longer waits for the next event), with the stall warning marked
  and exempt — an idle connection that never advances is exactly the
  condition it describes.

Also pins the worker containment guard on the read side (round-1
follow-up): a read_file call escaping the workspace by traversal or by an
in-workspace symlink is refused; reads never pass through file history,
so the executor check is their only guard.

Pins: four new hook specs (terminal verdict expiring across non-advancing
resyncs; terminal verdict restored when a reconnect fails past the
proof-of-life point; transient stream failure expiring on an error-free
idle reconnect; stall warning surviving an idle connection) plus the
read-side containment test. Each removal mutant reds its witness: no
transcript retirement on delivery, no stall-branch expiry, no catch-side
restore, final-only expiry, no stall exemption, and a read_file exemption
from the guard. The round-9 resync-verdict spec now clicks before the
reconnect: the guard half is unchanged, and the delivery half asserts the
retirement this round makes the contract.

Verification: managed suite 314/314 across 15 files; cli serve managed
suites 2269 passed (4 pre-existing skips) across 21 files; npm run build,
typecheck, and lint clean.

* fix(web-shell): gate the managed proof-of-life verdict expiry on an answered attempt

The proof-of-life timer expired the stream leg's standing verdict on wall
time alone, so a reconnect that opened and went silent — a proxy
black-holing the response body keeps the fetch in flight indefinitely —
erased a terminal verdict or transient failure and left the panel
rendering no alert over a dead stream. An open-but-silent connection is
not the leg's success evidence: the expiry now fires only once the
attempt has actually delivered a frame (a replay counts, since delivered
is set before the replay guard), keeping the mirror capture and the
catch-side restore unchanged.

The transient expiry spec is restored to an answered reconnect (it had
been rewritten to a quiet one, blessing the regression), and two new
specs pin the unanswered polarity for both a terminal verdict and a
transient failure; each goes red without the gate.

Verification: managed suite 332/332; mutation probe (gate removed) reds
exactly the two new specs; npm run build, typecheck, and lint clean.

* fix(web-shell): expire stream verdicts on heartbeat-answered idle reconnects (#13179)

On an idle Session the server sends no frames after the resume head,
only :keepalive comments, which the client dropped — so the R11-1
proof-of-life gate (expire a standing stream verdict only once the
reconnect has answered) never fired, and a one-off 404/502, an outage,
or a 401 period kept its banner until the next event.

streamEvents now reports each complete heartbeat/comment frame through a
new onAlive callback (a trailing fragment is a mid-frame disconnect and
is not reported), the Java provider forwards it, and the hook counts it
as the attempt's answer: it marks the attempt delivered and, when the
proof-of-life point has already passed, lands the expiry the timer was
holding. The black-hole guarantee is unchanged — a silent connection
never sends a heartbeat — and the catch-side verdict restore still
covers a keep-alive-credited attempt that goes on to fail.

* fix(web-shell): never resurrect a heartbeat-expired stream verdict (#13179)

A keep-alive landed the proof-of-life expiry but left the attempt's
verdict capture armed, so a later drop of that certified connection
restored the terminal verdict the heartbeat had refuted, and the
monotonicity guard then hid the real network error. Disarm the capture
on every heartbeat and never arm it for a heartbeat-certified attempt.

Also: a heartbeat no longer resets the stream reconnect ladder (it
certifies the connection, not the data path), and the session page
renders the hook's stoppedReason and error channels as sibling alerts
instead of collapsing them, suppressing exact repeats so one server
condition is announced once.

Pins the catch-side verdict restore (replay shape), the new-frame
disarm past the proof-of-life point, the verdict mirror clears, and a
pre-proof-of-life heartbeat's early-expiry gate.

* fix(web-shell): tighten the signal-leg ledger and alert de-duplication (#13179)

* fix(web-shell): land the proof-of-life expiry on late frames and bound re-arms (#13179)

A reconnect whose first frame lands past its own proof-of-life point —
a slow accept replaying the tail from lastEventId — set delivered but
never landed the expiry the point was holding, so a terminal verdict
the stream had already refuted kept rendering over a live, answered
connection. The frame path now mirrors the heartbeat path: a frame past
the point lands the expiry (a replay counts) and it is never restored
by that connection's later drop.

Also: when both bootstrap reads reject, a definite transcript answer is
now recorded on its own leg before the session error's throw discards
it, provided the session leg is transient and will retry (a terminal
session answer keeps owning the display); the session-leg re-arm is
bounded per session so an alternating definite-4xx/success backend can
no longer remount the effect every rung-zero cadence forever; the stall
guard drops an operand no observation can reach (every stall
re-assertion is preceded by the same pass's successful resync, which
retires any competing record first); SnapshotLegError loses its
never-read code field; the bootstrap catch reuses failed()'s
terminality predicate; and the signals doc now states the cross-leg
rule the frame path actually implements.

Pins the late-replay expiry, the both-reject classification, the
re-arm bound, the masked stream record in the paging spec, and the
message-keyed alert node identity through the 2-to-1 transition.

* fix(web-shell): keep the spent re-arm bound's verdict standing (#13179)

Round 4 review follow-ups on the managed session hook.

retire() deleted the standing terminal verdict even after the
session-leg re-arm budget was spent, and cleared endedRef, so an
alternating definite-4xx/success backend ended at a healthy-looking
empty panel with no alert and no path back but reload(). Past the
bound the verdict now survives the poll's successes.

The bootstrap catch's endedRef write was the one cross-run shared-ref
write without an abort guard: a superseded run's late definite session
answer could arm the live run's re-arm path and tear its effect down
on the next poll success. It is now guarded like its siblings.

record() let an identical terminal answer re-stamp its record with a
fresh seq, suppressing a newer live record on another leg; a repeat
booking of the same final message is now a no-op.

Also: the both-reads-reject spec's retry claim now counts the
bootstrap's own transcript read instead of getSession, which the poll
shares; the onAlive disarm and the late-replay landing disarm each get
a witness (replay-armed capture, then a heartbeat or a late replay,
then the connection's drop); the stall recovery beats now assert
retired-not-escalated; and the proof-of-life comment names the
late-frame disarm alongside the heartbeat's.

* fix(web-shell): recover a spent re-arm bound on the slowest rung (#13179)

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-autofix <[email protected]>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
2026-10-11 02:07:09 +00:00
Shaojin WenandQwen-Coder 5b1c701400 feat(managed-agent): enforce workspace actor roles across the bound-Session surface (#13545)
* feat(managed-agent): persist workspace actor roles and session owners

Slice B of #13535 (storage only, behavior-invisible):

- V48 migration replaces managed_workspace_access.can_read/can_create
  with a single role column (READER/OPERATOR/OWNER, CHECK-enforced),
  backfilling can_create -> OPERATOR and can_read-only -> READER, and
  adds managed_agent_session.owner_actor_key backfilled from
  creator_actor_key.
- WorkspaceAccess becomes NONE/READER/OPERATOR/OWNER (ordered, with an
  atLeast helper); NONE remains the no-row decision, never storable.
- Every grant reader (canRead, findReadable, listReadable,
  readableDefault, resolveForCreation, authorizePassiveAttachment, the
  cwd-change registry-facts join, the listSessions EXISTS filter) keeps
  its current truth table, re-derived from role.
- insertSession writes owner_actor_key = creator on both bound and
  legacy creation paths; anonymous legacy creation leaves both NULL.
- Test fixtures move to the role column; revocations that previously
  zeroed booleans now delete the grant row (the honest no-access
  equivalent under the CHECK); a (FALSE,TRUE) fixture state that role
  ordering makes unrepresentable becomes a grant-row deletion.
- New tests: V47->V48 migration shape (backfill, owner:=creator, CHECK
  rejects NONE/invalid, post-V48 store-path creation writes owner) and
  session-owner store test (bound/legacy owner write, anonymous NULL).

The full managed-agent-server suite differs from baseline only in the
new tests; ManagedSessionStoreIntegrationTest#holdsRestorePages
InsideThePerPageByteBudget errors identically on unmodified HEAD
(verified in a clean worktree at ac497aeed9).

* docs(design): managed agent actor roles and isolation acceptance

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

* feat(managed-agent): enumerate the public and WebShell surface with an admission gate

Slice A of #13535 (D5/D6): SurfaceRegistry over today's rules for all 78
mounted handler methods (32 public + 24 WebShell + 22 internal), the
Spring-scanned correspondence gate that fails a route without a registry
entry or a stale entry, and the parametrized acceptance walk that pins
today's per-rule-class refusals on both surfaces plus both arms of the
internal writer credential. Purely additive; no production edits.

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

* docs(design): match V48 to the shipped migration shape

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

* fix(managed-agent): drop unreadable workspace grants before the V48 backfill

A (can_read=FALSE) row grants nothing under the booleans, but the role relabel would have given it READER (or OPERATOR with can_create). Dropping such rows keeps the migration a pure relabel for every reachable grant; pins the drop for (FALSE,TRUE) and (FALSE,FALSE) shapes in the migration test.

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

* feat(managed-agent): enforce workspace actor roles across the bound-Session surface

Closes #13535.

Turn submit/cancel/rename, cwd change and Action respond now admit any
caller holding the OPERATOR role on the bound Workspace (Action respond
additionally keeps the Session-owner arm), so a second operator can
answer a pending approval — the blocked handoff of R1. Lifecycle (close,
archive, unarchive, delete) admits the Session's recorded owner
(owner_actor_key, with the pre-V40 creator fallback) holding a read
grant: exactly today's caller set.

The submitter family's readable-but-below-OPERATOR refusal normalizes
from 409 workspace_unavailable to 403 session_operation_forbidden;
all other refusal vocabularies are unchanged and the shape gates
(deployment files opt-in, ACTIVE qwen-code Session, frozen execution
profile) keep their domain 409 for an admitted OPERATOR.

The SurfaceRegistry flips SESSION_CWD_CHANGE and ACTION_RESPOND from
OWNER to OPERATOR on both surfaces, the D6 probes pin the post-flip
matrix with a non-owner OPERATOR admitted on every OPERATOR-family
route, and the WebShell workspaceTurns capability is computed from the
caller's role instead of creator identity. Contract v1.34 records the
role vocabulary, the refusal normalization and the capability rule; the
generated WebShell types are refreshed with it.

* docs(managed-agent): restore the 404-below-read qualifier in the registry javadoc

The RuleClass summary said a caller below OPERATOR everywhere answers 403, dropping the qualifier the per-class entries carry: below the read grant every Session route answers 404 session_not_found, and only a readable actor below OPERATOR gets the normalized 403 — the #12867 split the sentence claims to summarize. Review finding F4 on PR #13545.

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

* docs(design): point the zh-CN matrix note at the D4 column in the doc's own language

The Chinese section-10 note sent readers to D4's English 'Rule today' column; the Chinese table calls it '今天'. Review finding F2 on PR #13545.

* fix(managed-agent): correlate bound-Session admission with creator-keyed execution facts

The widened OPERATOR admission of the submitter family certified runs
the execution authority still keys on the create-command actor, so a
demoted creator meant an accepted 202 that could only fail. Admission
now also requires the creator-keyed execution facts (registry ACTIVE
and backing the binding, create-command actor keeps OPERATOR or
above), synchronously from the service and the WebShell advertisement,
and the cwd contract names the recorded actor at admission and
settlement. Actor-scoped idempotency replays now run before any
mutable role check on every family, so a role revoked after admission
still resolves a retry to the callers own operation. The v1.34
contract and design docs state the new matrix truthfully, and the
follow-up gaps are tracked: #13617 handover, #13618 legacy hardening,
#13619 command-key actor scope.

* fix(managed-agent): close the round-2 withdrawals of the role widening

Admission now fails the widened family the shape the delivery actually
certifies: Action respond joins the creator-keyed execution-facts check
on its OPERATOR arm, and V54 persists the cwd-change initiator so
settlement fails when either the recorded creator or the operating
actor is demoted after admission (pre-V54 rows settle on the recorded
facts alone). The submitter family keeps its two-tier gate — role
before the not-actor-scoped command replay, facts after it — cancel
stays exempt on role and shape alone with a demoted-creator live-cancel
IT arm, and replay completion always answers. Capability texts, the
v1.34 cwd pipeline order, the ownerless fall-through wording, the G0
design, the README and the registry javadocs all now say exactly what
the gates do, with discriminating pins for every cell: OWNER-rank
seven verbs, unbound-only fall-through, pre-V40 owner fallback,
role-vs-facts advertisement split, publication-token grant comparison
on an OPEN clone, and the WebShell latch reachable through its own
check-again affordance.

* fix(managed-agent): sync the cwd contract with the V54 settlement and the replay-first pipeline order

The changeSessionCwd and webShellChangeCwd descriptions now put the
idempotency_conflict before the role refusal (the actor-scoped replay
resolves first), state that the V54 settlement re-checks the recorded
creator and the persisted initiator alike, and the v1.34 changelog names
the settlement consequence; TypeScript types regenerated.

* fix(managed-agent): certify the creator's execution facts on every bound respond arm

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

* chore(managed-agent): drop the superseded session-mutation replay probe

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

* docs(managed-agent): uncouple cancellation from the workspaceTurns capability wording

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

* test(managed-agent): restore the retained-context and migration-fence cwd arms the merge dropped

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

* docs(managed-agent): split cancellation and respond certification out of the v1.34 facts conjunct

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

* docs(managed-agent): keep the cancel exemption and respond certification out of the facts conjunct

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

* docs(managed-agent): drop the duplicated resume-capability sentence tail

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

* docs(managed-agent): attribute the workspaceTurns mirror to the WebShell surface

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

* fix(managed-agent): fail closed on out-of-enum role reads with the vocabulary filter

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

* test(managed-agent): sync the V54 and held-turn expectations with the MySQL and hosted lanes

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

* fix(managed-agent): renumber the operation actor key to V55 and keep the owner gate command-order identity for H4b children

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

* test(managed-agent): expect the lineage V54 and the actor-key V55 in the fresh-migration tail

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

* docs(managed-agent): restore the generation-adoption, supersede, big-output and W1c README prose the merges swallowed

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

* fix(managed-agent): repair the contract union that the index shipped without the worktree rewrite (v1.37 + V56)

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-09 16:18:21 +00:00
bb66a52c96 fix(managed-agent): stop a bound Turn under refused authorization (#13163)
* feat(managed-agent): admit later Turns of a Workspace-bound Session for its creator

G0 admits only the initial file-tool Turn created with a Workspace-bound
Session; every later public submit is refused with workspace_unavailable,
so a Hosted Session cannot hold a conversation.

Admit a later Turn when the deployment's Workspace-files opt-in is on
and the submitter created the Session. Execution already authorizes
each Turn against the creator's Workspace grants
(WorkspaceExecutionStore.authorize), so letting another actor submit
would run tools under the creator's authority; every other actor keeps
the existing refusal (404 without read access, 409 otherwise). The
store admits a bound Session's later Turn only under the same opt-in.
Cancel, rename, lifecycle and cwd operations stay gated.

- ManagedWorkspaceRegistry.createdSession looks up the creator in
  managed_workspace_create_command.
- HostedPublicWorkspaceIT: a reader who is not the creator is refused;
  the creator's second Turn runs write, edit and read again through the
  real Broker and worker and completes.
- ManagedWorkspaceAdmissionTest: the enabled store admits the later
  Turn and the creator lookup distinguishes actors and tenants.
- The Workspace-binding capability description (contract and generated
  WebShell types), the README and both G0 design documents describe the
  new boundary. The contract version is left for #13101, which takes
  1.25.

Not built or run locally.

* feat(web-shell): let the creator send later Turns to a bound Session

The server now admits later Turns from a Workspace-bound Session's
creator, but the Managed panel still hid the composer for every bound
Session and the Java provider forced canSend to false.

- WebShellSessionCapabilities gains workspaceTurns, computed per Session
  and caller with the same rule the submit path enforces (opt-in on,
  caller can read, caller created the Session). The contract and the
  generated WebShell types describe it.
- The Java provider sends for a bound Session only when the service
  reports workspaceTurns, and reports it in the summary only when true.
- The Managed panel shows the composer for such a Session; cancel stays
  hidden for bound Sessions.
- HostedPublicWorkspaceIT checks the capability per caller through the
  WebShell route; the contract test's WebShell capability shape and
  provider/page tests cover the client.

* feat(managed-agent): let the bound Session creator cancel a running Turn

Later Turns of a Workspace-bound Session could be submitted by the
creator but not cancelled: the service and store refused the cancel,
and the coordinator skipped bound Sessions entirely.

Apply the same rule to cancellation. The creator, with the Workspace-
files opt-in on, may cancel; the coordinator then cancels through the
Hosted Harness like any other Turn, which aborts the Turn and settles
its Runtime calls through their original identities. Other actors and
deployments without the opt-in keep the existing refusal, and the
coordinator still leaves a bound Session alone without the opt-in.

- HarnessCoordinatorTest: without the opt-in only the opt-in is read;
  with it, the bound Turn is cancelled through the Harness.
- HostedPublicWorkspaceIT: a later Turn is held in the model call; a
  reader's cancel is refused, the creator's cancel is accepted, and the
  Turn ends CANCELLED with no tool execution.
- WebShell reports canCancel for such a Session when workspaceTurns is
  set, so the Managed panel shows Cancel.
- Contract text, generated types, README and both G0 design documents
  now keep only lifecycle operations gated.

* feat(managed-agent): let the bound Session creator rename it

Rename of a Workspace-bound Session answered 409 until a role source
decided who may change it (D4 4.9). #12867 settled the first version:
the Session's creator is its only owner. Rename changes only the title
through the Hosted Harness and touches no Runtime, so apply the same
creator rule under the Workspace-files opt-in; the store admits only a
RENAME mutation of a bound Session, and only under the opt-in.

Close, archive, delete and unarchive stay gated: the embedded Broker's
drain only stops warming a closed Session and has no Harness-level
teardown yet, so closing a bound Session would leave its worker and any
held Workspace lease behind.

HostedPublicWorkspaceIT: a reader's rename is refused; the creator's
rename returns the new title.

* style(web-shell): keep the workspaceTurns expression on one line as Prettier prints it

* test(managed-agent): read the renamed title from the public Session metadata

PublicSession carries its title under metadata.title; the rename
assertion read a top-level title and saw an empty string although the
rename returned 200.

* fix(web-shell): match the generated optional workspaceTurns capability

* test(managed-agent): keep the approval run out of the later-Turn checks

* docs(managed-agent): Align G0 gating statements with creator later-Turn admission

The G0 section and Prerequisites still published the pre-follow-up gate
("later submit/cancel remain gated"), contradicting the boundary
paragraph this PR adds; state the creator's submit/cancel/rename
admission and keep close/archive/delete/unarchive and cwd gated. Scope
the follow-up's refusal precisely: requireLegacyWorkspace answers
workspace_unavailable only when the actor can read the Workspace, and
session_not_found otherwise. Apply the same two corrections to the
bilingual design doc and note the follow-up's composer UI change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuoob5v90j

* chore(managed-agent): Bump the public API contract to 1.27.0

The deferral resolved: #13101 merged and took 1.25.0, and #13117 took
1.26.0. Record this PR's contract change per the version-history
convention: creator later-Turn admission and the workspaceTurns
capability.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuoob5v90j

* fix(web-shell): hide execution-unavailable banner line when creator can send later Turns

The workspace-binding banner always stated that message execution is
unavailable, contradicting the composer this PR enables for the creator
of a bound Session with workspaceTurns capability. Gate the third
banner paragraph on !capabilities.workspaceTurns so the page no longer
states both. Pin both branches in ManagedSessionsPage tests.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuoqgbnv0l

* test(sdk-java): pin bound-Session admission clauses with negative controls

Three of the four clauses this PR adds to bound-Session admission could
be deleted with the suite staying green:

- ManagedWorkspaceRegistry.createdSession's session_id column: create a
  second bound Session by actor-b under a distinct idempotency key and
  assert createdSession(tenant, actor-a, otherSession) is false, so the
  lookup must match this Session, not any Session the actor created.
- maySubmitWorkspaceTurn's harness.isWorkspaceFilesAvailable() clause:
  assert the bound-session web-shell GET reports
  capabilities.workspaceTurns == false while the opt-in is off.
- boundRenameAllowed's kind conjunct: assert beginSessionMutation with
  UNARCHIVE on the enabled store throws workspace_unavailable, and add
  the positive insertCancelCommand control so the cancel path stays
  open for bound Sessions under the opt-in.

Each new assertion was verified to go red under the corresponding
mutation (session_id dropped, flag clause dropped, kind conjunct
dropped, cancel conjunct dropped) and the suite returns green with the
sources restored.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuoqgbnv0l

* test(sdk-java): exercise later Turns under approval-mode=default and pin canRead

HostedPublicWorkspaceIT changes:

- register() now grants the reader in both runs (can_create only in the
  files run), removing the access-table primary-key collision that
  forced the approvals run to skip the later-Turn block.
- The approvals run now drives a later Turn through answerActions and
  asserts it completes, so a later Turn admitted under
  approval-mode=default is covered; the cancel/rename probes keep their
  held model reply and stay in the files run to fit the method timeout.
- proof.txt is reset to a sentinel just before the later-Turn submit, so
  the post-Turn "after" assertion can fail if the later Turn's write and
  edit stop reaching the bound root (R1-13).
- After the rename check, revoking the creator's read grant asserts
  submit, cancel and PATCH all answer 404 session_not_found, pinning
  the canRead clause of maySubmitWorkspaceTurn (R1-12 clause 3).
- Model-request counts become 16 (approvals) / 18 (files) and the
  approvals run now answers 8 actions across the two Turns.

Compile-verified via mvn test-compile. Execution requires the bundled
dist/cli.js plus a MySQL/hosted-Harness stack, which this environment
cannot provide; behavior was traced against requireSubmitter /
requireLegacyWorkspace / boundRenameAllowed and the fixture request
model, and CI remains the executor.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuoqgbnv0l

* fix(sdk-java): align bound-Session later-Turn admission with the execution authority

Round-2 review of #13112 found the admission predicate certifying Turns
that execution can never run. maySubmitWorkspaceTurn now also requires the
predicates the cited authority fixes at creation (Session ACTIVE, the
qwen-code agent, the frozen execution profile refs) and the creator's
can_create on an ACTIVE registry, read through the same grant row shape;
a can_read-revoked creator still falls through to the documented 404.
Cancellation attaches passively so an abort no longer depends on the
physical mount still verifying. renameSession answers a non-retryable
refusal with its own status and code instead of a transient 503.
requireSubmitter loads the Session once per call, and the session read
paths skip the canRead probe their entry already established. The OpenAPI
contract, README and both design docs now state the creator admission,
its grants qualifier and the still-gated lifecycle/cwd operations
consistently, and the generated WebShell types are regenerated from the
corrected contract. Tests pin the bound rename admission, the two
empty-creation profile escapes and the can_create-revoked arm.

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

* fix(sdk-java): cancel a live bound Turn through its running attachment

26de98cd7e made the live cancel attach passively. A passive attach reloads
the Session in the Hosted Harness instead of reusing the running Turn's
attachment, so the abort never reached the Turn and HostedPublicWorkspaceIT
timed out with the Turn still CANCELLING. Restore the ordinary attach for
the live path; cancellation recovery, which has no live attachment, keeps
attaching passively. Cancelling under a refused Workspace authorization
stays a follow-up.

* fix(managed-agent): stop a bound Turn under refused authorization

Follow-ups to #13112 from its round-4 verification (#13162).

Cancelling aborts work that is already running, so it no longer needs the
grants that admit new work: the creator who can still read the Workspace
may cancel while can_create is revoked, the Workspace is draining or it was
re-registered. A live cancel reuses the running Turn's attachment and runs
no Workspace authorization; only without one does it fall back to the
existing attach. A cancel the Harness did not take is re-sent with backoff
while the Turn is still CANCELLING, since the running dispatcher checks
CANCELLING only once.

Admission of new work now also requires the Workspace generation and
storage the Session was bound to, so a re-registration refuses submit and
rename synchronously, before any command row is written. Tests pin the
opt-in clause, the creator cancel under revocation and re-registration, the
live-attachment path and the resend, and the IT cancels a running Turn after
revoking the grant and draining the Workspace.

* fix(managed-agent): fail the Turn when a cancel attach is refused

The bound-Session cancel path attaches through the strict 3-arg createOrLoad.
When the Workspace authority, or the storage guard behind it, refused that
attach, the RuntimeException fell into the catch that promises recovery: no
harness.cancel was issued for a cancel the API had already answered 202 for,
no sweeper re-claims a Turn whose lease the live consumer keeps renewing, and
the Turn ran on and settled COMPLETED. Settle the Turn with the refusal's own
code instead of dropping the cancel silently.

Addresses review thread R2-1 (PRRT_kwDOPB-92c6n-tVW).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupkgkl21b

* fix(managed-agent): retire a refused rename command instead of wedging rename

The permanent-refusal branch in renameSession rethrew a 4xx after
beginSessionMutation had already written a PENDING RENAME_SESSION row. Nothing
retires that row, so every later rename with a fresh key died in
requireNoOpenOperation with session_operation_active for the Session's life.
abandonSessionMutation deletes the still-PENDING row before the refusal is
answered, so the Session accepts the next rename again.

Addresses review thread R3-1 (PRRT_kwDOPB-92c6n-tVl).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupkgkl21b

* refactor(managed-agent): drop the dead read-grant operand and fix the published rule

maySubmitWorkspaceTurn's canRead operand, readGranted flag and 2-arg/3-arg
overload pair cannot change any result: findReadable already joins the same
access row with can_read = TRUE, and createdSession still runs first so an
actor id the registry key cannot encode keeps answering false instead of
throwing. The README's later-Turn rule now names the create grant and the
registry's ACTIVE state, as the OpenAPI capability text already does.

Addresses review threads R1-8 (PRRT_kwDOPB-92c6n-tVt) and R1-5
(PRRT_kwDOPB-92c6n-tV0, PRRT_kwDOPB-92c6n-tV-).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupkgkl21b

* refactor(managed-agent): cancel an admitted Turn without attaching

bindHarness compares the given boot with the Session's bound boot, and an
admitted Turn's boot is already bound, so the attach in cancelAdmittedTurn
only recomputed it while re-running the Workspace authorization. Pass the
bound boot instead and drop liveAttachment; harness.cancel already reuses
the running Turn's attachment. Re-send at a fixed two-second interval
instead of a backoff table.

* docs(managed-agent): state the full later-Turn admission rule and narrow the bound-Session notice

The published rule on createSession, webShellCreateSession and the
workspaceTurns description omitted the caller's live read and create
grants and the registry's ACTIVE state, so the public surface could not
discover the rule maySubmitWorkspaceTurn actually applies; the public
descriptions now also say there is no per-caller flag there and callers
must handle 409 workspace_unavailable. The bound-Session notice claimed
message execution "is not available in this service yet", which is false
for every per-caller refusal (not the creator, grant revoked, Workspace
draining) while the deployment serves the creator fine; narrow the copy
to "You cannot send messages in this Session" so it is true for both
causes. The webShellCreationIsMetadataOnlyUntilExecutionIsWired comment
claimed to pin the isWorkspaceFilesAvailable clause though its 3-arg
registration's drifted profile refs make the refusal over-determined;
say so instead.

Addresses review threads R3-4 (PRRT_kwDOPB-92c6n-tWn), R3-6
(PRRT_kwDOPB-92c6n-tWw) and the R3-2 comment anchor
(PRRT_kwDOPB-92c6n-tWZ).

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

* fix(managed-agent): deliver cancellation through the running owner

* fix(ci): bound hosted browser dependency installation

* fix(ci): record the ci.yml growth in the workflow size baseline

The hosted-browser install step adds two lines to ci.yml, taking it to
147136 bytes: 4110 over the recorded 143026 and 14 past the 4096-byte
allowance, which is why "Check workflow file size" failed. Most of that
growth is main-side drift the allowance had been absorbing, but this PR
touches the file, so the ratchet's stale-baseline leniency does not apply
and the number has to move in the same PR. The growth is real — a bounded
apt/Playwright install — so bump the entry instead of shrinking the
workflow.

Verified locally: .github/scripts/check-workflow-size.sh exits 0 against
base a7deb01bcb, and scripts/tests/workflow-size.test.js passes 220/220.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmupvw3d00b

* fix(ci): reserve browser smoke time after slow dependency downloads

* fix(ci): keep time for smoke after slow hosted installs

* test(web-shell): wait for step expansion before selecting next group

* test(ci): align hosted browser timeout contract

* fix(managed-agent): retry transient cancel-attach refusals instead of settling the Turn

The cancelAdmittedTurn catch settled the Turn permanently for every
RuntimeException out of createOrLoad, so a transient blip (a retryable
broker refusal, a daemon 5xx, a store error) terminally failed an
already-admitted, still-executing Turn whose Harness side kept running.
Rethrow anything that is not a non-retryable RuntimeBrokerException so
the Turn stays CANCELLING and the dispatch sweep re-attempts the cancel,
the pre-existing recovery path. Only a permanent refusal still settles
the Turn, under its own broker code.

That classification makes cancelRefusalCode's non-broker fallback
unreachable — a RuntimeBrokerException code is non-null by constructor
invariant — so the helper is removed rather than tested as dead code.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuq5w6bb0f

* fix(managed-agent): retire the rename command row on every answered failure

A rename answered without completing retired its PENDING command row only
for non-retryable broker refusals; every other failure family (a daemon
4xx/5xx, the connector's approval-mode IllegalStateException) left the row
PENDING forever, and requireNoOpenOperation then wedged every later
lifecycle change of the Session with 409 session_operation_active. Retire
the row for any RuntimeException and keep the retryable/permanent split
only for choosing the answered status.

The freed key also stayed poisoned: the requested event the abandoned
attempt published has a source_key derived from the key, so a same-key
re-attempt collided on UNIQUE (tenant_id, session_id, source_key) and
answered 500. beginSessionMutation now skips re-appending an already
published requested event, matching the no-event replay it replaces.

Two existing tests pinned the wedge (a 409 for a different key after an
answered failure, and a replay header on the same-key retry); they now
assert admission and a fresh, non-replay re-attempt.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuq5w6bb0f

* docs(managed-agent): qualify Workspace cancel contract

* docs(web-shell): scope Session creation capabilities

* docs(managed-agent): state the retired-rename-command contract in the README

Two README passages still described behavior this PR changed. The lifecycle paragraph promised a failed rename leaves a resumable PENDING command, but abandonSessionMutation now deletes that row, so a same-key retry is a fresh attempt answered replayed=false and the requested event the abandoned attempt published is not re-appended. The W1a paragraph still denied the public next-turn admission that the G0 section and the v1.28 OpenAPI note now describe; narrow it to the creator's later Turns under the opt-in and keep the resume clause.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuqn1gku0t

* test(managed-agent): pin the command half of requireNoOpenOperation

allowsOneLifecycleChangeAtATime held the suite's only witness for the PENDING-command conjunct, and this PR replaced those four lines because they pinned the wedge the retirement removes. Deleting the conjunct from requireNoOpenOperation now survives the whole module suite, so build a PENDING command row through the store and assert a close and a delete both answer 409 session_operation_active while no operation row is open.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuqn1gku0t

* test(web-shell): pin the bound-Session Cancel control at the page level

This PR moved the Cancel button inside the composer <form> and made that
form's visibility depend on `!summary?.workspace || workspaceTurns`. No
page-level fixture was both bound and cancellable (0 of 23), so neither
half of that composition was observable here: reverting the form gate, or
re-adding `!summary.workspace` to the Cancel condition, left every case
in the file green while a bound Session's creator lost the ability to
stop a Turn they had just started.

Adds one case mirroring the unbound cancel test. Measured both mutants:
reverting the gate to `!summary?.workspace` and adding `!summary.workspace`
to the Cancel condition each turn this case red (1 failed / 33 skipped)
while the unmutated tree passes 34/34.

Refs review finding R1-10.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuqp6mdb01

* fix(managed-agent): answer same-key retries from the record, retire rows on any permanent rename failure

- submitTurn/renameSession: look up the recorded admission before the
  Workspace-admission gate, so a same-key retry of an admitted submit or a
  completed rename is answered with the recorded outcome even after a
  re-registration / revoked grant / DRAINING flip the gate off (R1-7). A
  PENDING rename row still falls through to beginSessionMutation, which
  answers it as replayed and re-drives the unfinished mutation.
- renameSession: classify the retire-on-refusal branch by permanence, not
  by exception type — the connector's approval-mode IllegalStateException
  and permanent (<500) DaemonHttpException wedge the PENDING row exactly
  like a non-retryable broker refusal (R1-16).
- OpenAPI: workspaceTurns is served on every response, so move it into
  WebShellSessionCapabilities.required (R1-5).

Tests are written but not executed locally (no JDK 21/Maven on this host);
the three new ManagedWorkspaceAdmissionTest cases go red without their fix
by construction.

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

* fix(managed-agent): clear rename admission when harness is disabled

* fix(managed-agent): repair the two red required lanes on this head

Both failures are this PR's own, not the base's:

- ManagedAgentService.renameSession called CommandRecord.commandStatus(),
  which does not exist; the record's component is status. This is the sole
  cause of both Java lane failures (maven-compiler-plugin:compile,
  "cannot find symbol / method commandStatus()"), and lines 304 and 363 of
  the same file already use the correct "COMPLETED".equals(command.status())
  idiom.
- WebShellSessionCapabilities.workspaceTurns moved into the OpenAPI required
  array, but the generated TypeScript mirror was not regenerated, so the
  contract-drift guard in managed-agent-api.test.ts failed
  (expected `workspaceTurns: boolean;`, checked-in `workspaceTurns?: boolean;`).
  Regenerated with `node scripts/generate-managed-agent-api.mjs`; the only
  line that changed is the one the guard reported.

Verified locally: managed-agent-api.test.ts 2/2 pass,
java-managed-agent-provider.test.ts + ManagedSessionsPage.test.tsx 53/53 pass,
eslint clean on the regenerated file. The Java edit could not be compiled
here (no JDK 21 / Maven on this host); it is a single symbol-name correction
read off the record declaration in this tree.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuqtgxwd07

* test(managed-agent): expect the closed Session status for a queued bound Turn

WorkspaceSessionCloseMySqlIT (#13135) was written before this PR, when a
bound Session refused every later Turn with workspace_unavailable. With
Workspace files enabled this PR admits the creator's later Turns, so the
Turn queued behind the close now reaches the Session status and answers
session_not_active, which is what the test means to show: the close commits
before the queued admission inspects the Session.

* test(managed-agent): read the renamed Session title from metadata

The public Session body projects the Session title into metadata.title
(ManagedAgentService.publicSession); PublicSession has no title
component, so the three rename assertions written against title() broke
testCompile and reddened both Java lanes on this head.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmur1oq2c0i

* fix(managed-agent): restore admission error precedence in CI

* fix(managed-agent): keep the Workspace refusal ahead of the Harness gate

Both red required Java lanes fail the same six assertions on MySQL 8.4 and
MariaDB (run 37022561191, jobs 110889014145 / 110889014257). Both causes are
in this branch's production code; main has not touched packages/sdk-java since
the merge base b3dda468f2.

- submitTurn ran requireHarness() ahead of requireSubmitter(), so a Session the
  Workspace refuses was answered with 503 hosted_harness_disabled instead of
  409 workspace_unavailable whenever the Harness is not configured. That is the
  four ManagedWorkspaceAdmissionTest failures
  (revocationHidesBoundSessionAndBlocksRetry:372,
  emptyBoundCreationOutsideTheProfileIsNotAdmittedForLaterTurns:663,
  creatorCancelsWithoutTheGrantsThatAdmitNewWork:1028,
  reRegistrationRefusesLaterWorkBeforeAnyCommandIsWritten:1055). Restore the
  order cancelTurn and renameSession already use, keeping the same-key replay
  lookup ahead of both gates so R1-7 still holds.

- renameSession classified every IllegalStateException as a permanent failure,
  retiring the PENDING command row and answering 409 session_mutation_refused.
  Only the attach path is permanent: that is where QwenHostedHarnessConnector
  refuses an unconfirmed Session approval mode (lines 133 and 364). An
  IllegalStateException out of harness.rename() is an unreachable Harness, and
  main pins it as a transient 503 hosted_harness_unavailable whose row a
  same-key retry re-drives (ManagedSessionLifecycleTest
  .allowsOneLifecycleChangeAtATime:382, ManagedAgentServerIntegrationTest
  .retriesAPendingRenameWithTheSameIdempotencyKey:660, both through the
  failNextRename fixture at ManagedAgentServerIntegrationTest:1744). Track
  whether the attach returned and narrow the arm to that call site, which keeps
  permanentNonBrokerRenameFailureRetiresItsCommandRow green without widening
  the transient path.

Not compiled or executed here: this host has no mvn and only JDK 1.8
(`which mvn` empty, `java -version` = 1.8.0_322, no ~/.m2), while the lanes run
JDK 21 with surefire/failsafe. The change is confined to two statements and
their guard; CI is the verifier.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmur4wgp80n

* fix(runtime-broker): close drained lost workspace bindings

* docs(managed-agent): state the cancel rule and refusal codes the code enforces

The public createSession description bound cancel to the submit-grade
conditions that requireCanceller deliberately does not check (create
grant, ACTIVE registry row, active undeleted frozen-profile Session), so
an integrator gating its Cancel control on them would withhold a cancel
the server accepts and could not abort a runaway Turn on a draining or
re-registered Workspace. Split the sentence: submit and rename keep the
submit-grade rule, now also naming the generation/storage conjunct, and
cancel states the narrower creator-plus-Workspace-read rule. Name the
404 session_not_found that requireLegacyWorkspace answers when the caller
lost Workspace read, next to the 409 already documented.

Drop the rename promise from webShellCreateSession - the WebShell surface
exposes no rename route, rename is updateSession on Public Sessions only -
re-landing the removal already made at 7720f6dfda. Add the ACTIVE
registry-state conjunct to the workspaceTurns capability so it matches
maySubmitWorkspaceTurn and the "same rule" claim beside it.

The generated WebShell mirror is updated to match; the generator imports
openapi-typescript, which is unavailable without node_modules here.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmurtwo5e1r

* fix(managed-agent): answer bound replays only to the creator and keep Cancel reachable

R3-1: submit and rename now check the creator's durable creation receipt
before the replay lookup, so a reader who did not create a bound Session
neither replays the creator's admission nor learns which keys exist. The
receipt survives revocation, draining and re-registration, so the creator's
own same-key retry still replays.

R3-2: cancel is no longer tied to workspaceTurns in the WebShell provider,
and the Cancel button renders outside the composer when the composer is
hidden. Send keeps its gate; the server refuses a cancel from anyone else.

R3-3, R3-4: scope the published cancel claim to a resident attachment
(after a restart or takeover the owner re-attaches through Workspace
execution authority and the Turn may stay cancelling while it refuses), and
align the README's G0 twin with the later-Turn rule.

* fix(managed-agent): keep a retired rename from reverting a newer title

completeSessionMutation accepts a FAILED receipt so a same-key sibling that
entered the Harness before the retirement can still complete. Nothing
ordered that completion: while the receipt was FAILED, requireNoOpenOperation
no longer saw it, so a rename under a fresh key could begin and complete,
and the late sibling then overwrote that newer title with a 200 and a second
session.updated event.

Refuse to complete a FAILED receipt when a mutation of the same kind
completed after its requested event, answering 409
session_mutation_superseded. Mutation commands on a Session begin one at a
time, so event sequence ids order this strictly where millisecond
timestamps could tie. A same-key request sent after the newer rename
revives the receipt to PENDING and still applies, so the newest request
wins as before.

Reported on #13112 (ManagedAgentStore.java:565 review thread).

* style(web-shell): format the bound Cancel page test with Prettier

* style(web-shell): apply prettier to ManagedSessionsPage.test.tsx

The Lint & Static job failed on `node scripts/lint.js --prettier` with
"Code style issues found in 1 file" for
packages/web-shell/client/components/managed/ManagedSessionsPage.test.tsx.
Reformat that one expectation so the branch matches the repo prettier style.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmus7hcqa2c

* fix(managed-agent): recover cold-cache cancels without new-work grants

* docs(managed-agent): include storage in both capability descriptions

* refactor(managed-agent): delete the review-minted dead surface and its duplicate

Closes four open review findings by deletion only; the diff shrinks by 92
lines and no production behavior changes.

- R2-3: both convenience constructors on WebShellSessionCapabilities had
  zero call sites. The only construction is the canonical 8-arg call in
  ManagedAgentService.webShellSession; the contract test touches the record
  through .class for schema naming. The 2-arg overload also took
  (tasks, actions) while the record's components run (tasks, artifacts,
  actions), so it silently mapped its second argument to `actions`.
- R2-4: isSessionCreator was a one-line wrapper over createdSession with
  the two String parameters reversed, leaving two public registry methods
  that differ only in argument order on a creator-identity authorization
  check. Deleted the wrapper; its single caller in
  ManagedAgentStore.requireWorkspaceCreator now calls createdSession with
  the declared (tenantId, actorId, sessionId) order. Equivalent by
  construction: the wrapper body was that same call.
- R2-5 and R2-6: aRenameFailureThatIsNotABrokerRefusalStillRetiresItsCommand
  and permanentNonBrokerRenameFailureRetiresItsCommandRow both pin the same
  single branch -- renameSession's catch fall-through to
  abandonSessionMutation plus dependencyUnavailable("hosted_harness_unavailable")
  -- with the same three assertions. They were distinct while the catch still
  classified DaemonHttpException(<500) and IllegalStateException separately;
  retiring the row on every answered failure collapsed them. The deleted twin
  also stubbed its DaemonHttpException double with a bare mock(), so
  getStatusCode() returned 0 rather than the documented 4xx, and its
  TransactionTemplate wrapper mirrors no production contract (renameSession
  carries no @Transactional). The surviving test uses the shared
  boundSession/boundServiceWith helpers, which build the identical
  workspaceFilesEnabled configuration and the identical bound Session.
  The two imports only that test used are removed, as checkstyle enforces
  UnusedImports for this module.

Not compiled or executed here: this host has no mvn and only JDK 1.8. Every
removed member was grepped to zero remaining references across the module,
and no surviving import was left unused. The Java lanes on the pre-push head
52704a9de0 are green and are the verifier.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmusjmlf72u

* fix(managed-agent): dedupe by dropping this PR's own added test, not main's

Corrects 8d98903588, which deleted the wrong twin.

aRenameFailureThatIsNotABrokerRefusalStillRetiresItsCommand is pre-existing
main coverage: it sits at line 776 of ManagedWorkspaceAdmissionTest.java both
at this PR's merge base 2a6879e648 and on origin/main, introduced by #13112
(2b15eac862), which is an ancestor of the merge base. Removing it made this
PR delete 80 lines of unrelated main test coverage.

permanentNonBrokerRenameFailureRetiresItsCommandRow is the twin this PR added,
and it is the one that duplicates main's test: both drive the same single
branch (renameSession's catch fall-through to abandonSessionMutation plus
dependencyUnavailable("hosted_harness_unavailable")) with the same three
assertions. They were only distinct while the catch classified
DaemonHttpException(<500) and IllegalStateException separately; retiring the
row on every answered failure collapsed them.

So the deletion moves to the PR's own addition. The test file's diff against
the merge base is now 233 insertions and zero deletions instead of 291/80.
The two imports 8d98903588 removed (DaemonHttpException, Mockito.mock) are
restored, as main's test still uses both; no import is left unused, which this
module's checkstyle enforces.

The R2-3 (unused WebShellSessionCapabilities constructors) and R2-4
(isSessionCreator wrapper with reversed String parameters) deletions from
8d98903588 are unchanged and unaffected.

Not compiled or executed here: this host has no mvn and only JDK 1.8. The
removed method was grepped to zero remaining references and the deletion is
brace-balanced; the Java lanes are the verifier.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmusjmlf72u

* fix(serve): retain passive recovery leases on refusal

* fix(managed-agent): keep the re-registration guard on the page twin

The page refactor on main rewired webShellSession(session, actorId) to
the batch assembler, whose maySubmitWorkspaceTurn twin answers the
creator submit gate from the batched grant read alone. That read carried
no binding stamp, so a re-registered Workspace kept advertising
workspace_turns on the page and on the single-session path while the
singular gate still refused the submit through bindingCurrent.

Carry workspace_generation and storage_id in the batch grant read and
compare them with the Session's recorded binding, so the twin answers the
same rule without a per-row registry query: the grant batch stays the one
managed_workspace_registry read the page budget asserts.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmutypl6357

* fix(managed-agent): keep the Session lifecycle term on cancel admission

requireCanceller hand-copied only the workspace and files disjuncts of
maySubmitShape, dropping the Session lifecycle term that main enforced
through requireSubmitter. A cancel on a CLOSED, ARCHIVED or DELETED bound
Session answered 202 and wrote a CANCEL command where main answered 409
workspace_unavailable and wrote nothing.

Cite maySubmitShape instead: it restores the lifecycle term and removes the
duplication. The grant and registry terms stay out of the cancel rule, so
the creator can still cancel while can_create is revoked, the Workspace is
draining or it was re-registered.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuu9s91d5p

* fix(managed-agent): record a passive adoption stranded by a concurrent teardown

The resident passive-load path answered its two post-await exits without
noteOwedAdoption, even though onPassiveRuntimeAcquired had already recorded
the adopted Runtime lease. close() admits a concurrent DELETE here: its guard
is only active/mcpBusy/mcpRecovering/hooksBusy, none of which a parked passive
recovery sets, and the route allows a missing client id. Its releaseLeaseNow
then runs while runtimeLeaseHeld is still undefined, so it releases nothing,
the acquire resolves afterwards, and the adopted Runtime Session is left
unreachable from every route with nothing named on stderr.

Record the strand on both exits rather than releasing it: a release persists
the record as RELEASED and every retried acquire of the same identity then
conflicts with 409 runtime_session_not_acquirable.

Also drain refusedAdoptions on both resident drive-redrive success exits,
matching the resident passive success exit and the record's own contract that
the next successful load of the id drains it. Without the drain a later
genuinely stranded adoption is suppressed by the has(sessionId) latch.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuug7qaz61

* fix(managed-agent): fence every resident reattach and keep its refusal retryable

The resident split answered a redriven load from the same resident connection
as a passive one, but only the passive branch revalidated the store identity.
Hoist that fence above both branches so a foreign tenant or Workspace stays
hidden and a drifted Session Store address is refused on either path.

A 404 is settled as a terminal Turn failure by the only caller, which would
drop an admitted cancellation on the storage-drift refusal; answer the
retryable conflict instead.

The drive branch also resumed from its recovery await with no post-await
revalidation, so a teardown admitted during the await answered 200 with the
detached Session's clientId and left the acquired lease unreachable. Mirror
the passive branch's fences there and record the owed adoption.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuva7z827l

* fix(managed-agent): fence the parked passive recovery against teardown

The resident passive load publishes its adopted Runtime lease onto the
route-reachable Session the moment broker.acquire() resolves, while the
recovery is still reading execution states. close() fenced only
active/mcpBusy/mcpRecovering/hooksBusy, none of which a parked passive
recovery set, and DELETE /session/:id resolves with a missing client id
allowed, so a teardown landing in that window released a lease that was still
mid-adoption. The release persists the record RELEASED, and
RuntimeBrokerService.acquireNewSession answers 409 runtime_session_not_acquirable
for any stored state that is neither READY nor ACQUIRING, so every later cold
takeover and every cancel-path status/cancel for that parked Turn failed and the
Workspace stayed pinned.

Hold close()'s own fence for the whole recovery rather than recording the strand
afterwards: the teardown is refused with 409 hosted_turn_active, nothing is
released mid-adoption, and the adopted lease goes back with the teardown the
fence lets through. The pre-diff teardown released nothing but left the adoption
unreachable from every route; the fence removes that window too, so the two
post-await exits no longer carry a false comment.

Also states the deployment's Workspace-files opt-in in the README's authoritative
cancel rule: requireCanceller reaches maySubmitShape, which returns false the
moment harness.isWorkspaceFilesAvailable() is false, before falling through to
requireLegacyWorkspace, which answers workspace_unavailable for a bound Session.
The other contract artifacts already state the condition.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuvp83ny8e

* fix(managed-agent): preserve concurrent recovery and rename ordering

Count all pending passive recovery requests before permitting teardown.
Persist the newest mutation attempt boundary so a failing sibling does not reject its successful retry.

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

* fix(test): import Mockito times in coordinator tests

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

* fix(serve): preserve live cancellation reattachment

Skip ownerless cancellation settlement while the original Turn still runs, so a cold Java attachment can reattach passively and cancel that owner.

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

* fix(serve): restore resident recovery outcomes

Preserve terminal decline reasons and reattach requested approvals. Settle payable terminal projections without retrying settled file history, and refuse a cancellation attachment removed during settlement.

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

* fix(managed-agent): retry transient approval mount failures

Use delivery-time mount verification for action responses and recover cold approval attachments passively after rechecking new-work authority. Preserve shared acquire verdicts and permanent workspace refusals.

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

* fix(managed-agent): renumber the mutation-attempt migration to V51

main published V48__workspace_storage_migration.sql plus V49/V50 while this
branch was open, and the merge that pulled them in left two files claiming
version 48, so scripts/check-flyway-migrations.js fails the "Flyway migration
version uniqueness" gate. V51 is the next free version across both migration
locations (SQL and BaseJavaMigration, whose highest is V29). Nothing refers to
the file by name or version: the code and tests only use the
mutation_attempt_sequence column, and Flyway applies every migration in the
location, so the renumber only moves this ALTER after main's V48-V50.

Verified locally with the same command the gate runs:
  node scripts/check-flyway-migrations.js packages/sdk-java/managed-agent-server \
    packages/sdk-java/runtime-broker packages/sdk-java/qwencode
before: "2 migrations claim version 48" (rc=1); after: "51 migrations, all
versions unique" (rc=0).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmuy7yk8zcm

* test(managed-agent): expect the V51 mutation-attempt migration in the upgrade IT

WorkspaceMigrationMySqlIT.upgradesCurrentMainWithoutChangingAppliedMigrations
pins every migration applied after the V47 baseline, so renumbering
V48__managed_mutation_attempt_sequence.sql to V51 left that expectation one
version short and the MariaDB failsafe job failed 1 of 58 tests.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmuya3q1ncp

* test(managed-agent): keep Hosted fixture ports distinct

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

* fix(managed-agent): retry operator-reversible refusals on the action-response path

A revoked creation grant or a registry that left ACTIVE is
operator-mutable state, not the structural verdict the terminal exit
promises: stamping the committed approval decision FAILED over it
certifies a confirmation the Harness never saw, and restoring the grant
cannot recover it. The passive attachment authority now splits its
verdict into structural, grant-only, and authorized; only the
action-response delivery maps the grant-only refusal to a retryable
workspace_unavailable so the delivery machine re-arms while recovery
waits for the operator. Every other caller — creation, passive load,
cancellation — keeps the previous terminal refusal.

* fix(cli): refuse a resident inapplicable redrive while a prompt slot is active

The helper re-validated registration and closing after its awaits but
never that the Session was still idle, and the latch clear removed the
only incidentally closing guard: a files/rewind admitted inside the
window had its prompt slot overwritten by the settle projection and its
AbortController dropped while /status reported the Session idle. Add
the third post-await re-validation the continue and managed-runtime
cancel routes already perform, answering the session-scoped refusal
before the projection can run.

* test(managed-agent): pin V52 after renumbering the mutation-attempt migration

* fix(managed-agent): preserve approval retries during cold attachment

* test(managed-agent): pin the legacy receipt rewrite and the acquire verdict control

- Wrap the two raw jdbc receipt mutations in exactly-one-row assertions
  so a 0-row arm can no longer collapse onto the tested path.

- Add the positive acquire-path control after the action-response probe
  retry proves its retryable refusal: the same mount must still authorize
  without a verdict change.

Review threads on the 2026-10-07 automated closeout round.

* test(sdk-java): allow heartbeat recovery time after refused detach

Keep the confirmed-detach observation at 200ms, but allow rejected detach responses two seconds to demonstrate continued HTTP heartbeats on macOS.

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

* test(hosted): pin teardown and pending rename recovery guards

Add HTTP-level deletion and closing interleavings for resident inapplicable recovery, and a public-store pending rename witness. Keep the inherited main heartbeat deadline fix unchanged.

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

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-10-08 11:05:37 +00:00
易良andQwen-Coder b585508733 feat(hosted): show captured inputs in native approval cards (#13407)
* feat(hosted): show captured inputs in native approval cards

Bind native approval options to the current captured input and include it
in the first Action commit. Preserve the API preview through the managed
provider and display literal fallback content with localized size notices.

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

* fix(web-shell): escape invisible format characters in the managed approval preview

R1-1: action.inputPreview.text reached the approval dialog verbatim, so a
command carrying U+202E rendered reordered while the approved bytes stayed
unseen. Reuse the package's existing escapePreviewText invariant instead of
adding a second character class, and pin it at the value the card renders.

R1-7: the cross-call separation case passed an empty transcript, so the
matching rule it names never ran. Give it two same-tool rows keyed by itemId
with no Harness input; the tool-name mutant now fails it.

R1-8: prefix the rendered-preview fixture with a leading space so the
"must not be trimmed" half of the guarantee is pinned at the DOM boundary.

R1-4: alias inputPreview to the generated WebShellActionInputPreview schema
rather than hand-declaring the same three fields.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuwnio38a6

* fix(hosted): name the version 2 preview tool set and pin its restore path

R1-2: the tools admitted to version 2 were an anonymous inline literal that
has to stay byte-identical to the Java reader's closed PREVIEW_TOOLS. Give the
set a name to grep, state the contract and both silent drift directions, pin it
in a test, and assert the Shell Action really carries v2 plus its own input:
dropping run_shell_command previously left the whole suite green.

R1-3: the managed-action-options branch of nestedResourceRefs was byte-for-byte
the fall-through of the generalised branch below it. Fold the kind into that
condition so the parse arguments and the throw-versus-degrade choice live in one
place instead of four lines apart in one function.

R1-5: nestedResourceRefs has two callers and only the commit path was covered.
Run the read-only snapshot inspector over a Session that raised a native
approval, so the options -> input edge is pinned on the restore path too.

R1-6: document the reader-side direction of the change - a CLI inspector from
before this commit cannot verify a snapshot written after it - in both design
docs, matching the precedent recorded for the journal change.

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

* test(acp): align deferred tool rejection assertions

Preserve target-specific validation guidance and the bridge refusal marker in the two existing ACP expectations. Production behavior is unchanged.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-06 16:53:50 +00:00
d092d5b439 fix(web-shell): managed session UI correctness from the #12692 R2 review (#13342)
* fix(web-shell): managed session UI correctness from the #12692 R2 review

Ten R2 review follow-ups on #12692 in the web-shell managed sessions
UI, re-verified against current main (2c591ecc08) before patching:

- A transient poll failure no longer leaves a permanent stale alert on
  an idle session: both success paths (the 3 s poll loop and the
  stream post-completion refresh) clear the captured error.
- The Turn-boundary settle now runs only for event types the fold
  consumes: a stale-Turn runtime_failed (or runtime_starting/ready/
  released, stream_gap, cancelling) landing between deltas of the
  active Turn no longer splits one answer into two bubbles.
- runtime_failed itself settles only for the Turn actually streaming:
  the tail stops rendering as forever-in-progress and pending tools
  settle to failed, while a stale-Turn failure cannot disturb the
  live one and a post-completed failure leaves the settled answer
  alone.
- A permanently failing pending prompt gains a Discard affordance
  (draft restored into the composer, same idempotency key freed from
  its absorbing state); before this the only escape was closing the
  browser tab.
- The standalone-Java provider no longer sends environmentId in the
  create body (the server 400s any non-blank value); it stays a
  storageKey scope input.
- The dead `enabled` prop on ManagedSessionsContent is gone together
  with its four permanently-true branches and the duplicate
  unavailable fallback.
- Durable replay now derives pending-vs-started from the raw
  attributes.status hint: a store-canonicalized in_progress item that
  was truly pending comes back as tool_requested instead of
  tool_started.
- A re-issued sessionId prop applies on the same commit via render-
  phase state adjustment (no stale frame, and a repeat of the current
  external value after internal navigation keeps working), and the
  component is fully controlled when both sessionId and
  onSessionChange are provided.
- The provider is rebuilt only when a real connection input changes:
  inline fetch/getHeaders/saveArtifact closures are read through a
  ref, ending the per-render abort/refetch/SSE-restart cycle for
  embedders.
- The session list effect depends on the workspace path only when
  the provider actually sends it, so a dropped workspaceCwd no longer
  discards accumulated pages for nothing.

Verified with the web-shell vitest suite for the managed components
(296 tests including the new pins) and repo build + typecheck.

* test(web-shell): pin the pending-prompt discard path end to end

The discard affordance for a permanently failing pending create now
has its playwright witness: permanent 500 on /sessions/create,
banner up, Discard visible, click, draft restored editable, no new
submission fired. Verified locally against chromium (3/3 smoke tests
in web-shell.managed-progress.spec.ts pass).

* fix(web-shell): scope the two widened R2 rules from the #13342 review

Two Critical findings on the managed-session fixes, each verified with
a red witness before restoring:

- The summary poll cleared the shared error field on every success,
  which also erased the resync loop's deliberately re-asserted
  "event stream is not advancing" alert — the role=alert banner
  flickered every ~3s for the whole stall. The poll now only clears
  an error it raised itself; a concurrent stall alert survives
  untouched. Witness: freeze the stream mid-stall while the poll
  keeps succeeding — the alert must persist (goes red on the
  unconditional clear; 28 others stay green).
- The projector let attributes.status outrank the canonical
  item.status unconditionally, but settleTurnItems rewrites
  item_status without touching attributes_json, so a tool still
  pending when its Turn ended replayed as tool_requested instead of
  tool_completed. The raw hint now arbitrates only while the
  canonical status is the ambiguous in_progress. Witness: settled
  completed + frozen pending hint → tool_completed (red against the
  ?? selection).

Managed suite 277/277 green; the unconditional-clear form of each
witness fails exactly and only the new case. web-shell typecheck and
ESLint clean.

* fix(web-shell): close the managed session R1 review findings (#13342)

* fix(web-shell): close the managed session R2 review findings (#13342)

* fix(web-shell): close the managed session R3 review findings (#13342)

Three clusters, each verified red before the fix and mutation-probed
after:

- The diagnostic-failure lifecycle in the transcript fold is now
  complete: runtime_failed floors a tool's endTime, the Turn's terminal
  event bounds it for tools that never reported (a lost turn.failed no
  longer leaves a failed tool growing its duration with the wall clock),
  and an authoritative result or completion retires the booking so a
  later cancellation can no longer relabel a server-resolved failure as
  cancelled — same-Turn or cross-Turn.
- The shared error ledger keeps each writer's live condition instead of
  one slot, so a poll blip can no longer displace a still-dead paging
  cursor or a live stream failure; a release reveals the
  highest-priority survivor (stall > stream > paging; a poll entry is
  never revealed — it re-asserts on its own cadence).
- Every ledger write and release is now gated on the run's own abort:
  a superseded run's late continuation (e.g. a poll parked in the
  host's token-refresh hook) can neither book into, release from, nor
  disarm its successor's fresh ledger.

Also: the pending-prompt uncertainty explanation is visible again in
foreign sessions (only the destructive Discard stays owner-scoped), and
the discard focus latch stays armed while the session summary is still
loading, so focus lands on the composer once it becomes focusable.

* fix(web-shell): close the managed session R4 review findings (#13342)

---------

Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-10-06 13:12:12 +00:00
2b15eac862 feat(managed-agent): let a Workspace-bound Session's creator submit, cancel and rename (#13112)
* feat(managed-agent): admit later Turns of a Workspace-bound Session for its creator

G0 admits only the initial file-tool Turn created with a Workspace-bound
Session; every later public submit is refused with workspace_unavailable,
so a Hosted Session cannot hold a conversation.

Admit a later Turn when the deployment's Workspace-files opt-in is on
and the submitter created the Session. Execution already authorizes
each Turn against the creator's Workspace grants
(WorkspaceExecutionStore.authorize), so letting another actor submit
would run tools under the creator's authority; every other actor keeps
the existing refusal (404 without read access, 409 otherwise). The
store admits a bound Session's later Turn only under the same opt-in.
Cancel, rename, lifecycle and cwd operations stay gated.

- ManagedWorkspaceRegistry.createdSession looks up the creator in
  managed_workspace_create_command.
- HostedPublicWorkspaceIT: a reader who is not the creator is refused;
  the creator's second Turn runs write, edit and read again through the
  real Broker and worker and completes.
- ManagedWorkspaceAdmissionTest: the enabled store admits the later
  Turn and the creator lookup distinguishes actors and tenants.
- The Workspace-binding capability description (contract and generated
  WebShell types), the README and both G0 design documents describe the
  new boundary. The contract version is left for #13101, which takes
  1.25.

Not built or run locally.

* feat(web-shell): let the creator send later Turns to a bound Session

The server now admits later Turns from a Workspace-bound Session's
creator, but the Managed panel still hid the composer for every bound
Session and the Java provider forced canSend to false.

- WebShellSessionCapabilities gains workspaceTurns, computed per Session
  and caller with the same rule the submit path enforces (opt-in on,
  caller can read, caller created the Session). The contract and the
  generated WebShell types describe it.
- The Java provider sends for a bound Session only when the service
  reports workspaceTurns, and reports it in the summary only when true.
- The Managed panel shows the composer for such a Session; cancel stays
  hidden for bound Sessions.
- HostedPublicWorkspaceIT checks the capability per caller through the
  WebShell route; the contract test's WebShell capability shape and
  provider/page tests cover the client.

* feat(managed-agent): let the bound Session creator cancel a running Turn

Later Turns of a Workspace-bound Session could be submitted by the
creator but not cancelled: the service and store refused the cancel,
and the coordinator skipped bound Sessions entirely.

Apply the same rule to cancellation. The creator, with the Workspace-
files opt-in on, may cancel; the coordinator then cancels through the
Hosted Harness like any other Turn, which aborts the Turn and settles
its Runtime calls through their original identities. Other actors and
deployments without the opt-in keep the existing refusal, and the
coordinator still leaves a bound Session alone without the opt-in.

- HarnessCoordinatorTest: without the opt-in only the opt-in is read;
  with it, the bound Turn is cancelled through the Harness.
- HostedPublicWorkspaceIT: a later Turn is held in the model call; a
  reader's cancel is refused, the creator's cancel is accepted, and the
  Turn ends CANCELLED with no tool execution.
- WebShell reports canCancel for such a Session when workspaceTurns is
  set, so the Managed panel shows Cancel.
- Contract text, generated types, README and both G0 design documents
  now keep only lifecycle operations gated.

* feat(managed-agent): let the bound Session creator rename it

Rename of a Workspace-bound Session answered 409 until a role source
decided who may change it (D4 4.9). #12867 settled the first version:
the Session's creator is its only owner. Rename changes only the title
through the Hosted Harness and touches no Runtime, so apply the same
creator rule under the Workspace-files opt-in; the store admits only a
RENAME mutation of a bound Session, and only under the opt-in.

Close, archive, delete and unarchive stay gated: the embedded Broker's
drain only stops warming a closed Session and has no Harness-level
teardown yet, so closing a bound Session would leave its worker and any
held Workspace lease behind.

HostedPublicWorkspaceIT: a reader's rename is refused; the creator's
rename returns the new title.

* style(web-shell): keep the workspaceTurns expression on one line as Prettier prints it

* test(managed-agent): read the renamed title from the public Session metadata

PublicSession carries its title under metadata.title; the rename
assertion read a top-level title and saw an empty string although the
rename returned 200.

* fix(web-shell): match the generated optional workspaceTurns capability

* test(managed-agent): keep the approval run out of the later-Turn checks

* docs(managed-agent): Align G0 gating statements with creator later-Turn admission

The G0 section and Prerequisites still published the pre-follow-up gate
("later submit/cancel remain gated"), contradicting the boundary
paragraph this PR adds; state the creator's submit/cancel/rename
admission and keep close/archive/delete/unarchive and cwd gated. Scope
the follow-up's refusal precisely: requireLegacyWorkspace answers
workspace_unavailable only when the actor can read the Workspace, and
session_not_found otherwise. Apply the same two corrections to the
bilingual design doc and note the follow-up's composer UI change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuoob5v90j

* chore(managed-agent): Bump the public API contract to 1.27.0

The deferral resolved: #13101 merged and took 1.25.0, and #13117 took
1.26.0. Record this PR's contract change per the version-history
convention: creator later-Turn admission and the workspaceTurns
capability.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuoob5v90j

* fix(web-shell): hide execution-unavailable banner line when creator can send later Turns

The workspace-binding banner always stated that message execution is
unavailable, contradicting the composer this PR enables for the creator
of a bound Session with workspaceTurns capability. Gate the third
banner paragraph on !capabilities.workspaceTurns so the page no longer
states both. Pin both branches in ManagedSessionsPage tests.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuoqgbnv0l

* test(sdk-java): pin bound-Session admission clauses with negative controls

Three of the four clauses this PR adds to bound-Session admission could
be deleted with the suite staying green:

- ManagedWorkspaceRegistry.createdSession's session_id column: create a
  second bound Session by actor-b under a distinct idempotency key and
  assert createdSession(tenant, actor-a, otherSession) is false, so the
  lookup must match this Session, not any Session the actor created.
- maySubmitWorkspaceTurn's harness.isWorkspaceFilesAvailable() clause:
  assert the bound-session web-shell GET reports
  capabilities.workspaceTurns == false while the opt-in is off.
- boundRenameAllowed's kind conjunct: assert beginSessionMutation with
  UNARCHIVE on the enabled store throws workspace_unavailable, and add
  the positive insertCancelCommand control so the cancel path stays
  open for bound Sessions under the opt-in.

Each new assertion was verified to go red under the corresponding
mutation (session_id dropped, flag clause dropped, kind conjunct
dropped, cancel conjunct dropped) and the suite returns green with the
sources restored.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuoqgbnv0l

* test(sdk-java): exercise later Turns under approval-mode=default and pin canRead

HostedPublicWorkspaceIT changes:

- register() now grants the reader in both runs (can_create only in the
  files run), removing the access-table primary-key collision that
  forced the approvals run to skip the later-Turn block.
- The approvals run now drives a later Turn through answerActions and
  asserts it completes, so a later Turn admitted under
  approval-mode=default is covered; the cancel/rename probes keep their
  held model reply and stay in the files run to fit the method timeout.
- proof.txt is reset to a sentinel just before the later-Turn submit, so
  the post-Turn "after" assertion can fail if the later Turn's write and
  edit stop reaching the bound root (R1-13).
- After the rename check, revoking the creator's read grant asserts
  submit, cancel and PATCH all answer 404 session_not_found, pinning
  the canRead clause of maySubmitWorkspaceTurn (R1-12 clause 3).
- Model-request counts become 16 (approvals) / 18 (files) and the
  approvals run now answers 8 actions across the two Turns.

Compile-verified via mvn test-compile. Execution requires the bundled
dist/cli.js plus a MySQL/hosted-Harness stack, which this environment
cannot provide; behavior was traced against requireSubmitter /
requireLegacyWorkspace / boundRenameAllowed and the fixture request
model, and CI remains the executor.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuoqgbnv0l

* fix(sdk-java): align bound-Session later-Turn admission with the execution authority

Round-2 review of #13112 found the admission predicate certifying Turns
that execution can never run. maySubmitWorkspaceTurn now also requires the
predicates the cited authority fixes at creation (Session ACTIVE, the
qwen-code agent, the frozen execution profile refs) and the creator's
can_create on an ACTIVE registry, read through the same grant row shape;
a can_read-revoked creator still falls through to the documented 404.
Cancellation attaches passively so an abort no longer depends on the
physical mount still verifying. renameSession answers a non-retryable
refusal with its own status and code instead of a transient 503.
requireSubmitter loads the Session once per call, and the session read
paths skip the canRead probe their entry already established. The OpenAPI
contract, README and both design docs now state the creator admission,
its grants qualifier and the still-gated lifecycle/cwd operations
consistently, and the generated WebShell types are regenerated from the
corrected contract. Tests pin the bound rename admission, the two
empty-creation profile escapes and the can_create-revoked arm.

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

* fix(sdk-java): cancel a live bound Turn through its running attachment

26de98cd7e made the live cancel attach passively. A passive attach reloads
the Session in the Hosted Harness instead of reusing the running Turn's
attachment, so the abort never reached the Turn and HostedPublicWorkspaceIT
timed out with the Turn still CANCELLING. Restore the ordinary attach for
the live path; cancellation recovery, which has no live attachment, keeps
attaching passively. Cancelling under a refused Workspace authorization
stays a follow-up.

* fix(managed-agent): fail the Turn when a cancel attach is refused

The bound-Session cancel path attaches through the strict 3-arg createOrLoad.
When the Workspace authority, or the storage guard behind it, refused that
attach, the RuntimeException fell into the catch that promises recovery: no
harness.cancel was issued for a cancel the API had already answered 202 for,
no sweeper re-claims a Turn whose lease the live consumer keeps renewing, and
the Turn ran on and settled COMPLETED. Settle the Turn with the refusal's own
code instead of dropping the cancel silently.

Addresses review thread R2-1 (PRRT_kwDOPB-92c6n-tVW).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupkgkl21b

* fix(managed-agent): retire a refused rename command instead of wedging rename

The permanent-refusal branch in renameSession rethrew a 4xx after
beginSessionMutation had already written a PENDING RENAME_SESSION row. Nothing
retires that row, so every later rename with a fresh key died in
requireNoOpenOperation with session_operation_active for the Session's life.
abandonSessionMutation deletes the still-PENDING row before the refusal is
answered, so the Session accepts the next rename again.

Addresses review thread R3-1 (PRRT_kwDOPB-92c6n-tVl).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupkgkl21b

* refactor(managed-agent): drop the dead read-grant operand and fix the published rule

maySubmitWorkspaceTurn's canRead operand, readGranted flag and 2-arg/3-arg
overload pair cannot change any result: findReadable already joins the same
access row with can_read = TRUE, and createdSession still runs first so an
actor id the registry key cannot encode keeps answering false instead of
throwing. The README's later-Turn rule now names the create grant and the
registry's ACTIVE state, as the OpenAPI capability text already does.

Addresses review threads R1-8 (PRRT_kwDOPB-92c6n-tVt) and R1-5
(PRRT_kwDOPB-92c6n-tV0, PRRT_kwDOPB-92c6n-tV-).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupkgkl21b

* docs(managed-agent): state the full later-Turn admission rule and narrow the bound-Session notice

The published rule on createSession, webShellCreateSession and the
workspaceTurns description omitted the caller's live read and create
grants and the registry's ACTIVE state, so the public surface could not
discover the rule maySubmitWorkspaceTurn actually applies; the public
descriptions now also say there is no per-caller flag there and callers
must handle 409 workspace_unavailable. The bound-Session notice claimed
message execution "is not available in this service yet", which is false
for every per-caller refusal (not the creator, grant revoked, Workspace
draining) while the deployment serves the creator fine; narrow the copy
to "You cannot send messages in this Session" so it is true for both
causes. The webShellCreationIsMetadataOnlyUntilExecutionIsWired comment
claimed to pin the isWorkspaceFilesAvailable clause though its 3-arg
registration's drifted profile refs make the refusal over-determined;
say so instead.

Addresses review threads R3-4 (PRRT_kwDOPB-92c6n-tWn), R3-6
(PRRT_kwDOPB-92c6n-tWw) and the R3-2 comment anchor
(PRRT_kwDOPB-92c6n-tWZ).

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

* fix(managed-agent): retry transient cancel-attach refusals instead of settling the Turn

The cancelAdmittedTurn catch settled the Turn permanently for every
RuntimeException out of createOrLoad, so a transient blip (a retryable
broker refusal, a daemon 5xx, a store error) terminally failed an
already-admitted, still-executing Turn whose Harness side kept running.
Rethrow anything that is not a non-retryable RuntimeBrokerException so
the Turn stays CANCELLING and the dispatch sweep re-attempts the cancel,
the pre-existing recovery path. Only a permanent refusal still settles
the Turn, under its own broker code.

That classification makes cancelRefusalCode's non-broker fallback
unreachable — a RuntimeBrokerException code is non-null by constructor
invariant — so the helper is removed rather than tested as dead code.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuq5w6bb0f

* fix(managed-agent): retire the rename command row on every answered failure

A rename answered without completing retired its PENDING command row only
for non-retryable broker refusals; every other failure family (a daemon
4xx/5xx, the connector's approval-mode IllegalStateException) left the row
PENDING forever, and requireNoOpenOperation then wedged every later
lifecycle change of the Session with 409 session_operation_active. Retire
the row for any RuntimeException and keep the retryable/permanent split
only for choosing the answered status.

The freed key also stayed poisoned: the requested event the abandoned
attempt published has a source_key derived from the key, so a same-key
re-attempt collided on UNIQUE (tenant_id, session_id, source_key) and
answered 500. beginSessionMutation now skips re-appending an already
published requested event, matching the no-event replay it replaces.

Two existing tests pinned the wedge (a 409 for a different key after an
answered failure, and a replay header on the same-key retry); they now
assert admission and a fresh, non-replay re-attempt.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuq5w6bb0f

* docs(managed-agent): state the retired-rename-command contract in the README

Two README passages still described behavior this PR changed. The lifecycle paragraph promised a failed rename leaves a resumable PENDING command, but abandonSessionMutation now deletes that row, so a same-key retry is a fresh attempt answered replayed=false and the requested event the abandoned attempt published is not re-appended. The W1a paragraph still denied the public next-turn admission that the G0 section and the v1.28 OpenAPI note now describe; narrow it to the creator's later Turns under the opt-in and keep the resume clause.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuqn1gku0t

* test(managed-agent): pin the command half of requireNoOpenOperation

allowsOneLifecycleChangeAtATime held the suite's only witness for the PENDING-command conjunct, and this PR replaced those four lines because they pinned the wedge the retirement removes. Deleting the conjunct from requireNoOpenOperation now survives the whole module suite, so build a PENDING command row through the store and assert a close and a delete both answer 409 session_operation_active while no operation row is open.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuqn1gku0t

* fix(managed-agent): clear rename admission when harness is disabled

* test(managed-agent): expect the closed Session status for a queued bound Turn

WorkspaceSessionCloseMySqlIT (#13135) was written before this PR, when a
bound Session refused every later Turn with workspace_unavailable. With
Workspace files enabled this PR admits the creator's later Turns, so the
Turn queued behind the close now reaches the Session status and answers
session_not_active, which is what the test means to show: the close commits
before the queued admission inspects the Session.

* fix(runtime-broker): close drained lost workspace bindings

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-10-03 01:42:31 +00:00
c15dd268c0 fix(web-shell): stop offering a Managed approval the viewer cannot answer (#13165)
* fix(web-shell): stop offering a Managed approval the viewer cannot answer

After the service answers 403 action_forbidden, the approval card kept its
Allow and Deny buttons enabled, so a viewer who is not the Session creator
could send one refused request per click until the Action expired. Disable
the card once that refusal arrives; the warning is scoped to the Action, so
the next approval is offered again.

* fix(web-shell): scope the creator-only approval latch to the Session

Review on #13165 found three gaps in the Managed approval refusal:

- action_forbidden is decided by the service from the (tenant, Session,
  actor) row on the respond path, but the disabled latch derived from the
  per-Action answerError, which the hook clears as soon as the refused
  Action leaves the pending list. A reader watching a Session that raises
  sequential approvals ate one guaranteed 403 per Action. The hook now
  keeps a Session-scoped respondForbidden flag, set on an action_forbidden
  answer failure and reset only when the selection changes, and the page
  disables the card from it; the per-Action warning keeps its scope.
- The persistent disabled state had no visual affordance: the only
  :disabled rule in ToolApproval.module.css is goal-scoped, so a refused
  Managed approval looked clickable while swallowing clicks. A base
  .option:disabled rule (after .option:hover so equal specificity resolves
  by source order) dims it and shows the not-allowed cursor; the goal
  variant's stronger rule is untouched.
- Every HTTP answer failure the client builds carries a string code, yet
  the only retry test used an uncoded Error, so nothing pinned that a
  coded but retryable failure stays answerable. A 503 negative control now
  fails if the action_forbidden condition is widened.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupzgp080a

* style(web-shell): format ToolApproval.css.test.ts with Prettier

The new stylesheet pinning test was committed without a prettier pass, so
the Lint & Static "Run Prettier" step failed on it. Collapses one
multi-line expect to the canonical single line; no behavior change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupzgp080a

* fix(web-shell): remember a creator-only refusal per Session

The latch was one boolean that the `[sessionId]` effect cleared
unconditionally, so leaving a refused Session and coming back re-enabled the
card and re-admitted the guaranteed 403. Remember the refused Session ids
instead and derive `respondForbidden` from the current selection.

The latch was also set below the `if (!isPending()) throw failure;` guard, so a
403 landing after a re-read had already dropped the refused Action rethrew
without latching. The service checks the creator before it checks that the
Action still exists, so that refusal is reachable; record it above the guard,
keyed by the Session the answer was aimed at rather than the one now selected.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuqa6hxd10

* fix(web-shell): keep the creator-only reason on screen while the latch holds

The warning was scoped to the Action that failed, so from the second approval
onward the card rendered disabled with no stated reason anywhere: a viewer
could not tell a permission policy from a stuck or still-loading card, and the
disabled options also drop out of sequential focus navigation. Render the
reason from the Session-scoped latch as a status line, since it restates what
the viewer was already told rather than announcing anything new, and keep the
per-Action alert for the failure itself.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuqa6hxd10

* fix(web-shell): bound the creator-only reason to the card it describes

The latch outlives the refused Action, so the reason line it now drives would
stay on screen after the Session's last approval had left, explaining an
approval that is not there. Render it only while a card is on screen; the
per-Action alert branch is unchanged, because `answerError` is only exposed
for the Action being shown.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuqa6hxd10

* fix(web-shell): let a dead Managed approval card state its reason to assistive tech

Round-3 review findings, one batch:

- R2-1: the creator-only reason rendered as a sibling of the alertdialog, so
  on a latch-only render a screen-reader user reached an inoperable dialog
  whose accessible description carried the question, the tool and the
  arguments caveat, but no cause. Route the reason's id into
  extraDescriptionId next to the caveat. aria-describedby is a
  space-separated IDREF list, so the prop stays a string, and one boolean
  now feeds both the element's mount condition and its id reference, so no
  IDREF can dangle.
- R3-1: drop the inert `|| approvalForbidden` operand and the derivation
  that fed it. `answerError` is exposed only while `answerFailure.current`
  matches the Action on screen, which only holds after `isPending()` passed
  with the latched Session still selected, so `respondForbidden` is already
  true on every render where the deleted operand was true.
- R3-2: pin that the latch records `target.sessionId` rather than the
  Session selected when the 403 lands. A live-read mutant survives all 27
  shipped hook tests; the new case is the only thing that turns it red.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuqkwas11i

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-02 14:46:09 +00:00
f73f6c39ac feat(web-shell): show and answer Hosted tool approvals in the Managed panel (#13107)
* feat(managed-agent): Serve durable permission Actions (D6b)

* test(managed-agent): Avoid concurrent Action mock stubbing

* feat(web-shell): show and answer Hosted tool approvals in the Managed panel

The Managed panel passed pendingApproval={null}, so a Hosted Session
waiting on a D6a approval showed nothing to answer. Build on the D6b
Actions contract (#13101):

- The provider gains an optional Actions reader. The Java provider lists
  requested permission Actions through actions/query and answers through
  actions/respond with the Action's inputRevision and policyRevision and
  a per-Action, per-option idempotency key. The Session summary reports
  the actions capability only when the service does.
- action.updated projects to action_updated. It carries no Turn, so the
  transcript skips it instead of settling the Turn being streamed.
- useManagedActions re-reads pending approvals on action_updated, on a
  stream gap, after an expiry and after an answer. It hides an answered
  approval and brings it back if the answer fails.
- The page maps the pending Action onto the shared approval card, with
  allow/deny as allow_once/reject_once so labels are localized, and
  attaches it to the tool row `${turnId}:${functionCallId}` so the row
  expands and shows its arguments.

Not built, type-checked, linted or tested locally.

* feat(web-shell): export the pending Action type for custom Managed providers

* fix(managed-agent): renumber the Actions migration to V24 and bump the contract to 1.25

Merging main brought in #12946's V23__managed_mcp_records.sql next to this
PR's V23__managed_actions.sql; Flyway refuses two migrations with the same
version, so the server would not start. The Actions migration becomes V24
with its content unchanged. The contract takes 1.25.0 on top of main's
1.24.0 (#12946), with a v1.25 note for the Actions routes this PR serves.

* fix(web-shell): memoize pending actions to keep the respond callback stable

The conditional selected a fresh empty array on every render, so the
useCallback dependency changed identity each time and ESLint's
react-hooks/exhaustive-deps warning failed the lint gate.

* style(web-shell): format the Managed approvals page test with Prettier

* fix(web-shell): retry failed approval reads and name them as such

A transient failure of actions/query on the first read left a pending
approval invisible until a manual Refresh: summary polls do not re-read
Actions, and without an Action there is no expiry timer. The page then
showed "The approval answer could not be sent" although nothing was
sent.

- useManagedActions reports loadError and answerError separately, and
  retries a failed read after 2, 5 and 10 seconds before giving up.
  retry() reads again on demand and restarts the bound.
- The page shows "Pending approvals could not be loaded." with a Retry
  button for a read failure, and keeps the answer message for a failed
  answer.
- Tests cover the automatic recovery, the retry bound and the manual
  retry.

* fix(web-shell): recover failed approval answers and bound expiry reads

* fix(web-shell): Show available managed approval arguments

* fix(web-shell): keep the approval card when an answer was not applied

* fix(web-shell): keep Managed approvals stable across reloads and ended Actions

Treat an unknown session summary as unknown rather than as no Actions
capability, so a reload keeps the shown approval answerable. Drop an
approval the service reports as ended instead of offering a retry, scope
answer failures to the approval that failed, stop retrying client errors,
restart the retry ladder whenever reads resume, and carry the
arguments-unavailable notice inside the approval card.

* fix(web-shell): land only the Critical R1-1 fix for Managed approvals

The previous commit also carried the review's Suggestions. The PR is past
its review-round budget, so only R1-1 stays here: an unknown session
summary keeps the shown approval instead of reading as no Actions
capability. The Suggestions move to the follow-up recorded in #12867.

* fix(web-shell): match Managed approvals to itemId-keyed tool rows

Since #13037, Managed tool rows are keyed ${turnId}:${itemId} and Java tool
Items always carry an itemId, so findManagedApprovalTool no longer found the
row for a pending Action: the card could not show that row's arguments or
expand it. Keep the producer's call ID on the row and match on it, still
within the Action's Turn.

Candidate patch from wenshao's round-4 real-stack verification (R4-1).

* test(web-shell): pin the page half of the Managed approval reload fix

Reverting the page gate back to detail.summary?.capabilities.actions === true
kept every test green (G9). Hold a reload's Session summary and assert the
shown approval stays and no extra Actions read happens.

Candidate test from wenshao's round-3 real-stack verification.

* test(web-shell): spell canCancel in the Managed approvals fixtures

The three approvals fixtures handed to `summary()` omitted
`capabilities.canCancel`, which is a required member, so each one was a
TS2741 that no gate in this package reports (both tsconfigs exclude
`client/**/*.test.tsx` and vitest does not typecheck). They also modelled a
summary the real mapper cannot emit, which always sets both flags.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupe138l16

* fix(web-shell): keep the Harness call title on Managed approval cards

`toManagedPermissionRequest` reads `tool.title` for the approval
`title`, but no producer ever set it: `managedEventsToMessages` only
assigned args/status/times/rawOutput, so the left branch was dead and the
card description degenerated into a restatement of the heading while the
Harness`s own per-call title was dropped.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupe138l16

* fix(web-shell): stop Managed approval state outliving its Action

Two lifecycle leaks in `useManagedActions`:

- a successful re-read cleared `loadError` but never `answerError`, so
  "the answer could not be confirmed" stayed on screen after the Harness
  ended the Action, and then labelled the next, unrelated card. The
  warning now carries the Action it belongs to and is dropped once that
  Action is no longer pending.
- a withdrawn reader cleared `pending` without resetting the retry budget,
  so a reader restored after the ladder was exhausted got a single attempt
  with no retry scheduled, contradicting the hook`s own comment.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupe138l16

* fix(web-shell): stop retrying approval reads the service already answered

R1-5: a 4xx other than 408/429 is the service's answer, not a hiccup, so a
deleted Session no longer costs four guaranteed-failing /actions/query
requests and a Retry that can never succeed. The classification the submit
path already used moves to managed-request-error.ts and is reused, so both
failure paths of the feature agree.

R1-4: the hook reports whether the pending approvals were read for this
Session, so a failed background re-read is named as a refresh instead of
claiming nothing could be loaded next to the loaded, actionable card.

R1-8: the "arguments are unavailable" caveat renders as a sibling of the
approval dialog, so ToolApproval takes an extra description id and the panel
describes the caveat too — otherwise a screen-reader user confirms a Hosted
tool call hearing only the tool name.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupe138l16

* test(web-shell): pin four Managed approval paths that no test observed

R1-10, four sites the review mutation-tested as unpinned:

1. the `stream_gap` arm of the re-read trigger — the durable transcript is
   the only route that reports a gap here, since use-managed-session breaks
   the live loop on a gap before merging it; the hook doc now says so.
2. the session-switch reset effect and the cross-session mask — the new test
   switches Session while the previous card is displayed and its failed
   answer is still flagged, so deleting the effect leaves the warning behind
   and reverting the mask leaves the stale card answerable.
3. the `pendingApproval` wiring into MessageList — the page test's mock now
   captures the prop, which is what keeps the turn owning the pending call
   from being folded away.
4. the `setAnswerError(undefined)` clear on a successful answer — the retry
   test now ends on the alert being gone, not just on the card.

Every assertion was checked to fail with the production line it covers
reverted (8/8 mutations RED).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmupe138l16

* fix(web-shell): isolate late Managed approval replies

Ignore answer settlements once their Session or pending Action has changed,
and clear or display warnings only for the Action they belong to.

Pin late success and failure after Session switches and Action replacement.

* fix(web-shell): drop a Managed approval the service reports as ended

Answering an Action that already expired, was cancelled or was answered
elsewhere returns 409 with a code the contract defines as ended, and no
retry can succeed. Keep the card hidden, clear the warning and read the
list again instead of offering a retry. A read that still lists the Action
shows it again, so a wrong report cannot hide an approvable Action. The
403 actor_scope_mismatch is not an ended Action and keeps its handling.

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-01 14:43:32 +00:00
jinye 6310dd38d5 feat(managed-agent): project durable tool results to WebShell (#13037)
* feat(managed-agent): project durable tool results to WebShell

* codex: address PR review feedback (#13037)

* fix(managed-agent): sync transcript exclusion text and localize forbidden output reads

- Name item.tool_result.updated alongside the other transcript-excluded
  event types in the WebShell transcript contract description, the
  generated client comment, and the event-replay design notes, keeping
  the three surfaces in sync as the event-replay post-merge review
  required.
- Explain the 3-byte UTF-8 page-boundary priming in
  ManagedToolResultPanel.
- Map HTTP 403 to managed.result.forbidden in ResultError instead of
  rendering raw error text, with a regression test.

* codex: address PR review feedback (#13037)

* codex: address PR review feedback (#13037)

* codex: address PR review feedback (#13037)
2026-10-01 06:19:23 +00:00