Commit graph

70 commits

Author SHA1 Message Date
jinye
97ebffaa70
feat(managed-agent): support Plan mode and inline attachments (#14531) 2026-10-18 16:58:07 +08:00
Shaojin Wen
e986a74dfc
fix(sdk-java): keep the snapshot flag when a load copy restates child Workspaces (#13905 item 40) (#14519)
The one-argument form of the load request's child Workspace setter
passed false for the snapshot capability, so restating child
Workspaces on a copy that served snapshots silently dropped them.
Every other copy of the request keeps the flag. The one-argument form
now keeps the flag the copy carries, so only turning child Workspaces
off drops it, as the two-argument form already does.

No control-plane caller uses the one-argument form today; this removes
the trap for the next one.
2026-10-18 11:51:47 +08:00
易良
398e916d70
test: fix the SDK Java suite's red jobs (FG6e frames, schema V8 pin, client flakes) (#14457)
* test(managed-agent): expect tool progress frames in the FG6e SSE gap gate

#14405 projects a committed tool intent and its receipt as public
item.tool_call.updated events, so a hosted tool turn now publishes five
events instead of three. The FG6e driver and probe still pinned the old
shape: depending on when the in-progress update reached the store, the
early observers saw frame 2 before the tool finished or the finished-phase
ledger check met two unexpected tool updates. The gate now waits for the
in-progress update before disconnecting and expects ids 1-2 before the
tool finishes, 3-4 after it and 5 at the terminal.

* test(sdk-java): fault only the caller's dispatch in the send-escape tests

The three pre-dispatch escape tests armed a one-shot executor fault and
let the next execute() take it. A SelectorManager task trailing the
previous response can call execute() first on a loaded runner; the fault
then kills the HttpClient's selector, the real send fails with an
IOException, and the test sees PromptAdmissionUnknownException instead of
the injected throwable. Only the calling thread's dispatch takes the fault
now.

* test(managed-agent): expect schema V8 in the SSH workspace MySQL IT

#14318 pinned the fully migrated managed schema at version 7 in this
integration test, and #14404 then added V8. The two merged independently;
the MySQL lane, which only runs in the SDK Java full workflow, now fails
all twelve cases in the setup assertion. The other version pins already
moved to 8 with #14404.

* test(sdk-java): widen the keepalive idle-budget margin

The keepalive test sent a comment every 120 ms against a 500 ms idle
budget, so any half-second stall in the server writer or the client's
body delivery on a loaded runner tripped the watchdog. It failed that way
on ubuntu-latest / Java 17 (SDK Java run 39018177648). The budget is now
1500 ms and the consumer stays parked for over two budgets, which keeps
the test proving that keepalives alone hold the stream open.

* test(sdk-java): keep tiny-executor setup traffic off the injected rejection

The one-worker executors used a two-slot queue, so a burst of HttpClient
tasks during createSession on a slow Windows runner was rejected before the
behavior under test (run 39021209055). Saturate an unbounded queue only once
the test arms it, and keep the submit-time rejection tests from rejecting a
selector-thread task trailing createSession, which aborts the client.

* test(managed-agent): expect FG6e tool progress as transcript items, not events

The transcript query keeps item.tool_call.updated out of its events and
materializes it as a tool_call item, like text deltas (run 39021209055).

* test(managed-agent): drop unused test imports that fail Checkstyle

---------

Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-10-17 21:15:22 +08:00
jinye
6c7c044533
fix(managed): Recover cancellation after process restarts (#14377) 2026-10-17 19:02:11 +08:00
易良
18a27de0b3
test(sdk-java): read the heartbeat scheduler queue on its own thread (#14427)
A periodic task leaves the ScheduledThreadPoolExecutor queue while it
runs, so an outside read can see an empty queue before the 404 has
retired the attachment. The drain check then exited early and its second
read failed (0.072 s on an ECS Java 17 leg). Sampling on the single
scheduler thread rules out a running heartbeat task.
2026-10-17 14:36:10 +08:00
jinye
2b6de671d1
feat(managed-agent): Add SSH Tool Runtime support (#14318)
* feat(managed-agent): add SSH Tool Runtime support

Add administrator-configured SSH placement, remote Workspace registration,
Session-isolated workers and strict retirement over the existing HTTP protocol.
Keep new admission disabled by default and preserve original-worker identity.

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

* fix(managed-agent): clean up SSH runtime transport lifecycle

Keep bootstrap work outside the endpoint lock and release unused forwards and stopped direct routes without disconnecting durable observers. Bound control work and complete queued futures during shutdown.

Align SSH registration with the shared SQL clock, reuse storage identity and dependencies, and fix the environment inventory and transcript budget. Normalize inherited CSI test formatting for Java verification.

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

* fix(managed-agent): align lifecycle and timestamp test fixtures

Use canonical SQL timestamp columns and database milliseconds in fault-gate evidence, and require parent close to cancel a held child Turn as current main specifies. Preserve production behavior and capability boundaries.

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

* fix(managed-agent): Bind environment query IDs as binary

Preserve canonical Session and Turn identity types when H2 recompiles the
latest environment query. Force recompilation in the existing regression
without changing its admission-order or environment assertions.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-16 18:18:04 +00:00
Shaojin Wen
d565d7463d
fix(sdk-java): Close Hosted Harness review criticals from #12654 (#13314)
* fix(sdk-java): Close Hosted Harness review criticals from #12654

#12654 merged before its second review round was processed. This closes
the 11 Critical and 2 Minor findings, each re-verified against main and
pinned by a new HostedHarnessClientTest case whose mutation witness was
run (reverting the fix reddens the test):

- HarnessEventStream close() no longer parks behind a next() blocked in
  the socket read: the closed flag is atomic, the raw input is closed
  without any stream monitor, and HostedHarnessClient.close() completes;
  the blocked consumer fails with DaemonTransportException (R1-1).
- The hosted SSE transport gains the idle watchdog its stdio sibling
  has: Builder.sseIdleTimeout defaults to 45s, and a silent peer makes
  next() fail with "Hosted Harness SSE idle timeout" (R1-43).
- submitTurn releases the active-prompt slot on a definitive 409 and
  throws PromptAlreadyActiveException; retaining a marker keyed by the
  rejected promptId wedged the session client-side (R1-2). A definitive
  non-409 failure on a same-identity retry releases that slot too (F2).
- getStatus() snapshots the entry before the remote read and removes
  only that registration, so a stale hasActivePrompt:false cannot delete
  a different prompt admitted in flight (R1-4).
- Responses missing the X-Qwen-Harness-Boot-Id fence header on mutation
  paths are classified outcome-unknown (create/prompt/mutation/delete)
  instead of a bare DaemonProtocolException (R1-5).
- A local executor rejection (saturated or terminated pool) surfaces as
  DaemonTransportException instead of claiming the request may have
  reached the daemon (R1-45).
- closeSession(ref) and detachSession treat a definitive 404 as done and
  tear down the heartbeat timer (R1-3, R1-46).
- streamEvents falls back to the session ref's validated watermark when
  the builder omits the resume pair, so the epoch fence actually
  applies (R1-44).
- The shared SSE parser accepts the id-less session_snapshot frame, so
  StreamHarnessEvents.snapshot(true) works (R1-47).
- computePayloadDigest hashes a sorted-key canonical form, so the wire
  dedup key is reproducible across JVM processes (R1-6).
- HarnessRuntimeExecutionRecovery requires status for known outcomes at
  construction instead of NPE-ing in isContinuationReady() (F1).

Test: HostedHarnessClientTest 34/34 (15 new); full module 181 green with
9 env-gated skips on JDK 17 and clean test green on JDK 11; 14 mutation
witness arms all red. The remaining 49 Suggestion-level findings are
dispositioned in their review threads and tracked in #13313.

* fix: Close second-round review criticals on the Hosted Harness client

Addresses the CHANGES_REQUESTED round on #13314, which execution-proved
one real regression in the first commit plus five more critical defects
around it:

- R1-1 (the regression CI confirmed on the fault-gates lane): the digest
  was canonicalized but the wire `prompt` member was not, and the hosted
  route re-derives the digest from the received bytes — every addText
  submission drew `400 invalid_hosted_prompt`. toJson() now emits the
  canonical form, the digest reuses the JsonSupport.encode facade over
  that same value, and the test asserts the wire bytes and the digest
  are literally the same value.
- R1-3: the 45s idle watchdog was inherited from the stdio transport,
  whose server heartbeats; the hosted events route wrote nothing while
  no event was committed, so healthy silent streams (tool calls, the
  10-minute approval wait) were aborted. The hosted route now mirrors
  the primary route's keepalive comment every 15s, and
  Builder.sseIdleTimeout accepts Duration.ZERO for callers that own the
  deadline. A keepalive-only peer is covered by a new test (consumer
  parked in read for three idle budgets).
- R1-4: an idle abort while the consumer was between next() calls
  surfaced IllegalStateException (not the DaemonException family). The
  closed guard now distinguishes who closed the stream: idle abort ->
  DaemonTransportException("Hosted Harness SSE idle timeout"),
  caller close -> IllegalStateException, matching the javadoc.
- R1-5: the cause-chain scan certified any rejection-shaped IOException
  as "never dispatched", but delivery also runs on the caller executor,
  so a rejection after the server accepted the prompt dropped the
  active-prompt marker for an admitted turn. Only the bare
  RejectedExecutionException (submit-time) and the terminated-executor
  case now classify as saturated/known; everything mid-flight stays
  outcome-unknown. A deterministic delivery-phase test drives this via
  a package-private executor seam (park the worker, fill the queue,
  then answer 202).
- R1-6: the missing-boot-header wrap reclassified every unfenced
  response as outcome-unknown, including definitive 4xx the server
  emits itself before the fencing middleware (413, 429). All four
  sites now re-derive: ambiguous (408/5xx) keeps outcome-unknown,
  definitive unfenced statuses surface as DaemonHttpException with
  their status preserved.
- R1-7 (suggestion folded in, same hunk family): detachSession tears
  the local attachment down for every definitive answer, not just 204
  and 404 — a 403/400/410 no longer leaks a heartbeat timer.
- R1-13 (suggestion, adjacent): a caller queued on cursorLock when the
  stream closes re-checks inside the lock and gets the closed-stream
  signal instead of a transport error.

Also: the snapshot frame comment now names the actual producer (the
bridge's withSnapshot on the primary events route — never the hosted
route); the StreamHarnessEvents builder javadoc documents the
lastEventId fallback precedence and that snapshot(true) is a
primary-transport option; the README retry section and both design-doc
languages now state the canonical digest as a cross-language wire
contract and record that changing the digest algorithm requires
quiescing or rewriting in-flight turn rows (R1-2's deployment note);
the two 10ms heartbeat tests assert liveness instead of comparing
zeros; and an explicit-cursor-wins case plus a definitive
boot-headerless DELETE case close the two remaining unpinned arms.

Test: HostedHarnessClientTest 42/42 (full module 189 green, 9
env-gated skips) on JDK 17; clean test green on JDK 11 (2 additional
version-gated skips: JDK 11 parks such sends instead of surfacing the
rejection); hosted-harness-session.test.ts 181/181. Nine mutation
witness arms all red (revert toJson canonicalization, forget idle
signal in the closed guard, drop the in-lock re-check, unbounded
cause-walk, blanket boot-header wrap at prompt and DELETE sites,
detach without finally, watchdog removed with the keepalive arm
staying green, ref-only resume fallback).

* fix(sdk-java): close third-round review criticals on the Hosted Harness client (#13314)

- submitTurn: a definitive unfenced refusal (a response without a valid
  boot header that is not an ambiguous 408/5xx) now releases only the
  active-prompt marker the call owns, in both the validateGeneration arm
  and the post-consume arm; a refusal produced before the route's
  promptId lookup can no longer delete an earlier same-identity entry
  whose outcome is unknown, and a refused fresh submission no longer
  wedges the session.
- send: snapshot the executor termination state before dispatch so a
  close() racing an in-flight send keeps its outcome-unknown
  classification instead of certifying non-dispatch.
- StreamHarnessEvents: an explicit lastEventId cursor now always wins,
  including a deliberate 0 (replay from the beginning), which
  HarnessCoordinator passes for turn rows without a recorded watermark;
  only an omitted cursor falls back to the session ref's watermark.
- HarnessEventStream: the idle watchdog now measures peer silence only
  while a consumer is parked in next(), so time between calls (a
  blocking queue handoff, a slow event handler) no longer aborts a
  healthy keepalived stream.
- docs: the payload digest is re-derived by the server from its own
  re-serialization of the parsed prompt member (JSON.stringify form),
  not from the received bytes; the README qualifies cross-release digest
  stability and the keepalive floor for a custom idle timeout.

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

* fix(sdk-java): close fourth-round review findings on the Hosted Harness client (#13314)

- Carry the wire status and machine-readable code through
  PromptAlreadyActiveException instead of collapsing every 409 into a
  fixed message
- Keep a prompt marker whose submission is still in flight from being
  cleared by a status answer that predates the admission
- Map a watchdog scheduling rejection to DaemonTransportException and
  drop the stranded stream registration
- Document the enforced idle-abort window (budget to 1.5x) and the
  peer-side backpressure limit on consumer pauses
- Pin the fenced definitive-refusal release, the Duration.ZERO watchdog
  opt-out, the next() entry stamp, and the terminated-executor release
  with dedicated regression tests

* fix(sdk-java): close fifth-round review findings on the Hosted Harness client (#13314)

- Retain the active-prompt marker when a fenced 409 carries
  hosted_prompt_recovery_required: that code proves the promptId is in
  the durable input log, so releasing the marker would admit a second
  identity into a session with two unsettled inputs and fail every
  recovery entrance closed; every other 409 code still releases it
- Clear the hosted events keepalive from stop() as well as from the
  close handler, and send X-Accel-Buffering: no so a buffering
  intermediary cannot swallow the keepalives the client watchdog relies
  on
- Pin the settled status-read clearing, the failed-close unregister, and
  the disabled watchdog's empty scheduler queue with positive witnesses,
  and extract the shared fenced-SSE response prelude in the client tests
- Correct the documented idle-abort window with the 100 ms poll floor,
  state the digest's sha256: wire encoding in the README, and narrow the
  PromptAlreadyActiveException javadoc to what the code does

* fix(sdk-java): lifecycle teardown runs only on definitive answers

Close the corridor the next-round review's close/detach receipts queued:
local attachments (and their heartbeat timers) must not survive a
definitive refusal, and must NOT be torn down on outcome-unknown.

- detachSession: the unconditional `finally { removeAttachment }` also
  ran when requireMutationStatus surfaced unexpected-success as
  MutationOutcomeUnknownException — a state where the server's answer is
  not decided and the lease must keep beating. Teardown now runs on the
  definitive limb (204 or tolerated 404) and inside the
  DaemonHttpException arm only; outcome-unknown surfaces keep the
  attachment (anUnexpectedDetachAnswerKeepsTheAttachment).
- closeSession(HarnessSessionRef) and closeSession(String): mirror the
  same try/catch — a definitive DELETE refusal (e.g. 403) now also retires
  the attachment and the active-prompt ledger instead of leaking the
  timer until close(), while ambiguous answers keep them
  (aRefusedCloseStillTearsDownTheHeartbeat ×2 overloads,
  anUnexpectedCloseAnswerKeepsTheAttachment,
  aBootHeaderlessCloseKeepsTheAttachment for the gateway-502 shape).
- closeSession(String) gains the ensureOpen() guard every other public
  entry has (a deferral receipt plus #13313's R1-50): the closed-client
  path is JVM-independent again, so aLocalExecutorRejectionIsNotReported
  AsOutcomeUnknown becomes closeSessionByIdFailsFastAfterClientClose —
  IllegalStateException on every JDK — and the saturated/terminated
  taxonomy stays pinned by aTerminatedHttpExecutorReleasesTheOwnedMarker.
- closeByRefTreatsAnAbsentHarnessSessionAsClosed now also counts the
  DELETE request (dropping the remote call reddens assertEquals(1)).
- Housekeeping of the takeover branch: aRecoveryRequiredRefusalKeepsThe
  OwnedMarker's stub never incremented its promptCalls counter, so its
  trailing assertion could never pass once it ran (CI never reached it
  through the verify-checkout race) — fixed by counting in the handler.

Witnesses: full module 214/214 green on JDK 17 (clean test green on JDK
11, 13 skips = 9 env + 4 version-gated); corridor mutation arms all red —
detach with an unconditional finally flattens the keep-beating assertion,
removing either close-mirror makes refused-close heartbeats keep growing
(9→14, 10→16), and removing ensureOpen flips the fail-fast test from
IllegalStateException back to the transport exception.

* fix(sdk-java): settle registered admission markers and align lifecycle teardown

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

* fix(sdk-java): retire hosted admission state on proven restart and scope the settle latch per attempt (#13314)

A proven harness generation change now retires every local record for
that peer at all three entrances: the submitTurn fence catch releases
the admission ledger entry, the scheduled heartbeat retires the
attachment and ledger instead of swallowing the mismatch, and
detachSession mirrors closeSession for the generation branch while
keeping the ledger across definitive refusals.

The admission settle latch is per-attempt and outcome-scoped: only the
owning call's concluded send settles it, every attempt re-arms it, and
a same-identity retry's 202 publishes a fresh settled entry so a status
read holding a stale snapshot cannot clear the marker that is live now.
Pre-dispatch unchecked escapes release the owned marker via a dispatch
boundary flag in send() instead of settle-and-keep.

The SSE idle watchdog merges the parked flag and activity stamp into
one volatile waitingSince write, so a tick can never pair a fresh flag
with a stale stamp; keepalive refills still refresh the budget.

Docs correct the persisted-digest claim: a form change invalidates rows
whose stored key order was not already canonical, not every row.

* fix(cli): drop ambient QWEN_HOME from the cli test setup (#13314)

A run from inside a Qwen Code session always exports QWEN_HOME, which
outranks the homedir() mocks the settings suite anchors on: the
module-load getUserSettingsPath() then resolves to the ambient home, the
mocked user-settings file is never found, and 13 settings.test.ts tests
fail. The verification gate runs in exactly such an environment. Delete
the variable in test-setup.ts, mirroring the existing QWEN_RUNTIME_DIR
sanitization; tests that exercise QWEN_HOME set it in-body.

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

* fix(sdk-java): close hosted harness ledger wedges from review (#13314)

Unfenced 2xx/3xx mutation answers are now outcome-unknown instead of
definitive, unfenced refusals carry the peer's buffered body, the
per-attempt settle latch is owner-gated, close/detach retire only the
records they observed, and wire 409s stay reachable through
DaemonHttpException.

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

* fix(sdk-java): close hosted harness review criticals on digest recovery, refusal typing, and ledger identity (#13314)

- SubmitHarnessTurn accepts a persisted pre-canonicalization payload
  digest and re-sends the prompt in the exact as-built key order that
  digest covers, so recovering an older turn row reaches the route's
  replay path instead of failing the self-check before any request is
  sent.
- An unfenced 4xx on the generic mutation channel surfaces as the
  protocol failure it is rather than DaemonHttpException, whose status
  consumers key terminal verdicts on; only submitTurn keeps the typed
  refusal its admission-marker release keys on.
- ActivePrompt records the submitting client id and replaces the shared
  settle latch with a per-attempt send counter: lifecycle teardowns
  retire only markers whose identity proved stale, and a status read
  clears a marker only while no send attempt is in flight.

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

* fix(sdk-java): close hosted harness round-10 review criticals on close/detach retirement (#13314)

- README: correct the digest-migration paragraph — the client re-sends
  the shape its digest covers and accepts a pre-canonicalization as-built
  digest, so upgrades do not quiesce or rewrite in-flight turn rows
- closeSession(String): a successful headerless DELETE deleted the
  session outright, so retire every attachment for the id, including one
  registered while the DELETE was in flight
- submitTurn: gate all six owning-release arms on no send attempt still
  open, so a definitive refusal cannot strand a same-identity retry's
  202 with no ledger entry
- detachLifecycle: retire the observed admission marker even when no
  attachment remains (a refused close drops the attachment first), in
  both overloads
- closeSession(HarnessSessionRef): a 204 proves identity-matched
  deletion, so retire every marker for the id; keep identity-matched
  retirement only for the tolerated identity-scoped 404

* fix(cli): wait out the record watcher's backoff in the unrecorded-reply squad test (#13314)

The deterministic verification gate rejected the previous commit on
"wakes the leader with the error when a member reply is never recorded"
(default 1s vi.waitFor timed out waiting for the leader's wake). The
give-up that wakes the leader is eligible the moment the test bumps its
virtual clock, but it fires on the watcher's next real-timer check, and
that chain backs off geometrically (recordWatchMs doubling to a 15s cap)
— under full-suite load the next tick lands past the 1s default. The
sibling restart-recovery test already guards the identical pattern with
timeout: 3_000 and the comment "The watcher backs off; its next check
may be a while."; apply the same bound here. Assertion-preserving.

Probe: with the watcher's initial interval raised to 1.5s, the
unmodified test fails with the gate's exact assertion at the same line;
with the timeout it passes.

* fix(sdk-java): close HostedHarnessClient admission-ledger fences found in review (#13314)

Round-14 review found the admission-ledger fencing still open in eight
places:

- detachLifecycle(HarnessSessionRef) retired the pre-send snapshot even
  on a tolerated 404, wiping the marker the session's current owner
  planted through a re-issued identity; it now retires through the same
  identity-matched retireLedger detachSession uses, which also covers a
  marker this identity replanted while the detach was in flight.
- detachSession collapsed a deleting 204 and a tolerated 404 into one
  identity-matched retirement, so a proven deletion kept a marker from
  an older identity; the 204 arm now retires unconditionally.
- Both closeSession overloads applied the observed-snapshot rule to a
  generation change, so a marker published while the DELETE was in
  flight survived a proven restart; both now retire unconditionally on
  HostedHarnessGenerationException.
- The sendMutation channel and the streamEvents fence observed a
  generation change without retiring local state; both now discard it,
  and sendMutation threads the session id from every caller.
- The unfenced definitive-4xx arm released without reading the refusal
  code; an unfenced 409 is now narrowed by code like the fenced arm, so
  a boot-header-stripped hosted_prompt_recovery_required retains the
  marker and surfaces as PromptAlreadyActiveException.
- When both the owning attempt and a joined same-identity retry were
  definitively refused, neither released the marker; an owning release
  refused by an open sibling send is now owed and discharged at the last
  concluded attempt, while outcome-unknown or durably admitted
  conclusions block it.
- A non-owning retry's send window closed before its 202 republish, so
  an owning release landing in between dropped the joined entry and the
  replace silently no-oped; the window now closes after the ledger
  decision.
- The dispatch guard probed the executor, so a saturated-but-running
  pool was misclassified outcome-unknown; send() now dispatches with
  sendAsync and maps the synchronous RejectedExecutionException to a
  known-outcome DaemonTransportException, keeping post-dispatch
  rejections outcome-unknown.

Each fix carries a pinning test, and the three pre-dispatch escape tests
move their injection seam from the removed executor-state probe to the
executor's execute().

* test(cli): settle fresh process rows before identity-judged ledger sweeps (#13314)

The deterministic gate rejected the previous round on
managed-runtime-ledger.test.ts: on some hosts ps reports a just-born
process with the negative-elapsed wraparound (e.g. 441077234-00:18:40,
which parsePsElapsed rightly rejects) until it ages past the glitch, so
an unwitnessed sweep judged the live fixture 'unknown' and held it
unproven instead of killing it. settledGroupTable now also waits for
the leader's row to carry a datable elapsed column, and the five
live-spawn sweep tests that never settled now settle before sweeping.
Assertions unchanged.

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

* fix(sdk-java): report a close-aborted parked SSE read as a transport failure (#13314)

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

* chore(sdk-java): re-verify #13314 on a tree clean of generated Maven artifacts

The deterministic gate rejected 253326ce58 because repo-wide
'npm run lint' (eslint flat config lints .js repo-wide) swept up the
JaCoCo report resources that local 'mvn test' generates under
packages/sdk-java/*/target/site/jacoco/ — 139 errors in
jacoco-resources/prettify.js and sort.js. Those files are git-ignored
build output, not committed content; the committed tree is lint-clean.

This commit carries no tree change: the repair is the cleaned working
tree, with 'npm run lint' now passing, and build/typecheck plus the
focused R15-1 contract tests (HarnessEventStreamCloseTest and the four
HostedHarnessClientTest idle-timeout/close cases) re-verified green.

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

* fix(sdk-java): close hosted harness ledger and SSE close races

- Classify a parked SSE read's raced close as DaemonTransportException,
  matching the interrupted-read signal, with the idle-timeout half kept
  distinct; the programming-error type stays reserved for callers
  arriving after the close.
- Keep the admission ledger on every refused close, on a settled release
  whose entry is releaseBlocked, and for a code-less unfenced 409, which
  is outcome-unknown rather than a definitive refusal.
- Retire the marker an owning same-identity replay just planted when the
  promptId's terminal event was already observed, via the settled-checked
  release path.
- Read the getStatus marker clock before settledness and re-check both
  inside the bin-locked clear, so a retry's beginSend landing between the
  snapshot reads cannot be cleared under its open send window.
- Retire only the observed attachment on a by-id close, and self-retire
  an attachment whose heartbeat meets a definitive
  hosted_session_not_found 404 instead of beating against the dead
  session forever.

* chore(sdk-java): repair round verification on a clean working tree

The deterministic gate rejected 4884dc8197 on npm run lint: the previous
round's mvn test had generated the git-ignored JaCoCo HTML report under
packages/sdk-java/qwencode/target/site/jacoco/jacoco-resources/*.js, and
the flat eslint config lints git-ignored-but-present .js files (it does
not import .gitignore), failing on the vendored prettify.js/sort.js. The
root eslint config is outside this PR's footprint, so the repair removes
the untracked build output from the working tree. No source change; the
rejected commit's content is preserved as-is.

* fix(sdk-java): retire hosted admission state on journal replays and deleting lifecycle detaches (#13314)

* docs(sdk-java): state the digest canonical key order the code implements (#13314)

The published digest contract claimed object keys are sorted
recursively, but JsonSupport.ownKeyOrder and the route's
sha256(JSON.stringify(prompt)) re-derivation use ECMAScript own-key
order: array-index keys ascend numerically first, then the remaining
keys in insertion order. A second-language client built on the
published sentence computes a different digest and is refused with
invalid_hosted_prompt.

* fix(sdk-java): keep teardown retirement off live state and durable history (#13314)

A detach or a DELETE closes only the live Hosted session: the durable
journal survives it, and after a re-load the prompt route answers a
settled promptId's same-identity replay with a 202 that owes no terminal
event. Retiring the terminal-event record on teardown therefore left
that replay's marker vetoing every later prompt. No teardown retires the
record any more; the map is bounded instead (least recently used past
4096 sessions), and an evicted session's replay marker is cleared by the
busy-turn status probe.

The headerless lifecycle detach also removed the admission marker
unconditionally after its attachment snapshot, so a re-load that
registered a new attachment and admitted a turn inside that window lost
its marker and a different prompt reached the wire. Every definitive
teardown answer (detach 204 on all three entrances, the headerless
tolerated 404, DELETE 204 on both overloads) now retires through one
bin-locked decision that reads the current attachment under the
marker's lock: a marker the current attachment's identity planted, or
joined with a same-identity retry, stays; an abandoned identity's marker
retires. The identity-scoped tolerated-404 retirement keeps a marker
another identity's retry joined, under the same lock.

Tests: the teardown cases are parameterized over all five entrances;
fixtures whose vetoed submit hit an unrouted /status now answer the
busy-turn probe instead of waiting out the request timeout (the class
drops from ~320 s to ~20 s), and the probe's generation-change and
send-clock behavior are pinned.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
2026-10-16 21:06:22 +08:00
jinye
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 <qwen-coder@alibabacloud.com>

* 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 <qwen-coder@alibabacloud.com>
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
Shaojin Wen
70758572ff
feat(managed-agent): read-only snapshot children (#13753 I3) (#14265)
* feat(managed-agent): read-only snapshot children (#13753 I3)

A hosted parent can launch a child agent with isolation "snapshot": the
child reads a frozen copy of the parent's working tree, taken when it
starts, and nothing it does reaches the parent's Workspace. This is the
last slice of #13753, and it records as final that serialized sharing
needs no value of its own: the storage lease already admits one holder
at a time.

The copy is a child Workspace prepared as a worktree child's is. The
child bound to it gets a new hosted profile, hosted-workspace-read/1,
which declares read_file and glob and nothing else, whatever lanes,
orchestration or MCP the Session has. The store sets that profile from
the row's mode in the transaction that checks the copy is ready, so no
caller can bind a copy to a profile that writes; the tool turn refuses
every undeclared call before the Broker is acquired.

A snapshot's child Workspace records its mode in the request of its
child Workspace operation, written once and never rewritten; a request
that names none is a worktree's, so worktree rows read exactly as
before and no schema change is needed. A snapshot never merges: the
relay asks only its discard and holds no settlement for it, the store
records a stray merge ask as a discard, and its result commits with no
workspace member. A relay that meets a mode it does not serve settles the run
never started instead of creating it shared.

A snapshot child has no message lane, so send_message refuses it while
it runs and a later message continues it as a new snapshot; a team
member, which trades messages with its leader, cannot be a snapshot.

A separate switch, child-snapshots-enabled (off by default, requires
child Workspaces), adds childSnapshots to every hosted create and load;
only a Session carrying both flags offers or admits snapshot. A replica
from before this change would create a snapshot run as a shared writer,
so the switch goes on only after every replica runs this version; the
design gives the rollback procedure.

The I1 and I2 designs gain notes, and the I2 design's decisions 5 and 8
now name the give-up of an unbound row as the one relay discard of
completed work (#13905 item 1).

* fix(managed-agent): resume a snapshot child's committed reads after Runtime replacement

Review follow-up for #14265 (#13753 I3).

- The G3 step-3 path (#14059) resumes a results_ready checkpoint without
  reacquiring a retired Runtime owner, but it accepted only
  hosted-workspace-files/1. A snapshot child's read profile fell through
  to acquire(), which failed with "Cleanup Runtime owner changed.", so a
  load driving runtime recovery returned 503. The read profile now takes
  the same path: its batch is always read_file and glob, the batch that
  path already resumes without file history.
- isHostedWorkspaceSearchProfile is back to the /2 profiles only;
  approval pre-approves glob through the new isHostedWorkspaceGlobProfile.
- HOSTED_WORKSPACE_READ_TOOLS takes read_file by name, not by position.
- A team member launched as a snapshot is told only of the isolation the
  host serves.
- The Java profile literal moves to WorkspaceToolProfiles.READ.
- Docs: the I3 design and the G3 step-3 recovery note name the read
  profile.

* fix(managed-agent): address the second review of snapshot children

Pin the new Row.snapshot() accessor in the lock-order audit's exact
exemption set, which main's stricter pin turned red after the merge.

A running snapshot on a closing Session now gets the closing refusal,
so nothing promises a continuation that admission would refuse.

The read profile's read_file lookup now throws at module load when the
file tools lose read_file; spreading the failed lookup would have
declared a tool with no name.

Add the tests the review found missing: both sides of the glob
predicate, a launch body naming shared, the snapshot flag without child
Workspaces, unqualified launches on a snapshot-capable host, the legal
boot configuration, and foreground, folded and notified snapshot
results carrying no workspace line.

Update the design's file inventory, its post-merge lanes and the
replaced-Runtime validation wording.

* fix(managed-agent): give a snapshot child no skill catalog

Main's static Skills put skill instructions into every Session whose
agent definition publishes Skills. The read profile declares only
read_file and glob, so a snapshot child would be told to use a skill
tool it refuses, and an undeclared call fails the Turn.

A read-profile Session now drops the catalog once its profile resolves,
on create and on load. The design records the exclusion and that a
snapshot child still inherits its agent definition's hooks.
2026-10-16 12:58:42 +08:00
Shaojin Wen
76437355af
fix(managed-agent): bound retry loops with terminal states (#13219)
* fix(managed-agent): bound retry loops with terminal states

Asynchronous retry loops across the managed-agent stack had no terminal
state, and the message projection wedged permanently on a sequence gap.
Issue #13182.

- Projection: a sequence gap observed under the session row lock is
  provably permanent, so materializeNextBatch now logs once per gap and
  skips ahead instead of throwing on every scan at 10Hz forever.
- Turns: a new maxPostAdmissionRetries budget (default 10) terminates an
  admitted turn as hosted_harness_unavailable_after_admission once spent,
  distinct from the pre-admission code because the turn may already have
  run.
- Operations: a new maxOperationRetries budget (default 10) completes an
  unsettleable close/delete unconfirmed (java_durable) after a best-effort
  drain, and completes an undeliverable action response FAILED with
  action_response_delivery_failed instead of retrying forever. A committed
  decision seen on the projection re-check still wins over the budget.
- Scheduler: a memory-blocked pump arms a 1s recheck wake so a missed
  capacity notification no longer starves queued activations; transient
  store failures on claim/renew/release abandon only the affected
  activation instead of halting the worker, which still halts when the
  store reports consistency damage.
- Store: persist() repairs a torn journal tail (truncate to the last
  synced byte, like the open() loader) and only fails closed when the
  repair itself fails.
- Session close seals the writer in a finally; a failed HTTP seal keeps
  the grant renewed and retries the seal on the renewal cadence until
  sealed, bounded so a broken seal endpoint eventually lets the grant
  lapse for takeover instead of wedging the session.

* fix(managed-agent): record honest terminal failures on exhausted retry budgets

Address the PR review: the lifecycle exhaustion arm no longer reuses the
success-path completion write — a new fenced failOperation terminates the
operation FAILED with the settle's failure code kept, leaving the Session
in its pending status (a live journal writer keeps the operation waiting
instead). The gap heal settles orphaned items as failed, guarded to never
touch a turn whose terminal event is in the journal or whose turn row is
still live. The materializer backs off per session and surfaces a stuck
projection once. The embedded scheduler retries transient renewal/release
failures within the lease, halts on a consecutive-failure streak, and
converges per-activation via a transient-failure budget. Pending HTTP
seals now abandon after two lease durations, and a FAILED action response
whose Action is still requested is re-admitted on the caller's retry.
Docs, the OpenAPI descriptions, and the generated client match.

* chore: retrigger CI after a dropped pull_request synchronize event

* fix(managed-agent): key the writer gate on the writer, not the admitted status

Round-4 audit fixes. The exhaustion gate now checks hasLiveWriter for every
delivered shape — a delete of a closed Session whose retention retirement
keeps refusing on a residual live writer waits instead of terminating into
an unrecoverable DELETING row. The terminal arm mirrors settle()'s routing
exactly: a closed-Session delete makes zero Runtime calls. A retried action
answer re-admits only while the Session still accepts responses; the
post-admission turn terminal keeps the Runtime Broker's own failure code;
the scheduler prunes budget entries for activations that can never re-queue;
and the seal bounds, blocked-fallback and fencing comments now state what
the code does.

* docs,test(managed-agent): align the writer-exception wording and pin the round-4 gates

Round-5 audit follow-ups: the design docs' writer exception now covers every
admitted status (matching the gate that keys on the writer itself, in both
languages); the blocked-fallback test comment states the scanner's real
re-drive condition; and the two round-4 gates that lacked witnesses gain
them — a closed-Session delete terminating without any Runtime call (red
when the terminal arm's short-circuit is removed) and a replayed FAILED
action response staying terminal on an inactive Session.

* docs(managed-agent): state the closed-Session delete's zero-call terminal routing

The budget-exhausted paragraph claimed the terminal arm always releases the
binding through the workspace close pair; for a delete of an already closed
bound Session settle() — and now the terminal arm — deliberately makes no
Runtime call at all. Both languages.

* fix(managed-agent): stop the budget terminals from certifying falsehoods

Round-5 bot review (R2) Critical fixes. The lifecycle terminal arm now fires
only when settle() never returned — a completion-write failure after a
successful settle retries instead of recording session_lifecycle_delivery_failed
for a close that actually settled. The action terminal exempts the shape
where the Harness answered 200 and only Java's projection lags (a dedicated
DecisionNotYetProjected failure), so a committed decision is never recorded
as an undelivered one. Action re-admission narrows to the delivery-budget
terminal: a definitive Harness 400 keeps replaying its recorded row instead
of re-sending the refused body. The scheduler's release retry re-checks
liveness after its sleep, so a dispose racing the retry no longer writes a
terminal release on a stopped worker. Each fix is pinned by a test verified
red under its mutant.

* fix(managed-runtime): keep scheduler error paths total when the store clock fails

R3-1: store.getCurrentTime() validates its clock, and the renewal/release
error paths called it outside any try — a throwing injected clock escaped as
an unhandled rejection and leaked the active slot forever (pump skips keys
in active, so the activation could neither run nor re-queue, and the worker
silently stopped claiming once every slot leaked). Read the clock through a
helper that treats a throw as no-headroom/lease-expired, move the slot
cleanup and trailing pump into execute()'s finally, and turn any escaping
execute/renewal rejection into a loud halt. Pinned by a broken-clock test
verified red (timeout + unhandled rejection) against the pre-fix source.

* test(managed-agent): give bound Session fixtures the tool profile main added

Merging main brought #13301, which adds a toolProfile component to
SessionRecord and turns a null profile on a bound Session into an invalid
state: QwenHostedHarnessConnector throws "Hosted Workspace Session tool
profile is missing". The bound fixtures in the two retry-terminal suites
still used the 16-argument shape, so the module stopped compiling after
the merge.

The value is inert for these tests - both suites mock HarnessConnector, and
toolProfile() is read only inside the real connector - but a bound fixture
without a profile now describes a state production rejects, so it follows
main's own HarnessCoordinatorTest fixtures.

* fix(managed-agent): keep the named load refusal code past Turn admission

The post-admission exhaustion arm this branch adds recorded the generic
hosted_harness_unavailable_after_admission for every error except a Runtime
Broker failure, so a named load refusal (#13349) reaching that arm lost its
own code and blamed Harness availability for a Session the Harness had
deliberately refused to open.

The arm is reachable with a refusal because submissionAttempted is seeded
from the claim: a Turn an earlier dispatch already submitted, re-driven
while a mixed-version fleet refuses its load, exhausts this budget with a
refusal in hand. Within a single dispatch the load precedes the submission
marker, so the pre-admission arm keeps handling the common case.

Mirrors the pre-admission arm's precedence and logs the refusal code through
failureLabel rather than the exception class name. Mutant-verified: deleting
the new branch turns namedLoadRefusalsHonourThePostAdmissionBudget red,
recording hosted_harness_unavailable_after_admission where
managed_session_open_failed is wanted.

* fix(managed-agent): close round-4 review findings in the terminal-outcome paths

Consolidate the embedded scheduler's terminal-outcome handling into
execute(): every abandon path now converges after the handler settles,
where the scheduler records the handler's own outcome with the budget
spent — re-claiming a lapsed lease so the write fences live, yielding one
commit turn so a racing dispose/halt cancels it, and guarding the failure
classification so a stopped worker never latches a bogus halt. Stale-ended
runs now count the dropped run against the activation budget, bounding
re-runs of an activation whose store calls keep resolving past the lease
edge, and a terminally recorded activation still reports its handler error
through onActivationError.

The activation journal repair now observes the file length before
truncating: truncate() extends a shorter file with NULs and reports
success, so a journal that lost synced bytes out of band must classify
fatal, not transient.

On the Java side, the action-response re-admission gate no longer
resurrects a FAILED delivery row whose Action has expired (a fresh
admission would refuse 409 action_expired, and the resurrected PENDING row
wedged every later lifecycle command via requireNoOpenOperation), and
HostedHarnessClient.submitTurn probes GET /session/:id/status once before
refusing a different pinned prompt, so an outcome-unknown admission entry
left behind by a terminal failure stops locking the Session forever (from
the maintainer's stack-validated candidate patch).

* fix(managed-agent): close round-5 review findings in the terminal-outcome paths

The embedded scheduler's spent-budget terminal record could not tell "the
handler settled and we could not release it" from "we aborted the handler
ourselves": a cooperative handler returns normally on abort, so a
scheduler-induced cancellation was durably written as outcome 'completed'
— a final, non-reclaimable false success. Track whether the abandon
preempted the handler and record 'failed' for that shape; a run that
genuinely settled before the abandon keeps the handler's own outcome.

ActionResponseCoordinator's budget terminal inferred "the Harness never
answered" from the exception type, so a Java-side store fault after
resolveAction's 200 — the decision is already committed then — was
recorded as action_response_delivery_failed with harness_confirmed false.
Track the answer fact (200 return or the definitive 400) and gate the
terminal on it instead of the type.

HarnessCoordinator's post-admission budget terminal failed a Turn the
Hosted Harness may still be executing, and failTurn clears the dispatch
owner, so nothing could later cancel the admitted Turn or re-attach to
journal its output. Reconcile before terminating: best-effort cancel
through the Session's bound Harness, then record the failure.

The action-response re-admission gate healed a budget-terminal
action_response_delivery_failed row only while the Action was still
requested; once the projection showed the Action decided with this very
response's decision — the delivery did land and only Java's record was
wrong — the replay returned the stale failure forever. On a digest match
the replay now settles the row in place (COMPLETED, harness-confirmed,
with the decision receipt), fenced on the terminal FAILED state; every
other end state keeps replaying the recorded failure, and the row is never
resurrected to PENDING, so a second vote stays impossible.

* fix(managed-agent): close round-6 review findings in the terminal-outcome paths

* chore(managed-agent): republish round-6 fixes after the lint-gate rejection

The deterministic gate rejected the round-6 commit at npm run lint:
eslint scanned untracked maven javadoc output (packages/sdk-java/*/
target/reports/apidocs/*.js, plus runtime-broker test-classes *.mjs)
left behind by the local verification runs. That output is git-ignored
build product, not part of the fix, and the only committed-tree lever
(the workspace-root eslint config) is verification machinery outside
this PR's footprint. The build output is removed and the round-6
commit is preserved unchanged; this empty follow-up re-publishes it
with a workspace that passes build, typecheck, and lint.

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

* fix(managed-agent): close round-7 review findings in the terminal-outcome paths

- SubmitTurn's busy-turn status probe no longer leaks its own
  DaemonHttpException/DaemonProtocolException types: a failed probe keeps
  the plain retryable busy-turn refusal instead of terminal-failing a Turn
  whose prompt was never submitted (R7-2).
- The action-response answered fact is durable in the row's budget-exempt
  watermark: answered attempts reschedule budget-exempt and the terminal
  gate reads the watermark, so a mid-projection-lag failure after an
  answered attempt never records action_response_delivery_failed; the
  re-admission UPDATE resets the watermark so a re-clicked row keeps a
  terminable budget (R7-3).
- A delete admitted on an ACTIVE Session now publishes its live-writer wait
  as recovery_blocked/session_close_writer_live once the budget is spent,
  and the recovery scan re-drives every blocked lifecycle shape, matching
  the published design contract (R6-2).
- Reformat the two durable-lifecycle design-doc tables with Prettier so the
  lint lane passes (R7-1).

* chore(managed-agent): republish round-7 fixes after the lint-gate rejection

The deterministic gate rejected the round-7 commit at npm run lint:
eslint scanned untracked maven report output (packages/sdk-java/*/
target/site/jacoco/jacoco-resources/*.js) left behind by the local
verification runs. That output is git-ignored build product, not part
of the fix, and the only committed-tree lever (the workspace-root
eslint config) is verification machinery outside this PR's footprint.
The build output is removed and the round-7 commit is preserved
unchanged; this empty follow-up re-publishes it with a workspace that
passes build, typecheck, and lint.

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

* fix(managed-agent): renumber the budget-exempt migration to V46 past main's V41

Main's #13289 landed V41__workspace_csi_reservation.sql while this branch
carried V41__managed_operation_budget_exempt.sql, so the merge made two
migrations claim version 41. Flyway fails migrate() on duplicate versions,
which took down the Flyway uniqueness gate and both DB-backed jobs that boot
the server (MariaDB integration and hosted fault gates).

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

* test(managed-agent): pin cwd probe retries against the lifecycle terminal budget

Follow-up to the main merge: widen the two OperationRecord construction
sites in OperationRetryTerminalStateTest to the merged 18-component shape
(the merge commit's staged copies predated this fix), and add
coordinatorNeverAppliesTheLifecycleTerminalBudgetToACwdChange — with the
lifecycle budget configured below the probe budget, a transiently failing
cwd probe keeps its plain reschedule (RUNNING/PENDING, zero budget-exempt
watermark, untouched binding) instead of taking the close/delete terminal
arm or the writer-live wait. Mutation probes: deleting the terminal-arm
gate turns the pin red with state FAILED; deleting the writer-live gate
turns it red with RECOVERY_BLOCKED.

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

* fix(managed-agent): renumber the budget-exempt migration to V48

Merging main brought #13497, which took V47 for managed_channel_route_delivery.
This branch's budget_exempt_attempt migration was also V47, so Flyway refused to
resolve the schema at all:

  Found more than one migration with version 47
  -> V47__managed_channel_route_delivery.sql
  -> V47__managed_operation_budget_exempt.sql

Every Spring-context test then errored — 740 of 1112 locally — which reads as a
catastrophic regression but is one duplicated version number.

The column is this branch's own: main ships no budget_exempt migration and its
Java has zero references to budget_exempt_attempt, so V48 is simply the next free
version and nothing depends on the old number (no test pins a migration count or
version, and the column is referenced by name only).
scripts/check-flyway-migrations.js now reports 48 migrations, all versions unique.

* fix(managed-agent): stop renewing a failed close-time seal; bound the writer wait by retryability (#13182)

R1-38: the pending-seal cadence kept a failed-seal writer's grant live, and
the backend replays the same writer generation to a same-credential
re-acquire during a live lease, so the stale client could seal the journal
out from under a freshly re-attached session. Drop the cadence: the grant
lapses, a post-lapse re-acquire bumps the generation and fences the stale
client, and a later close() still retries the seal (accepted without a live
lease).

R1-30: the live-writer wait exempted every settle failure from the terminal
budget, so a permanent refusal thrown before the Harness was asked to stop
(e.g. workspace_close_identity_unverified) re-blocked the operation forever
while the Harness kept the writer. The wait now keys on the failure being
retryable, so a permanent refusal terminates with its code even while the
writer is live.

* fix(managed-agent): publish the stale-Harness wait instead of hiding it

The generation-mismatch wait was budget-exempt and invisible. The terminal
arm is vetoed by !staleBoot, and the exempt reschedule writes
budget_exempt_attempt = attempt_count, so the effective budget stayed at zero
and the row read RUNNING/PENDING with error_code NULL — byte-identical to a
healthy retry — for as long as this replica kept its stale Harness boot, which
only a Java restart clears. The writer wait, the other unbounded external
wait, already publishes recovery_blocked with its code past the budget; there
was no reason for this one to be the invisible half, and an operator cannot
restart the replica that a row does not name.

Publishes it the same way, as hosted_harness_generation_mismatch. The wait
stays unbounded and the recovery scan still re-drives it, so nothing
terminates earlier than before — the row is only legible now. Gated on
!cwdChange because a cwd change never reaches settle() and is bounded by its
own CWD_CHANGE_ATTEMPT_BUDGET.

Contract and docs follow: the failure_code description on both OpenAPI
surfaces and the regenerated client, and the durable-lifecycle design doc in
both languages, which also claimed "the one exception" and then described a
second one four lines later.

Pinned by aStaleHarnessViewPublishesItsWaitPastTheBudget (renamed from
...KeepsTheOperationWaitingPastTheBudget, which asserted the invisible retry)
plus aStaleHarnessViewReschedulesExemptlyBelowTheBudget, so publication stays
budget-gated rather than becoming the first response to a generation error.
Mutant-verified: deleting the new branch turns the first test red.

* fix(managed-agent): bound the answered action response by its Action's expiry

Round 7 made "the Harness already answered" a row-level watermark: once
budget_exempt_attempt is set, `answered` is true on every later attempt, so
the terminal gate `!answered && ...` became unreachable and the only reset was
a client replay. Java keeps no expiry scanner for managed_agent_action — only
the journal projection writes it — so an answer whose decision never projected
retried forever. That is not merely a spin: hasOpenOperation counts
ACTION_RESPONSE rows and requireNoOpenOperation gates close, archive, delete,
rename and unarchive, so one such row refuses every later lifecycle operation
on its Session with 409 session_operation_active while reading as a healthy
RUNNING/PENDING with a NULL error_code — the wedge this PR exists to remove,
reintroduced.

Bounds the exemption by the Action's own life, which is the condition the
exemption exists for: waiting only helps while the Action can still be
decided. Admission validates expiresAt as a required number greater than
createdAt, so a real Action always carries that deadline; once it has passed
while the Action is still `requested`, the operation completes FAILED with
`action_response_decision_expired`. The code is its own rather than
action_response_delivery_failed because the Harness did answer, and the record
stays java_durable because with no projection Java cannot certify what was
committed. An absent expiry is not read as a deadline, so a row this code
should not judge keeps waiting instead of terminating on an invented one.

Contract and docs follow: the failure_code description on both OpenAPI
surfaces and the regenerated client, and the actions design doc in both
languages — which also still described re-admission as needing only
`requested` plus a Session that accepts responses, omitting the expiry
condition the code has checked since round 4.

Pinned by anAnsweredResponseTerminatesOnceItsActionExpired. Round 7's
aLaterFailureAfterAnAnsweredAttemptStaysBudgetExempt stays green unchanged:
its Action carries no expiry, so the exemption it pins still applies.
Mutant-verified: removing the bound leaves the new test red with "wanted but
not invoked", i.e. the row retries forever.

* chore: retrigger CI after two dropped pull_request synchronize events

The pushes of 9989e1cea6 and d80cc925bd each fired the pull_request_target
workflows (assign owner, self-report label, force-push reminder, review) but
created no pull_request run at all, so Qwen Code CI, SDK Java and Web-shell
Visuals never started on either head — the checks panel showed ten checks
instead of the usual thirty, with no failure and nothing to rerun.

Same symptom this branch hit before (d594b2c164). No run exists to rerun and
no approval is pending, so the only lever is another synchronize event.
Content is unchanged; the code these two commits carry is verified locally
(Java 1117 tests with only the known-exempt pair and the #13386 flake,
core managed-runtime 2202/2202, web-shell managed 347/347, typecheck and
Checkstyle clean).

* docs(managed-agent): document the recovery scan's BLOCKED population

The scan re-drives every non-ACTION_RESPONSE operation in PENDING or
BLOCKED once its available_at has passed, and claimOperation accepts the
same two states — the widened predicate is what keeps the published
writer-live and stale-boot waits alive, since a restricted one would
strand exactly the rows the publication creates (including a delete of an
active Session waiting on a residual writer). recovery_blocked labels a
row that is still being retried; it is neither terminal nor a pause.

* fix(managed-agent): close review findings in the retry terminal-state paths

Seven correctness fixes from the latest review of the retry terminal-state
work:

- The budget-exempt column now counts exempt attempts instead of
  watermarking attempt_count, so a wait after charged failures no longer
  refunds them into an endless retry.
- The scheduler only resets its transient store-failure streak when an
  enqueue actually wrote (a duplicate performs no I/O), and no longer
  records a bogus store halt when a disposal races a failing streak.
- The action-response digest-match heal also settles rows that terminated
  as action_response_decision_expired; resurrection stays closed to them.
- The action-response attempt renews its operation lease across
  resolveAction, whose cold-load duration can outlive the claim lease, so
  the answered watermark can no longer be silently dropped.
- The post-admission retry budget honors the lease-bounded 409 exemption
  instead of terminally failing a Turn whose predecessor writer lease is
  still lapsing.
- Permanent daemon refusals (capability mismatch, protocol error) are
  non-retryable: they consume the budget and terminate with their own
  failure codes instead of waiting forever on a writer their settle never
  reaches.

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

* fix(managed-agent): preserve lifecycle budgets and admitted turn cleanup

* test(daemon): report active replacement prompts in detach fixture

* fix(managed-agent): keep the await-action approval wait exempt from the retry budget (#13182)

The await_action recovery decline routed to transientFailure with the
exemption flag unset, so a Turn parked on a pending user approval met the
post-admission budget (default 10 retries, about 303 s of capped backoff)
and was cancelled and terminally failed while the approval timeout bounds
that wait at up to 24 h. The exemption now applies, keyed on the exact
await_action reason; every other decline reason still terminates.

The witness is parameterized over retryCount 5, 10 and 42 with the
terminal path's cancel made observable: it goes red at 10 and 42 without
the exemption and stays green with it (review R3-9).

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

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

* fix(managed-agent): close terminal-release and writer-wait gaps from review (#13182)

- retryable() whitelists the retention retirement's writer code
  managed_session_writer_active, raised under exactly the predicate
  hasLiveWriter reads, so a delete of a closed Session waits on its
  writer instead of burning the terminal budget (R5-2)
- the terminal arm releases a bound protocol-v1 Session through the
  workspace close alone — requestHarnessDrain would flip the fence row
  without the lifecycle store's claim checks (R5-3)
- failOperation flips the LIFECYCLE_ONLY claim mirror to DRAINING in
  the terminal transaction, the release the completion path already
  performs, so a dead operation cannot hold harness admission closed
  (R5-5)
- a generation error wins the publication overlap with a live writer:
  that writer is the old boot's unexpired lease, not what bounds the
  close (R1-30)
- the pre-admission terminal cancel gates on the re-read Turn record,
  so a submission mark that landed after the claim still reconciles
  the admitted Turn first (R4-1)
- the named-recovery-refusal rows move out of the post-admission
  budget test into the pre-admission method they exercise (R5-1)

* fix(managed-agent): stop terminal writes from certifying unearned outcomes (#13182)

Review round 6 follow-ups:

- ActionResponseCoordinator: pass the missing harnessConfirmed=false at
  the non-retryable workspace_unavailable arm; the five-argument call did
  not compile (R6-1).
- ManagedAgentStore.failOperation: release the lifecycle claim mirror by
  clearing the dead operation's claim instead of flipping the fence row to
  DRAINING — the phase alone authorizes drainHarnessSession, and the
  terminal fail never verified the worker stop (R5-5).
- HarnessCoordinator.cancelAdmittedTurnBeforeTerminalFail: restore the
  recorded-admission (event epoch) precondition before the session-scoped
  cancel, re-reading the row; a bare submission mark can belong to a Turn
  the daemon refused while another Turn is live (R6-3).
- ManagedSessionOperationStoreTest: wrap the claim-mirror test's session
  insert in a TransactionTemplate — resolveForCreation requires a creation
  transaction and the suite builds the store without Spring proxies
  (R6-2) — and pin the fence row's post-fail shape.

* fix(managed-agent): close the round-7/8 terminal-state review findings (#13182)

- Rename the budget-exempt migration V53 -> V54: main merged its own
  V53 (workspace roles) first, and the duplicate version broke every
  Flyway startup (R8-1). The migration-order IT now expects "54", and
  the operation-store test fixture uses the post-roles role column
  (R8-2).
- V54 backfills budget_exempt_attempt = attempt_count for in-flight
  non-ACTION_RESPONSE operations: those attempts predate the budget
  regime and were never classified, so charging them would terminate
  long-retrying operations on their first post-upgrade failure (R7-5).
  The retention IT's operation column count bumps to +6 (R7-6).
- Split the Turn retry budget from the pacing counter: the three
  fenced cursor updates (admission, recovery admission, journaled
  events) now zero the new consecutive_failures column instead of
  retry_count, which keeps pacing the backoff — a crash loop that
  keeps making progress still converges to the terminal arm with
  growing delays (R8-3).
- settle() reports its irreversible Harness-facing step out of band
  and the terminal arm skips a settle that reached it: a close that
  answered the Harness but failed the workspace close no longer
  records "the settle never happened" (R7-2).
- The terminal arm consults the journal writer independently of the
  failure's retryability and never releases the Runtime binding under
  a live writer; the terminal record itself still stands (R7-3).
- The action-response resurrection arm now requires the session's
  storage to be unfenced, so a migration in flight keeps replaying the
  recorded terminal failure until the fence lifts (R7-4).
- The embedded scheduler's transient-store-failure halt bound scales
  with maxActiveSlots: one shared journal blip charges the streak once
  per concurrent run and must not exhaust it (R7-1).

* fix(managed-agent): close the round-9 terminal-state review findings (#13182)

- Raise the v1 harnessSettled latch where the irreversible step actually
  completes: settleLifecycle answered. A receipt write that refuses after
  the Harness settled is no longer recorded as "the settle never
  happened" (R7-2).
- Never terminate under a live journal writer: the terminal record would
  stand while the Runtime binding's release it owes never ran, stranded
  behind a row nothing re-drives. The refusal's wait is published
  instead — budget-exempt, recovery_blocked with its own code — and the
  terminal arm fires once the writer stops (R7-3).
- Bound the post-settle wait with its own larger budget: past four times
  the operation budget, a settle that reached the Harness terminates with
  session_close_completion_unsettled, which names what stayed unsettled,
  instead of retrying forever behind the open-operation barrier (R9-4).
- Re-admit the same lifecycle kind when the Session sits in its pending
  status with no operation open and a terminally failed operation of that
  kind: the new operation carries the failed one's pre-operation status
  forward and rebinds the released v1 fence row, so a budget-terminated
  close or delete is no longer un-recoverable without database surgery
  (R9-3).
- Repair the activation journal tail only when a torn tail exists: a
  failed no-op truncate on a read-only filesystem no longer halts a
  healthy store (R9-5).

The OpenAPI error_code descriptions, the regenerated WebShell client,
and both design-document languages track the new terminal code and the
writer-wait semantics.

* fix(managed-agent): advance the contract version past main's 1.38.0 (#13182)

The API contract uniqueness guard (#13804) fails a PR whose changed
contract document declares a version equal to or below latest main's.
Round 9 extended the error_code descriptions but kept 1.38.0, which main
already declares; bump to 1.39.0.

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

* test(managed-agent): expect the merged schema's eight post-V31 operation columns (#13182)

The round-10 main merge kept both parents' textually identical
hasSize(+7) on the post-upgrade managed_agent_operation row, while the
containsEntry list unioned to eight: each parent was correct alone
(branch: V46 x3 + V51 x2 + V56 + its own budget column; main: the same
plus V62 task_id instead), but the merged schema carries both V62 and
V65, so the row gained eight columns and the MariaDB-lane assertion
failed. Expect +8 and record the column arithmetic so the next merge
recomputes it.

* fix(managed-agent): close the round-11 review findings (#13182)

Four Criticals from review round 11, each pinned by a mutant-verified
test:

- R11-1 (ActionResponseCoordinator): the retry-budget terminal sat above
  the non-retryable workspace_unavailable terminal, so a permanently
  refusing Workspace authority recorded the generic
  action_response_delivery_failed once the budget was spent — a code that
  ManagedActionStore's re-admission allowlist resurrects, letting a client
  retry resurrect the operation and burn another full budget on a refusal
  that can never succeed. The named-refusal arm now sits above the budget
  arm, so the refusal keeps its own (non-re-admissible) code at every
  attempt count. aWorkspaceUnavailableCompletesTerminallyWithoutHarness
  Confirmation is parameterized over attempts below, at, and above the
  budget, with a negative pin on the generic code.

- R7-2-fix-induced (SessionLifecycleCoordinator + qwencode): the
  harnessSettled latch was set only after harness.settleLifecycle()
  returned normally, so a settle the Harness answered 200 but Java failed
  to decode (an invalid-JSON body or a receipt identity mismatch) was read
  as "never reached the Harness" and terminated with the plain budget and
  the raw protocol code instead of the settled budget and
  session_close_completion_unsettled. HostedHarnessClient now marks the
  post-answer failures with a dedicated HostedHarnessSettleAnsweredException
  (DaemonException, since DaemonProtocolException is final), and the
  coordinator sets the latch on it before rethrowing; the pre-send
  protocol-1 refusal never carries the marker and stays unlatched.
  aSettleAnsweredButUndecodableKeepsTheSettledBudget pins the budget edge
  at 10 (still reschedules) and 40 (terminates unsettled).

- R11-2 (SessionLifecycleCoordinator): the recovered-receipt branch
  equated "a durable effects receipt exists" with "a prior attempt reached
  the Harness", but WorkspaceLifecycleStore.recoverEffects synthesizes and
  persists a neverInitialized receipt for a Session the Harness never
  booted, with zero Harness contact. The latch now sets only for a real
  receipt: `!recovered.path("neverInitialized").asBoolean(false)`.
  aNeverInitializedReceiptDoesNotReachTheSettledBudget pins it.

- R11-3 (hosted-harness-session): a rejected close whose finally sealed
  the writer left the Session stranded in memory — deregistered never ran,
  and every later request resolved a dead journal. The close route now
  deregisters the Session whenever close() was attempted (its own finally
  either sealed the writer or failed trying), while a pre-close hook or
  admission refusal — which left the writer open — keeps the Session.
  deregisters the Session when the close sealed the writer but the release
  failed pins the 404-after-503 path; the existing "keeps the Session
  usable" case (a pre-close admission failure) stays green.

* fix(managed-agent): close the round-12 review findings (#13182)

* fix(managed-agent): key the close-route eviction on the seal outcome (#13182)

Review round 13 (R13-1): the R11-3 eviction was keyed on close() having
been attempted, but close() has pre-seal failure arms (the early throws
in sealForHandoffOnce) that reject with the writer still open — the
route then stopped the streams and deregistered a Session whose writer
was still live, stranding it with no handle to retry through.

The eviction now keys on the seal outcome. ManagedSessionJournalHandle
gains a writerSealed query — the local JSONL handle reads the lease's
isReleased, the HTTP store reads its own sealed flag, the read-only
snapshot-inspection handle reports true — surfaced through
LocalManagedSessionAuthority.writerSealed. The close route deregisters
when close() settled or the seal landed, and keeps the Session on a
pre-seal rejection.

Pinned by keeps the Session when the close rejected before the writer
seal landed (503, then the retained entry answers 503 again rather than
404). Mutant-verified: negating the writerSealed disjunct turns both it
and the R11-3 deregistration test red. Verified: build + typecheck
clean, hosted-harness-session 446/446, the three touched core modules
178/178, eslint and prettier clean.

* fix(managed-agent): resume terminal-failed workspace lifecycle operations (#13182)

A terminally failed workspace DELETE parked the Session in DELETING, which
the workspace status gate refused before the resume branch could run, and a
settle that had saved its effects moved the fence to DRAINING with the dead
operation still named, so the resume rebind matched nothing and the resumed
operation was fenced out of its own recovery. Recognize the permitted
failed-operation resume before the workspace status/protocol gates with the
failed operation's protocol pinned up front, rebind both parked fence shapes
(LIFECYCLE_ONLY claim-released and DRAINING still naming the failed
operation), carry the validated effects receipt to the resumed operation,
and verify carried receipts against the receipt's own hook-execution
namespace.

* fix(managed-agent): repair the merge-resolution fallout in the retry terminal-state tests (#13182)

* test(serve): widen the hosted cancel waitFor windows for CI contention (#13182)

The verification gate rejected the previous round on four tests in hosted-harness-session.test.ts whose default 1s vi.waitFor windows span fsync-backed journal commits (prompt admission to model call, and the cancelled turn's settle before the active flag clears). Same flake class as #14101, which widened two sibling windows in this file for a loaded CI runner. Assertions are unchanged; only the polling windows move to the file's established 10s contention budget.

* fix(ci): restore the Linux-correct NOTICES.txt from main (#13182)

My local build regenerated packages/vscode-ide-companion/NOTICES.txt against a pre-install node_modules on macOS, and the result diverged from the CI-fresh regeneration (is-fullwidth-code-point 3.0.0 vs 5.1.0 block). The branch adds no dependencies, so the correct content is main's; take it verbatim.

* fix(managed-agent): close the round-14 review findings (#13182)

Four Criticals from review round 14, each pinned by a mutant-verified
test:

- R14-1 (SessionLifecycleCoordinator): the terminal arm's drain-route
  guard excluded only protocol v1 (lifecycleProtocolVersion() != 1), so
  a v2 (SHELL/MCP) bound operation minted the DRAINING fence
  authorization itself and closed the Workspace on the strength of it —
  the exact thing the adjacent R5-5 comment says this path must never
  do. The guard now keys on the codebase's own operationIsLifecycle
  predicate (which covers v1 and v2 alike), so only the non-lifecycle
  (v0) route — the one settle() actually calls requestWorkspaceClose on
  — takes the drain request. boundV2SessionExhaustionSkipsTheV0Drain
  Request pins it (reverting to != 1 turns it red).

- R14-2 (WorkspaceLifecycleStore + SessionLifecycleCoordinator): the
  recovered-receipt branch treated every non-neverInitialized receipt as
  proof the settle reached the Harness, but recoverEffects also
  synthesizes and persists receipts itself — with zero Harness contact —
  for a booted Session whose definition has no hookCatalog. The store
  now marks both synthesis branches with storeSynthesized: true and the
  coordinator's latch excludes it alongside neverInitialized.
  aStoreSynthesizedReceiptDoesNotReachTheSettledBudget pins it at both
  budget edges (removing the new disjunct turns it red); the existing
  hookReceiptsUseCompactProtocolIdentities test's expected receipt gained
  the marker in its recover branch.

- R14-3 (ActionResponseCoordinator + ManagedAgentStore + AgentStateStore):
  the answered watermark that makes action_response_delivery_failed
  unreachable after a Harness 200 is per-attempt state, and its durable
  half (budgetExemptAttempt > 0) was written only by
  retryOperation(..., true) — the last statement in the catch block — so
  a throw anywhere earlier escaped with no DB write and the watermark
  never landed. The new AgentStateStore.markOperationAnswered (with its
  ManagedAgentStore impl) writes the watermark at the top of the catch
  block, before any recovery read, so a re-claimed attempt reads
  budgetExemptAttempt > 0 for a decision the Harness committed. The
  answer is durable, not per-attempt.
  anAnsweredAttemptPersistsTheWatermarkBeforeTheCatchBlocksReads pins it
  (removing the call turns it red).

- R14-4 (session-writer-lease): the assembly's finally-seal ran even
  when releaseActivation() rejected, and sealForHandoff memoized a
  failed seal permanently (terminalPromise ??= with no catch/clearing),
  so every later close() replayed the same rejection and the DELETE
  route's retention gate kept an entry that could never be closed. The
  seal now gets the same retryable-clearing treatment release() already
  had: on a retryable failure the memoized promise AND the terminal
  operation are cleared, so a later close() retries the seal rather than
  replaying the rejection. retries a retryable seal failure instead of
  memoizing the rejection pins it (reverting to ??= turns it red); the
  terminalOperation reset keeps release() from reading a cleared promise
  as a seal in flight.

Verified: the touched Java classes 147/147 green, assembly + lease
124/134, hosted-harness-session 463/463, full npm build exit 0,
typecheck 0 errors, checkstyle SUCCESS, prettier clean.

* style(managed-agent): satisfy the Managed Agent Server Checkstyle on this branch's files

main's qwen-code-checks.xml now enforces import order and duplicate
imports, and the daily Managed Agent Server Checkstyle lane would fail on
the files this branch adds or edits: regroup their imports (others, javax,
java, then static), drop duplicate imports, collapse three empty lambda
bodies, brace two single-line ifs and drop a blank line after a class
brace. No behaviour changes.

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: yiliang114 <1204183885@qq.com>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen Autofix <qwen-autofix@alibaba-inc.com>
Co-authored-by: qwen-autofix <qwen-autofix[bot]@users.noreply.github.com>
2026-10-16 11:32:57 +08:00
jinye
b5a266e1ed
feat(managed-agent): wire static Skills and extension configuration (#14319)
Connect published Skill resources and administrator-provisioned MCP/Hook pins to immutable Managed Agent sessions, with on-demand loading and durable recovery.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-16 02:40:37 +00:00
Shaojin Wen
e5f8aaaf35
fix(cli): close post-merge takeover findings from the #13083 review (#13188)
* fix(cli): close post-merge takeover findings from the #13083 review

- Track the recovered-but-undriven Turn on the Session: /status reports
  it in flight and /prompt refuses a fresh Turn until continue/cancel
  settles it, instead of looking idle over an unsettled checkpoint
- Map reconciliation failures in the recovered continuation to
  recovery-blocked instead of a terminal turn_result, so the
  coordinator's next takeover load can re-drive the Turn
- Read a 409 runtime_broker_execution_unknown as a definitive not-found
  only when the Broker marks it terminal, so a reconcilable UNKNOWN no
  longer skips the cancel of a possibly running execution

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

* fix(cli): deflake the lost-cancel replay witness

The test could meet the second cancel before the first one registered
its admission: the route awaits the run authorization between request
and admission, and polling /status no longer proves the admission once
a takeover also reports the Session in flight. Gate the replay on the
stop step's first broker status read, which only runs after the route
registered the admission, and arm the first cancel explicitly — the
PROBE .then() it replaced was load-bearing, since supertest requests
are lazy and only fire from a continuation.

* fix(cli): handle the /review round on takeover follow-ups

- broker: return the terminally abandoned record as its own 'abandoned'
  state instead of folding it into the not-found sentinel the stop step
  reads as already stopped; share one terminal-unknown classifier with
  the observation poll (review R1-1)
- recovery: accept 'abandoned' as cancellation-complete for the cancel
  and terminal poll while a reconcilable unknown keeps failing closed,
  and say in the contract comment that this stop is unproven
- session: keep a pre-wrapped recovery error un-wrapped so the block
  log names the cause an operator needs (review R1-3)
- session: refuse a user cancel in the undriven-takeover window with a
  scoped 409 instead of confirming a cancellation that never happened,
  and log the fresh-prompt refusal for oncall (review R1-2)
- tests: a raw 'warm gone' escape pins the continuation wrap that the
  retried workspace refusal fixture could not (review R1-4); add the
  abandoned stop-step witness (review R1-5)

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

* fix(cli): handle the /review round on takeover follow-ups (round 2)

- A blocked-for-recovery escape does not exist on the continue route —
  the Session stays registered and the connector drops its recovery
  tracking on the 200 admission — so a raw reconciliation escape (a
  transient warmup failure or a retryable re-acquire refusal) settles
  the Turn terminally; only an already-wrapped recovery error keeps the
  recovery-blocked handler (review R2-1)
- Record a cancel replay certified while the stop still ran, and stop
  waiting for a retry that cannot arrive in that case: the failure now
  reports the Session recovery-blocked (marker cleared) instead of
  armed-and-idle, while the plain 503 path keeps dropping its admission
  for the retries that do come (review R2-2)
- A refused lease release after a confirmed stop no longer 503s over a
  settled cancellation: the route settles the Turn, owes the lease for
  the next terminal route to retry, instead of trapping the coordinator
  in an identity-mismatch loop (review R2-3)
- recovery: one stop-terminal predicate shared by the pre-cancel skip
  and the post-cancel poll, plus the abandoned-mid-poll witness; the
  passive report fold gains its abandoned parameter (reviews R2-4/R2-5)

* fix(cli): handle the /review round on takeover follow-ups (round 3)

- Arm the undriven recovered marker only for a recovery the coordinator
  can actually drive: a passive report with any unknown execution
  outcome (and a drive report short of results_ready) is answered
  without the marker, so the Session stays usable instead of refusing
  every later route forever (review R3-2)
- A Turn settling without completing now drops its pending file-history
  obligation with it — one transient continuation failure no longer
  wedges every later cold load behind hosted_turn_recovery_required
  (review R3-3); the witness gains the detach-plus-cold-load leg it
  claimed
- The owed-lease marker records the identity whose release actually
  failed (the stop step's journalled runtime session id), so the MCP
  profile's drifted promptId-vs-runtime id cannot pin a tool session
  that was never acquired, and retried handbacks land on one owner
  (review R3-1, half 1)
- Give an owed lease a retry site that actually runs: a settled /prompt
  settles its finally by retrying the owed handback, and the replay's
  terminal route keeps its retry too; the owed-lease witness now drives
  the counts through all three sites on a persistent refusal and pins
  every attempt to the same owner (reviews R3-1 half 2, R3-4)
- Assert the two operator-visible stderr lines in the tests that reach
  them (review R3-5)

* fix(cli): handle the /review round on takeover follow-ups (round 4)

- Retire the pending file-history marker safely: one shared
  dropPendingHostedFileHistoryTurn in hosted-file-history.ts (used by
  both the cancel and continue routes), called only when the settled
  Turn can no longer resume and only after the terminal record landed;
  a failing retirement no longer turns a completed continuation into an
  error, and it stays owed — retried by the next settled /prompt or by
  close — instead of wedging every later cold load behind a refusal
  (review R4-1)
- A cancel whose fallible step fails after the durable checkpoint moved
  (the file-history retirement or the terminal record write) can never
  be re-driven against the same recovery identity, so the route admits
  it loudly — best-effort terminal record, recovery-blocked, marker
  cleared — instead of staying armed-and-idle with a permanent
  identity-mismatch for the coordinator (review R4-2)
- The undrivable takeover witness now observes the admitted Turn's real
  terminal outcome before teardown instead of racing its own cleanup
  into an ENOENT (review R4-3)
- The load's final else arm logs the reconciliation root cause before
  answering managed_session_open_failed, so a refused load-resume is
  diagnosable without losing the retryable arms' raw mapping (review
  R4-4)
- A recovered Runtime Session the Broker already forgot is treated as
  handed back: one releaseIsSatisfied predicate clears runtimeLeaseHeld
  on 404 in both shared release helpers and the inline release, so an
  owed handback retires instead of retrying with log noise forever
  (review R4-5)
- The user-cancel guard refuses the blocked-and-undriven state too, so
  a replay-certified cancel failure cannot fall back to a 204 that
  certifies a cancellation nothing stopped (review R4-6)
- /files/rewind gains the undriven-marker guard, so a route that borrows
  session.active as its busy flag cannot be admitted over a recovered
  Turn it would wedge (review R4-7)

* fix(cli): handle the /review round on takeover follow-ups (round 5)

- Keep the fence of a terminally abandoned record in the passive
  recovery report instead of folding it away as unknown-without-marker:
  an execution the Broker fenced permanently arrives as known + state
  'abandoned', which lets the coordinator cancel it via stopComplete,
  while a still-reconcilable unknown keeps advertising unknown. The
  drivable decision now lives in recoverHostedRuntimeTurn itself
  (every-known, with drive restricting to results_ready) instead of
  being re-derived at the load site (review R5-3)
- Retirement after a terminal settle now runs everywhere a Turn is
  durably settled: the continue body's outer catch retires the pending
  file-history marker next to its compensating write, and every owed
  retirement records its identity before handing itself to /prompt or
  close; on the cancel route the same flag guards a terminal outcome
  that was already durable when any later step failed (reviews R4-1,
  R5-2)
- The cancel catch orders settledDurable before the replay arm:
  a checkpoint that already moved finishes its idempotent settle first
  (the journaled set journals nothing twice), and only a settle that
  cannot finish again leaves the Session recovery-blocked, because a
  completed terminal record serves the admission's watermark better
  than a permanently blocked Session (review R4-2)
- The settledDurable flag derives from the checkpoint identity, not
  from the settle call's return, so a partially applied settle inside
  one resolveAwaitRuntime at a time is told apart from a retryable
  miss; the never-completed witness pins recovery-unravelling into
  recovery-blocked with a no-turn_result guard for the still-unjournaled
  functionResponses (review R5-1)
- On merge of main: refuse-load ownership recording (refusedAdoptions),
  release-after-terminal-record without redoing answered cancellations,
  the load-refusal log phrase kept for the pinned variant, with the
  cause-chain root intact for wrapped ones

* fix(cli): close the round-6 takeover review's lease, marker, and honesty gaps

- gate the settled-/prompt lease release on a terminal route's failed
  handback so a passively adopted lease is never compensation-released
- retire an owed file-history marker at /prompt admission, awaited inside
  the busy flag, and retire a settled Turn's durable marker at load
- keep a replayed-but-failed cancel re-drivable instead of latching a
  permanent recovery block, and owe the marker only for a durably
  settled Turn
- answer the user cancel 204 on a recovery-blocked Session whose Turn
  already settled; only an armed undriven recovery refuses
- journal an unobservable outcome for executions accepted on the Broker's
  terminal fence instead of certifying an unwitnessed cancellation
- teach the wire type and the Java client the abandoned recovery state

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

* fix(cli): close the round-7 takeover review's cancel failure-semantics gaps

- classify a settle failure against the checkpoint the drive started
  from, never the request's possibly-stale load-time checkpointId, so a
  transient miss that moved nothing durable stays re-drivable instead of
  latching a permanent recovery block
- surface a refused compensating terminal write and drop the admission,
  so the coordinator's retry re-drives the idempotent settle plus the
  record instead of replaying a watermark no turn_complete can confirm
- owe the file-history marker only once the terminal record landed, and
  keep the recovered-Turn refusal armed until it does

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

* fix(cli): close the takeover review's cancel/continue re-drive gaps

- A partially applied settle that cannot finish now drops the cancel
  admission instead of latching the Session recovery-blocked, so the
  coordinator's retry re-runs the idempotent settle and a healed store
  settles the Turn instead of wedging every later cancel on a 409.
- A terminal cancel write that committed durably but lost its
  acknowledgement is detected by re-reading the durable log: the settled
  Turn's marker is owed and disarmed, so the settled replay answers the
  retried cancel and a fresh prompt is admitted instead of the marker
  wedging the Session forever on a record that exists.
- A redriven takeover load re-arms the undriven recovered-turn marker
  when the re-answered recovery is drivable, matching the first load, so
  a fresh prompt can no longer race the parked executions it describes.
- A retryable Workspace acquisition refusal escaping a continuation's
  resumeCommittedResults() raw is classified into the recovery-blocked
  handler, so the recovered Turn stays re-drivable by a later takeover
  instead of settling terminally and stranding the checkpoint's
  unconsumed Runtime receipts.

* fix(cli): keep the adopted Runtime lease on recovery-blocked continue exits

A continue drive classified recovery-blocked still ran the route's
unconditional finally handback, and the blocked replay refusal released
too: a successful release persists RELEASED, so every later takeover's
same-identity re-acquire conflicts with runtime_session_not_acquirable
and the parked Turn can never be re-driven. Both exits now keep the
lease held, mirroring the cancel route's blocked-refusal discipline;
close remains the single handback owner.

Also pin the witnesses this round's review found missing:
- release-spy assertion on the re-drivable continuation test
- compensating-settle marker retirement covered for both the direct
  drop and the owed-then-retired failure half
- dropPendingHostedFileHistoryTurn fixture gains pendingMessageId and
  undoReceipts so the rebuild shape and the merge are pinned
- the late-rejected handback's owed arm is epoch-guarded by a mirrored
  re-adoption test
- the Monitor wake pump test gains its precondition, retry-refusal and
  delivery oracles
- gate the acknowledgement-lost fault so the fresh-prompt phase
  witnesses a clean settle

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

* fix(cli): close the takeover review's redrive, wake-pump, and settle-honesty gaps

Round-4 review of the takeover stack, six Criticals:

- A redriven load that settles the parked Turn (the cancellation settle,
  the inapplicable projection, and the no-parked-Turn restatement) never
  discharged the undriven marker an earlier load's lost reply had armed,
  so /prompt refused the Session for the rest of the incarnation. All
  three arms now disarm once the Turn is settled.
- The Monitor wake pump parked on the marker alone, so an undrivable
  redrive released it onto the parked checkpoint while the journal still
  held the input unsettled; the pump now reads the same unsettled-inputs
  gate /prompt refuses on.
- settleParkedTurnCancelled resolved void on three silent exits, so a
  cancelled Turn whose settle could not even verify the park was
  certified settled. It now reports settled | noop | not-runnable; the
  cancel route treats not-runnable as a retry-inviting failure and the
  compensating retry maps the real outcome.
- The user-cancel marker guard duplicated the unsettled-input refusal
  five lines below it and refused the settled-Turn case that must keep
  its 204; the journal guard now owns the refusal alone.
- A recovery-blocked /prompt exit still retried the owed lease release,
  persisting RELEASED under an identity the next takeover must
  re-acquire; the retry now mirrors the continue route's blocked
  discipline.
- A Broker record that vanished after its cancel was issued counted as a
  witnessed stop; it now joins the unobserved set.

* fix(cli): close the takeover review's handback, settle-honesty, and noop-disarm gaps

Round-9 review of the takeover stack, four Criticals:

- releaseIsSatisfied treated any 404 from release() as a completed
  handback, so a foreign-coded 404 (a gateway, route version skew, a
  Broker-side route miss) cleared the lease flags and pinned the
  Workspace lease forever. The predicate now requires the
  runtime_session_not_found code, matching hosted-hook-session and the
  Broker's own 404 gate.
- The cancelled settle recomputed its outcome from each attempt's Broker
  reads, so a compensating retry republished a witnessed cancellation
  over a durable journal entry that honestly reported the outcome
  unobserved. The dedup projection now keeps the journaled parts keyed
  by functionCallId and the retry republishes them verbatim.
- The unobserved set died with its request: a record reclaimed after a
  failed attempt read as already-stopped on the retry and was certified
  as a witnessed stop. The cancel drive now accumulates the ids on the
  Session through a caller-owned set the stop fills as it counts — a
  stop that throws mid-way still carries them — and clears the entry
  when the Turn's terminal record lands.
- The cancel drive's nothing-durable branch deleted the admission
  without checking whether the refused terminal write had actually
  committed, wedging the Session on a committed turn.settled with the
  recovery marker armed forever. It now mirrors the sibling branch's
  settledRecordLanded scan: disarm and keep the admission when the
  record exists, delete only when nothing committed.

Also fixes the Lint & Static failure: parkToolTurn's
`boolean | typeof CALL[]` parameter type violated
@typescript-eslint/array-type and Prettier; it is now
`boolean | Array<typeof CALL>`.

* fix(cli): deflake the owed file-history admission witness

The admission-retirement witness synchronized on the owed drop having
been invoked, but the continue drive's busy flag clears only in its
finally, so the follow-up /prompt could meet a 409 hosted_turn_active
while the daemon behaved correctly — a ~2% red that reads as a product
regression. Gate the prompt on hasActivePrompt clearing, the same
synchronization the sibling settle witnesses already use.

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

* fix(cli): honor the unobserved-stop contract on every interrupted-turn settle exit (#13188)

Review round 9 found three holes in the interrupted-turn runtime
settlement:

- R9-1: a broker status() rejection at the pre-cancel read or the
  post-cancel poll unwound past every unobserved.add site, so the
  execution id never reached the caller's carry; a retry then read the
  reclaimed record as already-stopped and certified a cancellation
  nobody witnessed. Both reads now count the id into the carry before
  the fault unwinds.
- R9-2: settleInterruptedTurnRuntime discarded settleParkedTurnCancelled's
  'not-runnable' verdict, letting the tail and the caller's terminal
  record build on a settle that journaled nothing. It now throws a
  plain retry-inviting Error, matching the takeover cancel drive.
- R9-3: the abandoned-call tail never received the unobserved set and
  stamped executionStatus 'cancelled' on calls whose stop was never
  observed. It now answers them with the honest unobservable outcome,
  exactly as the parked-execution settle does.

Also fixes two stale stopParkedRuntimeExecutions call sites left over
from the {broker, unobserved} return-shape change: one in
settleCrashedWakeTurnAftermath (a TS2740 that broke build/typecheck)
and one in its test (which made the broker identity assertions fail).

* test(cli): deflake the three serve tests the verification gate caught

The merged tree's gate run failed six tests in five files; the three
whose details survived the capture all share one shape — a timing
budget sized for an idle runner:

- hosted-harness-session: both child-acceptance tests witnessed
  turn.settled in the journal and then asserted a 204 close
  single-shot, but the pump's busy epoch is still closing out at that
  point and the close can answer 409 for one more tick (the window the
  F10 test documents). Both closes now retry through vi.waitFor,
  exactly like F10.
- hosted-hook-session: the 30-fire drain history costs ~25 s unloaded
  here and overran the gate's 60 s test timeout under runner
  contention; pin a 180 s per-test budget.
- acp-http/transport: the SSE helpers' 2 s default frame budget aborts
  a healthy stream when a contended runner stalls the event loop for
  seconds; raise the default to 15 s. Call sites that assert silence
  keep their explicit budgets.

* fix(cli): carry unobserved runtime stops through the wake aftermath settle (#13188)

- settleCrashedWakeTurnAftermath discarded the stop's unobserved set, so a
  Broker-fenced execution journaled as a witnessed cancellation (R10-1).
- The pre-cancel 'unknown' throw never counted the id into the caller's
  carry, so the retry certified a stop nobody observed (R10-2).
- The carry was add-only: an id counted by a transient fault survived a
  later attempt's positive settled witness and journaled a fence that
  never happened. Prune at the two positive witnesses only (R10-3).

* test(cli): stop over-asking frames the stream never carries (#13188)

The raised 15 s default frame budget turned two latent over-asks into
test-timeout hangs: both call sites waited for more frames than the
server ever sends, previously truncated silently by the 2 s budget.

- TOCTOU test: the session/new reply rode the delivery stream newSession
  opened, so the later connection stream is owed only the load reject
  (a replayed duplicate may or may not precede it) — read until it lands.
- Untrusted-workspace Workflow test: the connection stream carries
  exactly five replies, not seven.

* test(cli): drop a leftover DIAG console.error from a serve test (#13188)

* fix(cli): honor the settle verdict and unobserved carry in the wake aftermath (#13188)

- R11-1: settleCrashedWakeTurnAftermath discarded
  settleParkedTurnCancelled's 'not-runnable' verdict, so a settle that
  could not verify the park still released the lease, consumed the input
  and journaled the Turn's terminal record. It now throws into the
  function catch, answering 'pending' for the retry — the contract the
  three sibling call sites already honor.
- R10-1: the aftermath's unobserved carry died with a thrown stop (a
  per-pass local), so the pump's retry read the reclaimed record as
  already-stopped and certified a cancellation nobody witnessed. The
  carry now seeds from and persists to session.recoveryUnobserved,
  clearing when the Turn settles — mirroring the cancel drive.
- R11-2: pin the abandoned-call tail's per-item discrimination with a
  mixed-batch witness (one fenced execution, one witnessed stop), the
  shape under which a batch-level flattening previously survived every
  shipped test.

* fix(cli): persist pruned unobserved carries and refold committed siblings on the compensating cancel settle (#13188)

* fix(cli): refold committed siblings on the continue route's settle exits (#13188)

R13-1: the continue route's compensating settle wrote the terminal
record without the committed-sibling fold the cancel route just gained,
so a takeover continue drive faulting before its resume (the drive's
first project() read) durably settled the Turn with a committed
team_create/task_create still unanswered — and the kept admission
replays a retried continue without re-driving, so no route ever paired
it. Core's orphan repair would then tell the next Turn's model to retry
the call, redoing committed work under a new key.

Both continue settle exits now run answerCommittedTurnCalls (the
uncommitted: 'none' variant, per the hosted-runtime-recovery contract)
ahead of the terminal record: the compensating arm in the outer catch,
and the direct error settle after the inner catch. Each fold gets its
own try/catch, so a fold refusal is never mistaken for an ambiguous
terminal-write failure — the Turn stays re-drivable instead of settling
over unanswered calls.

Pins (mutation-verified: deleting either fold turns its test red):
- the compensating settle answers the orphaned team_create when the
  drive's first project() read fails
- the direct settle answers it when the resume fold's own answer write
  fails raw

Also declare the teams flag on the domainEnablement fixture: the team
fold tests set it, and without the declaration the file no longer
compiled (TS2339), which broke the build and every downstream CI job.

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

* test(cli): deflake the park settle waits the verification gate caught (#13188)

The merged tree's gate run failed five tests in hosted-harness-session,
all at one shape: a vi.waitFor on the 1 s default budget polling a full
HTTP settle (202 prompt -> refused durable write -> status flip). Solo
the park completes in ~400 ms; under the gate's 25%-worker contention
the settle outlives 1 s and the wait times out with the last poll's
hasActivePrompt still true. The waits are healthy; their budgets were
sized for an idle runner.

Pin the file's established 15 s / 100 ms budget on the failing class:
the shared parkUnsettledPlainTurn helper (feeds all five failures), the
three waits in the compensating-settle test body, and the byte-identical
park in the cancel-on-settled test. No assertion changed or removed.

* test(serve): widen approval-fallback and issue-13328 windows for CI contention (#13188)

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

* fix(cli): answer committed board commands on every recovered-Turn settle exit (#13188)

R13-1: the three committed-sibling folds this PR added to the continue
route (direct and compensating settle) and the cancel route's
compensating settle copied the fold's argument list without
`commands: session.commands`. Without the command funnel the fold
cannot see a member's committed board call (`commandId` stays
undefined), and `uncommitted: 'none'` then skips it, so the terminal
record lands over an unpaired call that the kept admission never
re-drives.

All four folds on these two routes now go through one helper,
answerRecoveredTurnCommittedCalls, so they share a single argument
list.

Witnesses (the member command-wait park, continued after a takeover):
- the compensating settle after the drive's first project() read fails
- the direct settle after resumeCommandWaitRuns fails raw
Both assert call-1 gets exactly one answer, taken from its command
journal. Mutations: dropping `commands` from the helper turns both red
(`expected [] to have a length of 1`); restoring the commands-less
inline list at either continue exit turns only that exit's witness red.

The cancel route's compensating exit is reachable only from a Runtime
park; its existing team_create witness covers the fold there, and the
helper gives it the same argument list.

* fix(cli): retire the owed file-history marker before recovered-lease cleanup (#13188)

A cancellation whose marker retirement fails once lands turn.settled on
its compensating path and owes the marker in memory. The recovered
lease's hosted.cleanup obligation refuses to release while the marker
still names its Turn, but both terminal exits ran that cleanup first:

- close released the lease, swallowed the refusal, then retired the
  marker and removed the Session, so it answered 204 with Broker.release
  never called;
- the lifecycle effects phase retried the owed cleanup with nothing left
  to retire the marker, so it answered hosted_lifecycle_recovery_required
  on every retry. This is the control plane's close path for a bound
  files Session.

Both exits now go through retireOwedFileHistory before any lease
cleanup: releaseLeaseNow (close/detach and the v2 settle_resources
phase) and the lifecycle route, which holds the closing gate before it.
A failed retirement still stays owed in memory and durably, and the next
retry or load retires it.

Witness: an it.each over DELETE and lifecycle effects + detach, with
warm() returning the Runtime binding so the parked owner commits a real
cleanup obligation, asserts Broker.release reaches the original Runtime
once and the obligation becomes confirmed. Dropping either retirement
site, or restoring close's release-then-retire order, turns its case red.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
2026-10-15 12:04:21 +00:00
jinye
859a337500
chore: adopt qwen-code-checks.xml and fix all checkstyle violations (#14186)
* chore: adopt qwen-code-checks.xml and fix all checkstyle violations

Add qwen-code-checks.xml (based on alibaba/fastjson2 convention) with
ImportOrder enabled (groups *,javax,java; static at bottom). Enable
includeTestSourceDirectory so both main and test sources are checked.

Fix all 869 violations across src/main/java and src/test/java:
- Reorder imports to *,javax,java groups with static-at-bottom
- Remove unused imports
- Normalize control statements (if/for/while/do/else/try-catch-finally)
  to multiline braced form
- Fix indentation to match expected levels
- Split multiple statements per line
- Fix whitespace around braces, annotation style, and DOT spacing

mvn checkstyle:check -> BUILD SUCCESS, 0 violations
mvn test-compile -> EXIT 0

* fix(sdk-java): repair the checkstyle rewrite and satisfy qwen-code-checks.xml

Two mechanical passes in the original rewrite were wrong, and neither was
caught because the verification in the description never actually ran.

The control-flow pass inserted braces around wrapped expression
continuations, treating a ternary, a cast, a string concatenation or a
constructor call in a ternary branch as a braceless if/for body. That left
the module unparseable: javac reports 809 syntax errors across the module,
while the same sources on the merge base parse clean.

The import pass was not anchored at column 0, so it reached inside a Java
text block, treated the embedded TypeScript import as an unused Java
import, and deleted the class declaration, a field and a live ObjectMapper
import along with it.

Each spurious brace is undone only when doing so strictly lowers javac's
syntax error count for that file, which keeps the insertions NeedBraces
genuinely requires; nested constructs converge one level per pass. The
text-block file is rebuilt from the merge base and reordered by a
column-0-anchored pass that refuses to rewrite a block holding anything it
does not recognise.

The remaining violations are then cleared: imports into the *,javax,java
groups, whitespace from checkstyle's own line:col so no edit can land in a
literal or comment, and the two places where the ruleset has no mechanical
solution -- an empty switch-arrow block, where RegexpMultiline forbids
"-> { }" and WhitespaceAround forbids "-> {}", so the no-op default arm is
dropped; and a switch expression directly after "(", where ParenPad and
WhitespaceAround contradict, so it moves into a local.

checkstyle:check goes from unparseable to 0 violations, against 1835 on the
merge base under this ruleset. javac is clean, test-compile succeeds, and a
token-level audit against the merge base -- literals and comments compared
verbatim, braces and whitespace ignored -- shows no difference outside the
six edits this message names.

* ci(sdk-java): gate managed-agent-server checkstyle in the fast lane

qwen-code-checks.xml applies only to managed-agent-server, but nothing
enforced it outside the clean-verify lanes, which need MariaDB and a host
lock and have starved before. checkstyle:check declares no dependency
resolution, so it needs no sibling install and slots in beside the two
existing checkstyle steps.

64669 bytes, within the 4096-byte ratchet over the 69416 baseline.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-10-15 03:56:11 +00:00
jinye
462cae3dd8
fix(managed-agent): Wire OSS capture budgets into Hosted sessions (#14124)
* fix(managed-agent): Wire OSS capture budgets into Hosted sessions

Pass the configured raw output budget on enabled Shell-family creation while preserving saved budgets on load. Cover the SDK wire shape, connector profile selection and the public lifecycle collection path.

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

* test(managed-agent): Align Hosted recovery fixtures with OSS capture

Adapt the public and process-recovery fixtures to the current publication catalog, remote capture budgets and canonical storage owners. Retain immutable test objects across Spring restart and verify physical collection after all four recovery cuts.

Reserve fixture ports together and allow the existing ledger and capture retry cadence without changing production scheduling or identity checks.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-14 08:21:37 +00:00
jinye
17fe37cdc1
feat(managed-agent): add L4 Shell and MCP session lifecycle (#13931)
* feat(managed-agent): add Shell and MCP lifecycle v2

* fix(managed-agent): enforce exact lifecycle profiles after main rebase

* fix(managed-agent): reject new model attempts throughout lifecycle v2

* test(managed-agent): verify public inline capture collection

* fix(managed-agent): verify foreground lifecycle capture evidence

codex: fix CI failure on PR #13931

* fix(managed-agent): verify original Shell capture input digest

codex: fix CI failure on PR #13931

* codex: address PR review feedback (#13931)
2026-10-13 08:16:54 +00:00
Shaojin Wen
c5098a2733
feat(managed-agent): H4 workflow child runtime (#13858)
* feat(managed-agent): H4 workflow child runtime (#13803)

Issue #13803's four exit checks land in one change because the
`managed-child_run` kind gate fires on every revision commitment: the
relay and cascade verbs on a workflow row would answer 409 until the
gate admits the kind, so the query widening (K2) and the gate flip
(K4) cannot be separated.

K1 — the ChildResultRelay discovery now selects `workflow` rows, and
creation branches on the record kind: a workflow launch mints exactly
one child Session under the parent's Workspace binding and lineage,
whose first Turn carries a `workflow_launch` block (pinned definition,
script, args) instead of a prompt. K3 — every produced launch pins
its definition from revision 1: the pin's digest is the sha256 of the
exact script bytes (revision 1 by convention, id `workflow/<name>`),
and the child's first Turn re-verifies the digest before spending
anything.

The daemon gains a `HOSTED_WORKFLOW_TOOL` declaration that mirrors
the Agent tool's admission behind its own kind reader; a workflow
child Session runs its first Turn deterministically in-process via
WorkflowRunner (its agent() calls are in-process AgentHeadless
subagents under ordinary caps, never nested managed grandchildren).
Wake, close, and consumption predicates treat both child Session
kinds through one funnel; quotas already count the two kinds
together.

One cross-language contract consequence: a workflow launch block is
the first prompt ever to carry fractional `args` across the daemon's
`sha256(JSON.stringify(prompt))` admission digest, so the SDK's JSON
digest substrate is canonicalized to ECMAScript byte-fidelity —
otherwise a fractional arg wedged the first Turn with a
deterministic `invalid_hosted_prompt`.

Physical acceptance in #13532's shape ran against the packaged stack:
end-to-end foreground and background workflow children with result
delivery verified on a real host; workspace-close cancellation is
blocked-with-reason on macOS (host capability, re-confirmed on the
final tree); a tampered pin refuses execution; the mid-interruption
re-claim path is partially observed and filed as issue #13838.

* fix(cli): refuse nested saved-workflow resolution outside the pinned slice (#13803)

The managed workflow child pins its script from the launch, but a
model-authored script's own workflow() call resolved saved workflows
the pin never examined, including bytes from the daemon host's own
~/.qwen/workflows (user tier). Admission now refuses any script whose
masked source invokes the injected workflow global, with the same
model-correctable text that names name, scriptPath, resumeFromRunId
and sourceRef as the deferred reference scope. The hosted runner
passage also passes restrictNestedScriptPaths as a second line, and
the declaration no longer advertises workflow() in this profile.

Witnesses: both workflow('x') and workflow({scriptPath}) forms refuse
without committing a child_run, the same text inside strings or
comments admits, and removing the admission gate turns both refusals
red.

* fix(sdk-java): render big numbers through the double the script peer holds (#13803)

The canonical writer computed its 2^53 exact-integer guard on a
value already narrowed through longValue(): Long.MIN_VALUE stayed
negative under Math.abs, and any BigInteger at or past 2^63 was
truncated -- sometimes sign-flipped -- before the check, printing
digits JSON.stringify never produces while both sides digest the
same corrupted text and therefore agree. Long and BigInteger now
flow through the shared double path, and the BigDecimal arm renders
through the double the parser could hold except in the subnormal
band, where the exact digits are the ECMAScript short form ('5e-324')
that a blunt double round-trip would print as '4.9e-324'.

Golden vectors pin Long.MIN_VALUE, 2^63, 2^64 and the decimal
examples (12345678901234567890.5 prints 12345678901234567000;
1E-400 prints 0; 1E+400 is refused); a 38-case parity probe matches
Node's JSON.stringify byte-for-byte, and removing each guard turns
the named witnesses red.

* fix(cli): close the review's pin-, admission-, preview- and doc-record gaps across the workflow child lane (#13803)

* fix(cli): adopt main's mount-refusal taxonomy in the workflow child admission path (#13803)

* fix(integration-tests): teach the hosted workspace tool-turn fixture the H4c workflow advertisement (#13803)

* fix(cli): harden the workflow child runtime lane against the review's round-1 findings (#13803)

Real-stack verification round 1 found the lexical admission's nested
workflow scan bypassable by indirection, and the foreground workflow
wait lost to any Harness restart. This change closes both:

- WorkflowRunner gains refuseNestedWorkflows: when set, every nested
  workflow() resolution — by name or by path, aliased or through the
  global — is refused, since any of them resolves bytes the launch pin
  never examined. The managed workflow Turn passes it; the lexical
  pre-launch scan stays as the model-friendly early refusal.
- acceptChildWorkflow's foreground arm parks #13708's durable
  commitAwaitAgent checkpoint before it waits, exactly as the child
  agent arm does, so a restart re-enters the wait from the journal
  instead of declining the parent's Turn unresolved_after_settle.

Merge-integration cleanups ride along: the member-chain walk narrows
by kind where the roster view needs ChildAgentRun, and the recovery
gap fill and the shared ended text name the run's kind honestly.

Witnesses: indirection forms now settle failed with the named refusal;
the park commit is asserted with the exact checkpoint evidence; the
canonical-args refusal of the existing pre-commit gate is pinned
end to end (top-level and nested subnormals, nothing committed).

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

* fix(cli): settle a registry-cancelled workflow start as cancelled, not failed (#13803)

The real stack showed a WorkflowStartCancelledError — cancelStarting or
abortAll reaching the run's controller while the Turn's own signal stays
live — settling the child as child_failed because only the abort signal
classified the outcome. The turn now reclassifies that cause to
cancelled/cancelled, the same reading the Workflow tool gives for the
identical error; deadline_exceeded keeps its name, and error keeps its
name — only the generic error outcome yields to the cause.

Witness: the harness session settles with a cancelled turn_result when
the workflow start is torn down registry-side.

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

* fix(sdk-java): normalize lone surrogates before the digest and the insert, and keep protocol messages' field paths (#13803)

Two digest-honesty findings from the real stack and the reviews:

- The canonical encoder escapes an unpaired surrogate in the admission
  digest, while no store column keeps the character; re-encoding the
  read-back input then never matches the payloadDigest, and the Turn
  retries forever (payloadDigest does not match promptContent). Input
  admission now trades unpaired surrogates for U+FFFD before the payload
  digest, the replay digest and the insert, on the API path and on the
  child-creation path, so all three describe one representation.
- Four DaemonProtocolException messages that named only the context lost
  their field path in this PR's digest work; they name the field again,
  pinned by a witness that reads all four texts.

Witnesses: the child-creation and user-text admissions normalize before
the captured digest and insert, with valid pairs and non-strings left
untouched; the helper-rejection tests pass unchanged.

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

* docs(design): retire the stale closed-gate claims and record the runtime slice's settled honesty contract (#13803)

The H4c design's items 5 and 6 still named the kind gated and the Java
paths child-agent-only; the kind doc now names them closed only in its
own slice and opened with this one, in both languages. The runtime
design gains the behaviors the review rounds settled: the runner's
refuseNestedWorkflows line behind the lexical scan, the waiter arm's
durable park, the registry-side cancel settling cancelled through
WorkflowStartCancelledError, and lone-surrogate normalization at
input admission keeping the digest and the column one representation.

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

* fix(sdk-java): satisfy Checkstyle NeedBraces on the surrogate-normalization helpers (#13803)

The helpers added in 52958aade4 landed bare for-loop bodies; the
managed-agent-server Checkstyle lane (NeedBraces) refuses them on real
runs. Wrap all three loop bodies in braces - no behavior change.

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

* fix(cli): write the hosted workflow turn fixture's NUL byte as a printable escape (#13803)

The pin-tamper fixture embedded a literal NUL inside the definitionId
string, which makes the file binary to git and to review tooling. Write
it as \x00; the test asserts the same byte.

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

* fix(cli): refuse protocol-ceiling tool inputs per call instead of wedging the Turn (#13803)

prepareRequests computed the canonical digests unguarded, so a tool
input past the protocol JSON ceiling (depth/size) threw out of the
whole parent Turn and settled it hosted_turn_failed (real-stack
round 2, D1). The canonical-digest calculation now converts
ManagedToolProtocolError into that call's own model-visible validation
refusal - "Hosted tool input cannot commit as protocol evidence: ..." -
while the inline Session Store limit keeps main's rejection contract
exactly. The workflow-child witness pins the refusal text and that no
record commits under the ceiling.

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

* fix(cli): give the managed workflow child's in-process subagents no tools at all (#13803)

The real-stack round-2 review found the child script's agent()
subagents dispatching tools against this Harness's own cwd and host,
never the Session's Workspace. In v1 the child declares no tools at
dispatch (WorkflowRunner.refuseSubagentTools threaded into
createProductionDispatch): the fast path hands AgentHeadless an empty
declaration list, while the override path denies every requested tool
by name through the existing narrowAgentTools refusal. Each arm sits
under its own witness, verified red by disabling its guard. Routing
subagent tools through the Session's Hosted tool path is the named
follow-up; the runtime design records the v1 posture in both
languages.

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

* fix(sdk-java): encode non-contract request values instead of refusing them (#13803)

The canonical encoder that replaced fastjson2 for every daemon request
body threw on any value outside Map/List/String/Boolean/Number. The
channel submit_input body carries its policy as the Jackson tree read
from the store column, so every inbound channel event was refused with
400 and all six FG7 channel fault gates failed (0 physical sends).

Such values now render as fastjson2 rendered them before, are read back
strictly into contract values, and are then canonicalized, so the wire
bytes stay canonical whatever the caller handed in.

* fix(core): give a tool-refusing workflow host's override-path agents no tools and no host-bound options (#13803)

The managed workflow child runs with refuseSubagentTools, but only the
fast path declared an empty tool list: any dispatch-affecting option
(model, schema, effort) took the override path, where an absent
allowlist is the subagent manager's '*' floor, so the subagent got the
Harness host's whole registry. The override path now declares [] (or
only structured_output for a schema dispatch), and agentType, isolation
and workingDir, which resolve definitions, start MCP servers or provision
worktrees on this host, are refused before anything resolves.

* fix(cli): state and admit the managed workflow's text-only agents up front (#13803)

The declaration advertised agent() without saying its agents carry no
tools here, so a model wrote the ordinary tool-bearing shape and the run
failed inside the child after a launch slot was spent. The declaration
now states the posture, and admission refuses a script whose agent()
literally passes tools, agentType, isolation or workingDir before any
record commits; options built elsewhere still meet the runtime refusal.

* fix(cli): refuse workflow args where the digest would, and over-limit encodings per call (#13803)

- The args walk now starts at the level the protocol digest counts
  args at, so it refuses exactly where that digest would throw instead
  of passing a band the digest then refuses (R3-6).
- The subnormal number arm is gone: the Java digest substrate now
  renders every finite double the way JSON.stringify does (R1-3, R4-6).
- An arm's own refusal is no longer overwritten by the generic
  protocol-evidence text.
- A workflow launch (or any already-refused call) whose input passes
  the inline Session Store limit once JSON-encoded twice answers a
  per-call byte_limit refusal instead of failing the Turn (R2-12); such
  a batch is never captured for G3 recovery.

* fix(cli): encode a child launch only on its fresh path, never before its replay (#13803)

The admitLaunch extraction evaluated the envelope encode as an eager
argument, so a re-driven agent launch whose member preamble had grown
past the 32 KiB bound since its commit threw byte_limit before the replay
branch could name the committed record, recovery-blocking the Turn. The
bytes are now a thunk read only where a fresh launch publishes them or
the default matcher compares them.

* fix(cli): run the workflow lane on the Session's pinned model (#13803)

- runHostedWorkflowTurn now takes the Session's AgentDefinition and
  applies its model pin through the same helper the text Turn uses
  (exactly one configured non-runtime match, cached credentials, the
  registry base URL); a pin that does not resolve fails the Turn instead
  of running every agent() on the default model (R3-4).
- The assistant commit is no longer duplicated across the two lanes
  (R4-5).
- The truncation marker no longer points at a run snapshot that lives on
  the Harness host, where this Session's tools never read (R1-32).

* fix(core): refuse a non-finite number in workflow launch args instead of writing null (#13803)

* test(core): state which narrowing step is the workflow gate test's control (#13803)

* test(cli): abort the half-batch deterministically at the first admission (#13803)

The test observed the first record from a 10 ms polling timer, so the
abort sometimes landed after the second launch was minted (about one
run in four). It now aborts as the first admitWorkflow commit returns;
the batch admits launches one after another, so the second meets the
cancel guard every time (20/20), and removing the guard turns it red.

* refactor(core): hand the pre-launch gate's compile and shape to its callers (#13803)

workflowPreLaunchRefusal compiled the script and scanned its shape, then
threw both away, so the runner compiled the same bytes again and the
hosted admission scanned them again. workflowPreLaunchGate returns the
meta and shape of a script that passes; the runner reads its meta from
it and the admission its shape (R2-13).

* fix(cli): answer a committed workflow launch on recovery and name every child answer by its record's kind (#13803)

- The settle and resume routes looked a committed launch up only for an
  agent call, so a committed workflow launch was answered by neither
  (R3-9); both now answer it by what committed, like an agent launch.
- The kind label is derived from the record in one helper, so the ended
  sentence, the background started receipt and the notification can no
  longer name a workflow child 'Child agent'; the recovery fill's
  background receipt now names the workflow kind too (R3-7, R3-8, R4-7).
- A message addressed to a workflow child's task is refused as a
  workflow task instead of as an unknown one (R2-10).

* test: make the workflow tests assert what their names claim (#13803)

- The broker-warm test runs a tool-profiled Session, so only the
  workflow-launch half of the guard keeps the tool turn away.
- The Turn-owned mount hold takes the background launch it advises.
- The over-size envelope case reaches the envelope's own bound with
  three-byte args, and the encoder's boundary gains a UTF-8 case.
- The funnel test reads the notification the funnel wrote for its kind.
- A launch re-driven at the full concurrency cap names its record (R2-8,
  R4-8).

* fix(sdk-java): derive canonical number digits JDK-independently (#13803)

JsonSupport.encode took its digits from Double.toString (via
BigDecimal.valueOf), whose shortest-round-trip guarantee only exists from
JDK 19; on the Java 11/17 runtimes this artifact supports, doubles past
2^53 picked up a 17th digit JSON.stringify never prints, so the prompt
digest disagreed with the daemon's. The subnormal band rendered a
BigDecimal literal's own digits (3E-324 as 3e-324, not 5e-324) and
refused the same double when it arrived as a Double.

Numbers now render as ECMAScript Number::toString of the double the
script side holds, derived from the double's exact value: the first
digit count whose floor or ceiling neighbour round-trips, the closer one,
the even one on a tie. Every finite double encodes on every JDK, and a
Node-generated oracle of 10,694 doubles (every binade boundary, random
subnormals, each notation band) pins it.

The builder now refuses a BigDecimal or BigInteger past the double range
up front, and an admission body that fails to encode never leaves the
client and releases the session's prompt slot instead of holding it
forever. The per-character escape no longer goes through String.format.

* fix(managed-agent): name a workflow launch in the projected user message (#13803)

A workflow child's first turn input is one workflow_launch block with no
text key, so materializeInput skipped it and the public message list showed
a user item with zero parts. The projection now renders the launch pin as
one input_text part, in the same shape as the daemon journal's
workflowLaunchSummaryText; the block itself keeps its closed key set.

* test(managed-agent): pin the relay's unknown-kind refusal and the creation replay digest (#13803)

refusesToCreateAnUnknownKindInsteadOfGuessingAnEnvelope verified the
5-arg createChildSession overload the relay never calls, so a guess that
took the prompt arm stayed green. Verify the 6-arg overload (also in the
workflow drive test) and assert the deferral reason names the refusal.

The lost-reply replay test compared the replay digest with the attempted
one, which holds for any derivation. Both now equal a sha256 of the
canonical creation request computed in the test itself.

* refactor(managed-agent): share the child Session kind list and retire the stale cancel premise (#13803)

The relay discovery page and the close cascade each hand-listed
task_kind IN ('child_agent', 'workflow'); both now read one constant so a
third kind joins both queries or neither.

CANCELLABLE_TASK_KINDS still said the workflow kind has no runtime. It
does now; its public cancel is a named follow-up slice, so the set stays
closed by slice boundary.

* docs(managed-agent): retire stale workflow-gate claims and record the subagent tool posture (#13803)

The H4c and I2 designs now mark the closed-gate statements the runtime
slice superseded (decision 3, decision 7, the runtime non-goal, the
validation notes and the acceptance criterion; the I2 quarantine lines),
and quote the shipped gate value with H3's shell. The H4a delivery row
names what the workflow slice still defers. The runtime design records
the migration numbers at the head (V68 highest, V69 next free), fixes
decision 6's broken link to workflow-agent-tools.ts, states that both
subagent dispatch paths declare no tools and that admission refuses an
agent() tools option, and adds the subagent-tools follow-up row.

* docs(managed-agent): name every host-bound agent() option the workflow child refuses (#13803)

* chore: type the funnel notification read and declare the oracle generator's node global (#13803)

* fix(managed-agent): hand a tool-refusing workflow agent its runtime tool list (#13803)

The override path wrote the refusing host's empty allowlist onto the
SubagentConfig, but the subagent manager reads an empty definition
allowlist as "inherit everything" and convertToRuntimeConfig turns it
into '*'. A Hosted workflow agent() carrying model or effort therefore
received the Harness's whole registry, read_file included.

Pass the list as the manager's toolConfigOverride instead: [] for a
plain dispatch, structured_output alone for a schema dispatch. The
unit tests now pin the runtime override the manager receives.

* fix(managed-agent): start Hosted workflow agents without the Harness's startup context (#13803)

Every agent() chat of a Hosted workflow child opened with the per-turn
config's startup context: this Harness's absolute cwd and a listing of
it. That is host filesystem metadata, never the Session's Workspace,
and it reached the model even when the agent declared no tools. The
sibling text turn never sends it because it replaces that prelude with
the Session's own history; the workflow agents have none, so the
per-turn config now skips the startup context.

hosted-workflow-turn.integration.test.ts runs the plain, model, effort
and schema dispatches through the real subagent manager and a real
per-turn config against a fixture model, with a Harness-only file in
the cwd: every request declares no tools (structured_output alone for a
schema dispatch) and carries neither the file's name, its content nor
the Harness path. It goes red with either fix reverted. The design doc
records both.

* fix(sdk-java): order canonical map keys as JSON.stringify enumerates them (#13803)

JsonSupport.encode wrote a map's keys in the map's iteration order, but
JSON.stringify writes array-index keys ("0".."4294967294", no leading
zero) first in ascending numeric order and only then the rest in
insertion order. The daemon re-parses a body and stringifies it that
way, so a map built or read with integer-like keys out of that order
would digest differently on the two sides. Maps with no array-index key
keep the original iteration path.

* test(sdk-java): pin signed zeros and JSON.stringify key order in the canonical encoder (#13803)

The generated oracle never reached -0: its loops clear or randomize the
sign elsewhere. canonical-doubles.mjs now ends with 0 and -0 (appended,
so the seeded lines before them are unchanged), and the golden vectors
carry 0.0 and -0.0 explicitly.

A key-order golden test pins Node's output for an object with
array-index, leading-zero, out-of-range, negative, fractional and
padded keys, flat and nested. It goes red with the encoder's own-key
ordering reverted.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-13 05:05:40 +00:00
Shaojin Wen
21b78c55d5
feat(managed-agent): member board tools ride the session_command channel end to end (H4e-b3b) (#13928)
Some checks are pending
SDK Java / Runtime Broker and Managed Agent MariaDB / Java 21 (push) Waiting to run
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Flyway migration version uniqueness (push) Waiting to run
SDK Java / Hosted process fault gates / MySQL 8.4 / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(managed-agent): member board tools ride the session_command channel end to end (H4e-b3b)

Carry decision 13's second wave from the team-member-commands design: members of an open team write and read their leader's task board through the session_command channel. A member's board call mints an outbound entry on her own journal before her Turn parks on an await_command checkpoint (never carrying an input, digest-bound args/reply pairs, per-run budget, batch-locked against Workspace or Runtime siblings); the message relay's new command pass drives it in sender order to the leader — planned → handover → execute → answered, never through consume — and the parked call folds the leader's reply back and marks the entry consumed. The leader executes off the model Turn path: replay-stable receipts for board_writes, an open-team member on its running chain head for fresh commands, the member-facing refusal texts, and a board_read that changes nothing and never receipts.

Both languages carry the contract, the lineage and resource rules, and the store side of the line. The lead's execute answers a not-yet-attached sender's run with a 409 session_command_blocked the relay retries without spending attempts (it can outrun the control plane's attach commit), while a genuine lineage or evidence mismatch stays a terminal session_command_refused. The wait holds a 2-minute bound from the outbox's commit; its timeout tells planned asks 'not sent', handed asks 'reached the leader, reply missing, write possibly landed', and locks the rest of the batch under one text, and the harness descent into await_command now resumes or settles every named wait from the journal alone (decision 14). A member Session declares the three tools exactly behind her lineage, the enabled domain, and her member preamble — never before all three are true — and join keeps refusing 'parent' as a name.

On the aisle where recovery and workspace restore walk the journal, session_command joins the recoverable-domain whitelist so a member holding a commanded write never wedges her later loads, and qwencode classifies await_command like await_agent so a Turn parked on it takes over cleanly. G3 physical: two members write and read back the board, their leader reads it, a member's write races a leader's write without a phantom create, and a member Harness restart mid-wait lands her report anyway — each over a real MariaDB stack with scripted models (HostedTeamMemberCommandMySqlIT, 2/2).

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

* test(managed-agent): admit the scenario lead over the internal TeamLead lane (#13674)

The public Shell admission a8990042 introduced is behind its own
workspace-shell opt-in and approval-mode gate, so a Session written to
hosted-workspace-shell/1 by test fixtures now refuses every wake POST as
workspace_unavailable even when the lane is enabled. The MySQL Scenario
ITs ride the same internal TeamLead profile the baseline migrated to:
insert the lead over the trusted internal lane and let it pin the team
lead profile itself; no public Shell flag is involved.

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

* test(managed-agent): align the team MySQL ITs with b3b and the broker opt-out

Members of an open team now declare the three board verbs (task_create,
task_list, task_update) through the session_command channel, so the
team-lead fixture's lead-only exclusion narrows to team_create and
team_delete. The scenario IT's child-result settles may end close_debt
whatever the OS — delivered, with only the workspace close owed — so
both settle gates accept it as terminal.

The broker's durable local-process mode only exists on Linux; the
team-lead fixture now opts it out on non-Linux hosts instead of
shipping the flag unconditionally, keeping Linux CI's boot surface
identical. Without workspace execution a workspace Session's close
answers 409 workspace_unavailable: closeAndSettle tolerates it (no
operation binds to wait on) and the cascade case aborts through
Assumptions rather than failing where its premise cannot run.

Verified locally on macOS: both ITs, 5 tests run, 0 failures, 0
errors, 1 skipped (the cascade case abort), BUILD SUCCESS.

* test(cli): give the budget witness its own timeout budget

The merged-b3a run slows each member-side command commit enough that
255 hand-minted outbound records plus the assertion ride past Vitest's
15 s default on a shared runner, while the rule's own check is single-
digit seconds on an idle box. The budget's witness now carries an
explicit 30 s allowance rather than riding the suite-wide default.

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

* fix(managed-agent): judge command order by the outbound's open window

A cancelled outbound's leftover relaying row never gets another scan —
HANDOVER_SQL selects only planned/accepting/unknown records — so the
ledger-only open predicate held every later command of that sender
behind it forever, deferring each one every five seconds without
spending an attempt. The row's open window is the record's
planned/accepting span; a terminal record never blocks again, whatever
row state its last step left.

Refs: QwenLM/qwen-code#13928 review 4244450992.

* fix(managed-agent): classify a sender-terminal command on sight

HANDOVER_SQL still selects a command record whose accepting span the
member's own timeout flipped to cancelled or unknown, and workCommand
only distinguished planned: the terminal entry fell through to the
leader's execute, re-running a command its sender already saw settled —
the second execution decision 2 forbids, live in the member's board the
next time it re-reads or retries a write. A non-planned, non-accepting
record is classified on sight and returned, whatever the ledger row
holds; the classification depends on the record's own committed state,
not on the row having been classified first.

Refs: QwenLM/qwen-code#13928 review 4244450996.

* fix(serve): answer a never-sent command wait on resume, never settle nothing

The await_command checkpoint names every call of the batch before any
outbound entry commits, so an owner that exits between the two leaves
a run with no record. The recovered wait anchored the bound on the
resume itself, waited the full two minutes, and then settled cancelled
— which throws "has no record to revise" — leaving the parked call
unfoldable and the Turn unresumable. A missing record is permanent, so
the resumed poll ends the wait at once with the live arm's own not-sent
answer, word for word (decision 14), and folds the run.

Refs: QwenLM/qwen-code#13928 review 4244450991.

* fix(serve): carry decision 4's command timeout lock in the wait group

The lock that refuses every later command of a Turn once one command
timed out lived only in the owning Turn's memory: after the result and
the folded checkpoint committed, a restart raised a fresh
HostedWorkspaceToolTurn for the same prompt and the next board_write
minted an outbound that ran to consumed — exactly the leader silence
the lock existed to refuse. The fold that carried the timeout now
stamps the group, every later fold carries the stamp, a fresh wait
starts clean, and a replacement owner of the Turn re-arms from the
carried group before it mints the next wait.

Refs: QwenLM/qwen-code#13928 review 4244450997.

* fix(serve): fold the parked wait on a member-facing command refusal

The await_command checkpoint commits before the batch's first outbound
entry, so a budget_exhausted (or byte_limit) refusal that answered the
call but skipped the fold left the Turn's durable wait parked: the next
command call of the same Turn met its own durable wait and surfaced as
a recovery fault instead of the same local refusal. The member-facing
refusal arm now answers and folds, like every arm that runs.

Refs: QwenLM/qwen-code#13928 review 4244451000.

* fix(serve): judge member addBlocks writes against the board as it stands

A well-formed but absent addBlocks target reached the ownership probe
before the reference check, so memberTaskUpdate read undefined.owner
and escaped as a TypeError instead of an answer. Existence now comes
first, in the initial validation and again per replayed place. And a
partially committed task_update replayed only an owner check for its
remaining places: an edge the original validation allowed could since
have closed a cycle on the live board, which the authority rightly
refused — but the refusal escaped execute as a fault, so the relay
retried it forever and the member never got its receipt. Each remaining
place is now re-validated against the board as it stands, and the
authority's deterministic conflict rides the same Not-committed
receipt.

Refs: QwenLM/qwen-code#13928 reviews 4244451002, 4244451008.

* fix(serve): answer a never-sent command wait on resume even when cancelled, never settle nothing

The recovered wait checked the cancelled signal before checking whether
the checkpointed call's outbound ever existed: an owner that cancelled a
recovery over a parked, never-minted call fell into the journal arm,
which treated the missing record as planned and settled cancelled —
throwing "has no record to revise" with neither the tool result nor
the fold committed, stranding the wait at await_command. The
missing-record arm now runs before every signal arm, and the shared
journal answer treats an absent record as the planned arm, never
reaching settle.

Refs: QwenLM/qwen-code#13928 review 4246316365.

* fix(serve): rebuild the command timeout lock from the journaled answer, not just the fold

The lock that refuses every later command of a Turn after one timeout
relied on the fold carrying the timedOut stamp: the dead owner commits
the unknown (or cancelled) tool result first and the fold stamps second,
so a crash between the two leaves a takeover owner holding a group that
still reads timedOut=false. The next command of that prompt then mints a
fresh outbound and rides to consumed — the leader stayed unanswering and
nobody told the model. The owner now also re-arms from this prompt's
journaled timeout answer itself (decision 14's own texts and the locked
leader-unanswering text), so the persisted evidence of the timeout never
depends on the last fold having succeeded.

Refs: QwenLM/qwen-code#13928 review 4246316346.

* fix(serve): re-check the blocker cap per remaining place on a replayed member task_update

The replay loop re-validated existence, ownership and cycle for each
remaining addBlocks place against the board as it stands, but never the
blocker-count limit the initial whole-command check covers: a partially
committed command whose leader since filled its target to the cap met
teamTaskRevisionBody's own "blockedBy exceeds 64 entries" record error, a
deterministic refusal that surfaced as an escape fault — no receipt for
the relay, the same error again at the next retry. Each remaining place
now re-evaluates the current board's picker count against the maximum
before committing, folding the deterministic refusal into the same
Not-committed receipt the cycle and authority checks already fill; true
storage faults still escape.

Refs: QwenLM/qwen-code#13928 review 4246316356.

* fix(serve): stamp decision 4's lock at detection time, never scan tool-result text

Two repair attempts rebuilt the timeout lock from journaled tool-result
text, and both misfired on legitimate data: an ordinary task_list reply
that contains the leader-unanswering words as its subject locked the
Turn's next command, and the missing-record path's planned-not-sent
answer did the same on a healthy continuation. A crash between the
settle(unknown) write and the tool_result commit passed the window too —
the record is already terminal and nothing structural says the Turn
timed out once. The lock now has exactly one durable carrier: the wait
group's own timedOut field, stamped at deadline detection time by
markCommandWaitTimeout before every other commit the timeout arm makes
(settle, tool_result, fold). Recovery re-arms from that field, never
from what text happened to land in the journal, and the prompt still
recognizes the genuinely-timed-out chain and refuses it identically.

Refs: QwenLM/qwen-code#13928 reviews 4247995945, 4247995960.

* fix(serve): keep decision 4's lock on the Turn and settle the timed-out call by its journal

The timeout lock lived on the command wait group, which the Turn's next
command batch replaces and its Runtime work clears, so a takeover in
either window let a later board call mint an outbound and reach the
leader. The lock is now a root `commandLock` naming the Turn that
stamped it: every checkpoint of that Turn carries it, and a checkpoint
of another Turn drops it.

A recovered wait no longer answers every run with the leader-unanswering
text once the lock is on. A run whose outbound exists is the call whose
own wait timed out — its owner may have died between stamping the lock
and settling the entry — so its journal answers it: planned is cancelled
as not sent, accepting becomes unknown, a settled state is said as
committed. Only a run that sent nothing gets the fixed refusal, as in
the live arm, which applies the same rule to a re-driven call.

* fix(serve): never consume a timed-out call on resume, and end the lock with its Turn

A recovered wait answered a timed-out call through the journal even when
its answer was already committed, so a relay reply copied in after the
settle was consumed although the call only ever read the unknown
evidence. A call already answered was settled first, so the lock arm now
only folds it, as the funnel's rule for timed-out calls requires.

The next-turn-ready checkpoint marks its Turn finished, whether it is
written under the settled Turn's id or the next prompt's, so it drops
the lock: an idle Session carries no commandLock key. The carry also
tolerates an in-memory checkpoint shaped without the key.

* fix(serve): let the takeover continue a member's command wait, and answer its committed commands by journal

The takeover reports a parked member Turn as await_command and qwencode
calls continue for it, but the continue route admitted only results_ready
and the agent wait, so every command wait was refused with
continuation_not_ready and resumeCommandWaitRuns was unreachable. The
gate now admits the command wait the same way, through the resume arm
that was already written for it.

The load and settle routes answer an interrupted Turn's owed calls before
reading its round. A member's board calls share the team tools' names,
so a committed command was answered "never ran" while its outbound stayed
planned or accepting for the relay to deliver later. Those routes now
carry the command funnel, and a call whose outbound committed is answered
by its journal — planned cancelled, accepting unknown, a reply as the
reply — through the helper the cancelled wait already used.

* fix(serve): a resumed command call consumes only the reply it commits

The lock arm already folds a timed-out call whose answer was journaled
without consuming it. The unlocked arms kept consuming: a call cancelled
or timed out as unknown, whose owner died before the fold, read the
unknown evidence, yet a reply the relay copied in afterwards was marked
consumed on resume. Both the journal arm and the resumed reply arm now
consume only when the reply is what they commit.

* fix(serve): keep a resumed command wait off the Workspace mount, and cancel it through the recovery cancel

Now that the continue route admits the command wait, its resume fell
into the Runtime-results branch, whose acquire claims the Workspace
mount for a Turn that never held it — a member shares its lead's
Workspace, so a busy mount failed the recovered Turn after its commands
had folded. A command wait whose Turn did not hold the mount takes the
agent wait's branch; one that held it got it back in its resume arm.

A cancelled takeover of a Turn parked on a command wait was classed as
owing no Runtime settlement, so the load settled it plainly and left the
checkpoint at await_command, refusing the next Turn's model start. Like
the agent wait, it now goes to the recovery cancel, which answers each
run by its journal and folds it.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-12 23:53:21 +00:00
haochen
008c0de746
docs(sdk): clarify empty core-tool allowlists across SDKs (#13949)
Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
2026-10-12 13:19:59 +00:00
jinye
f70344345d
feat(managed-agent): recover Hosted G3 parked turns (#13777)
* feat(managed-agent): recover Hosted G3 parked turns

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

* fix(serve): preserve G3 recovery across channel integration

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

* test(managed-agent): align recovery fixtures and acceptance evidence

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

* test(managed-agent): align hosted process fixtures with recovery storage v2

The fake hosted session store now advertises supportedStorageVersion 2 and
accepts the commit-v2 route like the Java store, and the packaged no-tool
stream pin follows the durable delta projection (session_update chunk,
managed_journal_event record, turn_complete). The e2e runner pin accepts
the Linux-gated durable-local-process value.

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

* fix(hosted): address recovery review feedback (#13777)

* fix(hosted): restore cleanup fence and update process gates (#13777)

* fix(serve): bound native approval recovery plans

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

* fix(serve): keep approval fallback consistent across recovery

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

* fix(managed-agent): preserve G3 recovery intent and cleanup liveness

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

* fix(test): defer G3 gate success output until teardown

Keep both G3 success records behind the final failure and signal checks, preserving their JSON payloads and successful output order. Extend the exit guard regression to cover both deferred records.

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

* test(managed-agent): align G3 cleanup gate fixtures

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

* fix(managed-agent): align Java envelope kinds with G3

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-12 13:05:52 +00:00
jinye
a8990042e6
feat(managed-agent): admit public foreground Shell with mandatory approval (#13674)
* feat(managed-agent): admit public foreground Shell with mandatory approval

* codex: address PR review feedback (#13674)

* codex: address PR review feedback (#13674)

* codex: address PR review feedback (#13674)

* fix(managed-agent): keep Shell action answers with the recorded owner

A Workspace operator may still answer other Actions. A Shell answer runs a command, so that arm does not apply. The rollout note says to upgrade the Harness before enabling the flag, because an older Harness ignores suppressChildAgents.

* fix(managed-agent): state that only the owner may answer Shell Actions

The public contract still said any Workspace operator could respond. Shell answers stay limited to the recorded owner; the route text now says so.

* fix(managed-agent): omit the message funnel when Shell suppresses child agents

Session messages share the child orchestrator's write chain, so a Shell lane that does not attach that orchestrator has no message funnel. A message-stop load keeps suppressChildAgents.

* fix(managed-agent): require Shell approval before lifecycle and message-stop loads

These two loads already send the Shell profile, but they skipped the check that a persisted Shell uses default or auto-edit approval.

* fix(managed-agent): isolate internal TeamLead admission

Preserve public Shell approval, child suppression and lifecycle boundaries while trusted internal TeamLead creation pins a reserved profile and private execution policy. Isolate public/internal creation replay and move the existing MySQL process rig to the trusted admission.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-11 16:06:34 +00:00
Shaojin Wen
263d47e29d
fix(managed-agent): unwedge Hosted stream close and bound task cancel attempts (#13877)
* fix(managed-agent): unwedge Hosted stream close and bound task cancel attempts

The H4f real-stack acceptance (#13746) found a task cancel that never
retried or parked. After a Hosted Harness restart behind a half-open
connection, the first call to the new Harness got the generation 409, and
adoptGeneration closed the stale client. HarnessEventStream.close() then
waited on the ReentrantLock that a Turn's reader held across a blocking
SSE read that never returns, so the cancel attempt hung inside the
adoption. Its claim kept renewing the lease every 20 s, the Session's
operation slot stayed held (close answered 409 session_operation_active),
and the claim budget never advanced.

- HarnessEventStream.close() no longer takes the reader's lock. Closing
  the response body is what ends the blocked read, and that read now
  reports the stream as closed rather than as the server ending it.
  getLastEventId() reads a volatile field instead of waiting on the
  reader.
- TaskCancelCoordinator runs each send on its own thread and waits for it
  at most five minutes, or four Harness request timeouts when that is
  longer. A send still running then is logged as abandoned and counts as
  an unproven outcome: the delivery reads the record and retries or
  parks, and its lease renewal stops. An abandoned send that lands later
  is judged by the record like any lost reply.
- The H4f design doc records the real-stack round and states the bound:
  608 s measured when attempts fail fast, and about 90 minutes computed
  at the default timeouts if every attempt hangs. It also lists the open
  close-stops-children item (#13875) and a Hosted stream heartbeat.

Refs #13746

* fix(managed-agent): evict an unverified attachment and log abandoned sends

Review follow-ups for the task cancel attempt deadline:

- The abandoned send is interrupted, and an interrupt can make the
  connector's approval-mode check throw after a cold attach already
  cached its Harness ref. doCreateOrLoad and doRecoverManagedRuntime now
  evict the ref they minted (and its recovery marker) when that check
  throws instead of answering, so no later verb takes an unverified
  attachment. A ref verified earlier stays cached, as before.
- An abandoned send logs how it ended, failure or return, so an operator
  can tell a late refusal from a late landing.
- sendWithinDeadline documents that its executor must stay unbounded.
- HarnessEventStreamPinningTest and HarnessEventStreamCloseTest share one
  fake Harness (HeldStreamHarness), and two new cases pin the post-read
  closed check: a close racing a buffered frame and a close racing the
  end of the body both report the stream closed.

* fix(managed-agent): flag every abandoned send and pin its logs

Second review round on the attempt deadline:

- An interrupted delivery abandons its send like a timed-out one, so the
  interrupt branch also marks the send abandoned before cancelling it,
  and the send logs how it ended on both paths.
- The abandoned-send failure WARN carries the throwable, so the cause
  chain (where the abandoning interrupt shows) reaches the log.
- TaskCancelCoordinatorTest captures the coordinator's log: the
  timed-out send logs its return once released, a late failure logs its
  cause, and an interrupted delivery logs its send's end too.
- QwenHostedHarnessConnectorTest pins the other side of the eviction: a
  ref verified when it was attached stays cached when a later
  approval-mode check throws, on the plain attach and the recovery load.

* docs(managed-agent): count the abandoned-send log tests in the H4f design
2026-10-11 14:33:06 +00:00
易良
504e4a7997
fix(managed-agent): reconcile approval delivery and concurrent session titles (#13682)
* fix(managed-agent): recover queued approval delivery after cache loss

Keep accepted Workspace approval responses deliverable with the default recovery configuration after attachment-cache loss. Leave approval retry claims untouched on Harness-disabled replicas.

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

* fix(managed-agent): reconcile durable session rename delivery

Fence late title writes by durable revision and replay the latest intent after ambiguous replies. Renew approval claims during slow attachment and settle journal-proven terminal answers without dispatching from disabled replicas.

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

* test(managed-agent): scope the held-delivery mock to its own action

ActionResponseCoordinator.recover() scans every deliverable
ACTION_RESPONSE operation across tenants, so the PENDING response left in
the shared test database by
disabledReplicaSettlesOnlyJournalProvenEndedResponses reached this mock
and bumped the resolveAction counter, making calls.get() 2 instead of 1
on both the MySQL 8.4 and the MariaDB lane.

Ignore resolveAction calls for other action ids, the same action-scoped
shape the shared harness mock already uses via its responses map.

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

* test(managed-agent): extend the pinned migration list to V53

WorkspaceMigrationMySqlIT.upgradesCurrentMainWithoutChangingAppliedMigrations
pins the versions applied on top of the V47 baseline. V53 was added by this
pull request without extending that list, so the MariaDB lane failed with
["48", ..., "53"] against ["48", ..., "52"].

The failure only surfaced after the earlier ManagedActionsTest failure was
fixed: the lane stops once the first surefire batch fails, and this class is
in the second batch, so it never executed on the previous head.

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

* fix(managed-agent): fence title delivery ownership and migration admission

Reject disabled or unsupported title requests before admission, claim inline
delivery before I/O, and fence failed-owner cleanup against lease takeover.
Include unfinished title deliveries in workspace migration idle checks.

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

* fix(managed-agent): renumber rename-delivery migration to V54

Upstream #13544 added V53__managed_workspace_roles.sql, so V53__managed_session_rename_delivery.sql
collides on the pull_request merge tree and fails the "Flyway migration version uniqueness" gate.
Renumber the rename-delivery migration to V54 and extend the WorkspaceMigrationMySqlIT expectation
to 48..54, which is the version set the merged tree applies.

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

* test(managed-agent): declare fenced titles in workspace fixtures

Keep the existing positive rename and creator replay fixtures compatible
with the explicit title protocol preflight.

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

* docs(managed-agent): record title migration ordering after role sync

Document V54 rename delivery following the main V53 Workspace roles and Session owner migration. The verified product and test tree reuses the concurrently pushed main-sync history.

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

* fix(managed-agent): renumber rename-delivery migration to V55

main merged V54__managed_child_lineage_relay.sql after this branch
numbered its rename-delivery migration V54, so the PR merge tree had two
migrations claiming version 54 and the Flyway uniqueness gate failed.
Renumber the PR-side file to V55 and extend the pinned upgrade list in
WorkspaceMigrationMySqlIT.

Local check: node scripts/check-flyway-migrations.js packages/sdk-java/managed-agent-server packages/sdk-java/runtime-broker packages/sdk-java/qwencode -> "55 migrations, all versions unique". Java compilation is not available on this box (no mvn, JDK 1.8); CI is the verifier for the IT.

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

* test(managed-agent): register a Clock bean in the artifact API fixture context

ManagedAgentService.configureRenameDelivery is an @Autowired setter that takes
ManagedAgentProperties and a Clock. The mini context built by
ManagedArtifactApiIntegrationTest.configureApi registered the properties bean
but no Clock, so context.refresh() failed with UnsatisfiedDependency and errored
all 26 artifact integration tests (ManagedArtifactApiIntegrationTest, plus
ManagedArtifactReadIntegrationTest which reuses the same fixture through
configureApi()). Production wiring already supplies the bean via
ExecutionConfiguration.managedAgentClock(), so the fixture only has to mirror it.

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

* fix(managed-agent): retire exhausted renames behind a durable fence

Preserve retained titles and block lifecycle until the remote retirement is acknowledged.

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

* revert: preserve confirmed managed rename recovery contract

Revert commit 3294358ee4.

The PR owner reconfirmed preserving uncertain-write recovery and permanent
Harness unavailability as an external blocker, without adding a retirement
or cancellation protocol. Restore the entire tracked tree to 9ae0c662,
retaining its artifact-fixture Clock repair.

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

* fix(managed-agent): renumber the rename-delivery migration V59 to V66

main's highest applied migration is now V64 (session message relay), so a
database already migrated by current main failed Flyway validation on this
PR's jar with "Detected resolved migration not applied to database: 59".
V65 is already claimed by two open PRs (#13642, #13654), making V66 the
lowest free version strictly above main's highest.

Mechanical rename plus the pinned version list in WorkspaceMigrationMySqlIT
and the bilingual design notes. No SQL, schema or behavior change.

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

* test(managed-agent): make the rename store-failure retry deterministic

renameStoreFailuresPreserveTheOriginalErrorAndAllowRetry raced the
SessionRenameCoordinator that this test class schedules at
dispatch.scan-delay=50ms. Once the inline attempt failed, the delivery row
became claimable, so the coordinator could re-drive it: the command row read
COMPLETED instead of FAILED, and a trailing PATCH that lost the claim answered
503 "already being delivered" instead of 200.

Gate the coordinator at its only precondition -- Harness availability, which
the inline createOrLoad/rename path never consults -- from the insert onwards,
park both claim branches while pinning the post-failure state, then hand the
row back to the recovery path and wait for it to settle. A retry that loses the
claim legitimately answers 503 in production, so the PATCH is only deterministic
once the delivery has completed and it replays; the await also proves the row
was re-queued at all.

The cleanup arm now asserts the state its abandon failure leaves behind
(delivery RUNNING under the dead inline owner, command still PENDING) instead
of only expiring the lease, and both UPDATEs assert they affected one row.

Measured on JDK 21 + Maven against H2, 20 repetitions of the method:
12 red / 20 before, 0 red / 20 after. Full class 35/35, checkstyle clean.

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

* fix(managed-agent): renumber the rename-delivery migration V66 to V67

main's V66 is now managed_session_agent_definition_digest (#13530), so the
merge result carried two V66 migrations and the open-PR Flyway guard failed
with "Version collision when merged into main: V66 =
V66__managed_session_agent_definition_digest.sql +
V66__managed_session_rename_delivery.sql".

V67 is the lowest version strictly above main's highest (V66) and is not
claimed by any open PR. Mechanical rename plus the pinned version list in
WorkspaceMigrationMySqlIT and the bilingual design notes. No SQL, schema or
behavior change.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-11 04:50:38 +00:00
易良
fa18c9fc83
feat(managed-agent): execute pinned AgentDefinition revisions (#13530)
* docs(managed-agent): design AgentDefinition execution (D8b and D8c)

Proposes how a Session pins a stored AgentDefinition revision (D8b) and how
the definition's fields take effect (D8c): permission_policy and tools through
the existing per-Session approval mode and tool profile pins (Java only), then
model and instructions through the Hosted Harness. Content that cannot take
effect refuses admission instead of being dropped. The D8a design's open
questions now link here.

Decisions for review are listed in section 7.

* docs(managed-agent): address D8b/D8c design review

- Default the definition tool-profile allowlist to files/1; files/2 becomes
  an operator opt-in after the #13166 rollout, since Java has no switch.
- Add W1 recovery's source validator as the fourth qwen-code check and keep
  the pinned revision and digest in the recovery source.
- Refuse approval_timeout_ms in v1 instead of accepting a value the
  connector would not deliver.

* feat(managed-agent): pin and compile stored AgentDefinition revisions (D8b+D8c-1) (#12867)

Session admission resolves a stored definition: an explicit agent_revision
pins that revision, an omitted one resolves the latest revision locked
under FOR UPDATE in the creation transaction, and a missing agent or
revision answers 404 agent_not_found. The pinned (agent_id, revision,
digest) lands on the new agent_definition_digest column (V53), and replay
returns the original Session without resolving again.

D8c-1 compiles permission_policy and tools into the approval mode and
tool profile the Session already pins: shell profiles never pair with
yolo, unbound Sessions stay model-only, and selectable profiles sit
behind the qwen.managed-agent.definition-tool-profiles allowlist
(default hosted-workspace-files/1). model, instructions, skills,
mcp_servers and environment_template_id refuse with 409
agent_definition_unsupported naming the field in error.field instead of
being dropped.

The four hard-coded qwen-code gates relax to qwen-code-or-pinned:
later-Turn admission, Workspace execution authorization and W1 recovery
sources, which now carry the pinned revision and digest. PublicSession
gains the optional agent_digest and the contract becomes 1.34. Two
design amendments ride along: FOR UPDATE implements the stated locked
read (H2 has no FOR SHARE) and the refusal field is named error.field,
matching the envelope shape.

* chore(web-shell): regenerate managed agent API types for error.field

* test(managed-agent): include V55 in migration upgrade assertions

The MariaDB integration run applies the new definition-digest migration correctly. Keep the strict ordered upgrade expectation in sync while preserving the applied-history checks.

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

* fix(managed-agent): correct three review findings on stored-definition execution

- insertChildSessionCommand now copies agent_definition_digest from the
  parent, so a child of a stored-definition Session is no longer born
  with a NULL digest (non-executable, and poisoning W1 recovery).
- hosted-harness-session: a deadline expiry keeps naming the deadline;
  only a generic error outcome is reclassified as model_unavailable.
- README and the definition-service javadoc no longer claim Sessions do
  not use stored definitions.

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

* fix(managed-agent): accept published agent instructions in W1 recovery

The W1 recovery reader's PUBLISHED allowlist covered only the three
managed-tool-result kinds, so reading a definition Session's digest-
addressed instructions resource threw resource_layout_unsupported and
failed the whole storage's capture/verify (R1-3). The reader now also
accepts managed-agent-instructions up to 64 KiB, mirroring the
allowance ManagedSessionStore.verifyStoredResource already carries,
and the mixed-storage capture test publishes non-empty instructions
through the production publisher and reads them back through the
recovery RPC.

Also fixes two test/doc findings from the same review round: the two
compiler-test @CsvSource text blocks were passed as value, so JUnit
executed one row per annotation (14 of 16 rows dead) — now textBlock =
(R1-50); and the OpenAPI route descriptions for createAgent and
createSession (plus their WebShell twins) still claimed Sessions do
not use definitions and public callers cannot select the profile,
which this PR's own admission changes falsify (R1-7, R1-6). WebShell
generated types regenerated.

* fix(managed-agent): round-2 review — migration version, stop-load pin, store guard, doc truths

R2-3 (fails-closed): the new migration took V58, below versions main
already shipped (V60/V62/V64), so with Flyway's default outOfOrder=false
it resolves-but-never-applies on any upgraded database and the
agent_definition_digest column every Session INSERT and read touches
silently does not exist. Renamed to V66, the next free version.

R2-1: messageStopAttachment is the third LoadHarnessSession site and the
only one that never carried the pinned agentDefinition, so a
definition-pinned child Session's message stop was refused with 409
hosted_agent_definition_conflict. The load now carries the pin, and
aMessageStopOfAPinnedSessionCarriesTheAgentDefinition pins it red/green.

R2-2: Session admission pinned a stored definition even when the Managed
Session Store the Harness create needs was disabled — a supported-looking
config (harness on, store off, files off) answered 202 for a Session that
could never run. validateWorkspaceFiles now refuses that shape at startup,
and ManagedAgentPropertiesTest witnesses it.

R1-28: two of the four falsified statements were still standing — the
README's revision-source sentence (a pinned Session records the revision
resolved at admission, not QWEN_MANAGED_AGENT_REVISION) and the
AgentDefinitionRequest javadoc's 'no field changes Session execution yet'.

Also from the same round: the 64 KiB agent-instructions bound is now one
named constant (MAX_AGENT_INSTRUCTIONS_BYTES) instead of four bare
literals across publisher, both verifiers and admission (R2-5); the
createAgent description's stale (v1.34) label is now v1.41 and names the
operator allowlists that gate model and tool profile (R2-13), the
webShellCreateSession description no longer promises a fixed server-owned
profile for definition-pinned Sessions (R2-6), and the workspaceTurns
predicate wording now matches the implementation (active and undeleted,
both branches on the frozen execution profile) (R2-11); the generated
managed-agent-api.ts was regenerated.

* fix(managed-agent): pin V66 in the workspace migration IT list

842d8a62e8 renumbered the agent definition digest migration from V58 to
V66, but WorkspaceMigrationMySqlIT still asserted the old pinned version
list. upgradesCurrentMainWithoutChangingAppliedMigrations therefore
failed with the applied set [48..57, 60, 62, 64, 66] against the expected
[48..57, 58, 60, 62, 64].

Align the list with the migrations actually present in the tree, keeping
installed_rank (ascending version) order.

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

* docs(managed-agent): name V66 as the definition-digest migration in the design

Review round 2 renamed the migration V58→V66 (main had since shipped
V60/V62/V64 and Flyway never applies a version below the applied
maximum), but §10 of both design docs still said 'the database
migration is V58'. Both now name V66 with the reason, matching the
shipped V66__managed_session_agent_definition_digest.sql. The PR
description's V55 references are updated the same way, keeping the
two historical repair notes as records with the superseded numbering
marked inline.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-11 04:00:50 +00:00
Shaojin Wen
a7e26adde7
feat(managed-agent): H4d-b session message runtime (#13822)
* feat(managed-agent): H4d-b session message runtime

Produce the records the H4d-a contract pinned. The Hosted Shell lanes
declare a managed send_message: a parent messages the child tasks it
launched by task id, a child messages its parent. A running child gets a
message, a completed child gets a continuation that carries its chain's
history, and any other ending gets a named refusal. The routing decision
shares the child funnel's writes chain with settlement, so a message
either opens before the run settles and holds that settlement, or finds
the run ended.

A new message relay in managed-agent-server fixes each message's target
at the handover, commits the receipt into the target together with the
input and wake that carry it, and advances the sender as the target
accepts and consumes it, with its own V61 ledger for leases, backoff and
the orphaned/unknown classifications. The target reads a message as a
wake turn and consumes the receipt when that turn completes.

The child result relay now settles a child only once no message on its
edge owes a handover and its journal holds no pending input, and takes
the result from the child's newest settled turn, read from the child's
own journal when a message's wake turn ran last. The parent refuses a
result commit with child_messages_pending while a message to the run is
undelivered. A continuation's first input is composed from the parent's
committed instructions and results, bounded below the Hosted prompt
limit.

session_message joins the enabled domains, the continuation gate opens,
the Hosted recovery allowlist admits the domain, and send_message joins
the input-preview set in both languages. No public contract change.

Refs #12380, #12827, #13744 (slice H4d-b)

* fix(managed-agent): close the H4d-b round-1 audit findings

Five independent audits of the session message runtime found these.
Each one is closed here.

A give-up now ends the entry on its sender before it classifies the
ledger row unknown: cancelled if the entry was never handed over,
accepted if its receipt exists, unknown otherwise. Undelivered now means
planned or accepting in both languages, so a message the relay gave up
on no longer holds the child's settlement forever.

The child result relay reads the child's journal first and its edge
after. Only delivered messages waiting for their turn hold the
settlement, and only for 30 minutes without activity. The result is the
newest settled API or message turn. The settlement names the
messageCount the relay read, and the parent refuses it when a newer
message exists, which closes the window between the reads and the
commit. A committed result is accepted without being recomputed.

The message relay decides from the entry as committed now. It pages
waiting entries from its ledger index, so an accepted entry whose
advance was lost is not stranded. It cancels messages to a child being
stopped. A target store fault is a 503 retry, never a rejection. A
closed sender's entry is classified delivered when its receipt exists.
Waiting entries are reconciled through a new consume operation, which
also reloads a target that a replaced Harness dropped.

Senders are bounded to 8 messages in flight and 64 in all per child run
and direction. A closing parent sends no messages. Refusals from the
store become tool errors. A receipt for an unknown run is refused before
anything publishes, and the route bounds the content it accepts.

Continuation history is now XML-escaped. V61 gains a per-Session index.
The migration IT lists V61. The foreground-agent sibling check no longer
exempts send_message, to match #13769's recovery.

Refs #12380, #12827, #13744 (slice H4d-b)

* fix(managed-agent): gate message receipts like other new-work relays

- Route receive and consume through the Workspace admission the
  automation relay runs, and re-attach a Session a prior control-plane
  process attached through the takeover load for every message verb.
- Refuse an accepted step whose input id is not the one the message id
  derives, pinning the Java relay's derivation to the Hosted one.
- Keep the attempt count across a forward ledger step, matching the
  child result relay.
- Record that a failed message wake turn fails the child on purpose.

* fix(managed-agent): leave the message ledger to instances with a Harness

A Session Store replica runs without a Harness, and its message relay
claimed entries it could only fail, counting attempts against them and
pushing their real worker behind the backoff. Skip the scan there, as
the turn coordinator does.

* fix(managed-agent): close the H4d-b round-2 audit findings

- Cross-check the edge against the journal read: a parent message the
  child received between the two reads holds the settlement instead of
  being closed away unread.
- Count any journal event as activity for the message-turn wait, so a
  long running message turn is never cut off at 30 minutes.
- Stop counting a closed child's orphaned outbox as owed, and hold the
  close of an active child until its own messages left.
- Let a committed result or failure stand when a newer turn would
  re-settle the run the other way.
- Read the child journal only when the child holds session messages.
- Decide a give-up from the entry as committed after the failed step.
- Count only messages not yet handed over against the in-flight cap.
- Fail a send_message call on the store's own faults instead of
  answering a refusal for a message that may still commit.
- Join journal text runs a thought separates with a newline, as the API
  turn result does.

* fix(managed-agent): bound the hold on a child's own messages by the give-up

A child that can take no further revision (a declined recovery) never
lets the message relay's give-up land its sender step, so its outbox
entries stayed planned and held the child's settlement and close
forever. An entry whose ledger row is classified or past the attempt
bound no longer holds either.

* fix(managed-agent): fail a child whose message turn never comes

Past the 30-minute wait without a journal event, a child still holding
an unread message settled from its earlier turn's result, reporting the
message as acted on; a turn queued behind a busy shared mount reaches
that bound without any fault. Settle it as child_failed instead.

* fix(managed-agent): hold a child's message for its task and refuse it once a close began

A run attaches as soon as its Runtime binding exists, before the
coordinator submits the child's task, so a parent's message could be
received first: its wake turn ran without the task and the task then
met the busy Session. The relay now hands a message to a child only once
the Harness admitted the child's task (its first Turn carries the
admission epoch), and cancels it when that task ended without an
admission, so the child's settlement is never held by it.

The message route now refuses every verb with hosted_session_closing
once the Session's close began, like the channel and automation routes,
and the close waits for an operation already past that check before it
cancels the pending inputs, so no receipt lands after that pass.

* fix(managed-agent): stop a stopped run's message turns and answer interrupted send_message calls

Main's H4f stop arm decides from the child's API Turn, but a message's
wake turn never becomes one, so a stopped run could settle cancelled
while a message turn kept running in its child, and a stop met a child
whose task had completed but whose messages were still owed as a natural
completion. For a child holding session messages the stop arm now reads
the child's journal: while a message input waits or runs it sends the
child's message route a new `stop`, which keeps the wake pump from
starting any message input, aborts a message turn in flight and, once no
turn runs, settles the waiting ones cancelled with stop_requested. The
natural outcome is the newest settled turn, API or message; a stop the
child cannot take yet spends no attempt, and a message input no turn took
within the 30-minute bound stops holding the stop. A message handed over
before the stop but not yet received is rejected instead of waking the
child.

A send_message writes only to the journal before its answer, like a team
call. Every interrupted-turn route from main's #13829, and the parked
round's gap fill that runs before them, now answers it by what it
committed: an outbox entry as queued, a continuation as continued, and
one that committed nothing as never run where the turn resumes, so the
model never sends a committed message twice.

The relay's migration moves from V61 to V64: main has V62, and other
open pull requests claim V61 and V63.

* fix(managed-agent): carry a message stop into the load that attaches its child

A stopped run's message stop went through the takeover load every message
verb uses when the control plane holds no attachment for the child. That
load drives recovery and kicks the child's wake pump before the stop
itself arrives, so a queued message turn could start in that window, and
the later stop could only abort work already begun.

The stop now has its own attachment path: a child this process does not
hold is loaded passively with `stopMessages`, which marks its message
inputs stopped before the load kicks the pump, under the cancellation
authorization a stop needs rather than the grant new work needs. A held
child takes the stop directly.

* fix(managed-agent): authorize a message stop by its committed request and keep stopped children unloaded

The message stop's own load took the cancellation authorization, whose
query needs a CANCELLING Turn. Message work never leaves one, and the
stop arm skips the Turn cancel once the task ended, so a child the
control plane no longer held could not be stopped at all. The load is
now authorized like a persisted cancellation, by the committed stop the
child result relay read: only the Session's own binding is checked, so
neither the missing Turn nor a grant changed since refuses it.

Other loads could still attach a stopped run's child without the stop.
The stop now goes out before the arm's Turn cancel, whose own load
carries none, and the message relay no longer attaches such a child for
a consume reconciliation or for the child's own outbox steps; those wait
for its close. A message turn that crashed in an earlier process is no
longer left owed: the pump skips a stopped Session's message inputs, so
the stop runs its aftermath. The Hosted load now marks a resident
Session stopped only after the load passes its identity and admission
checks, so a refused load changes nothing.

The 30-minute message-turn bound counted a resident Session's lease
renewals as activity, so it never ended a blocked child's wait; renewals
no longer count.

* fix(managed-agent): hold a stopped child's message steps only while its stop still settles message work

The message relay held every outbox step of a stopped run's child until
the child closed. When the stop met a natural end (the task completed or
failed on its own, no message work owed), the child result relay settles
through its ordinary walk, which waits for the child's own outbox to be
handed over before it settles or closes, so the two relays waited for
each other with no attempt spent and no bound to break it. The hold now
lasts only while the child still owes message work the stop is settling;
after that a load starts nothing, and the steps go on.

A resident Session's stop flag was set before the passive load checked
its tool profile, so a load refused for a profile conflict still stopped
the Session's queued messages. Each load branch now sets it only after
its own refusals.

* fix(managed-agent): bind the claimant in the relay lease CAS and refuse a closing child's message to its parent

Both relay ledgers guarded the lease takeover with the owner their read
saw rather than the claimant. Two workers that both read an expired
lease could therefore both win it: the first renewed it as its own, and
the second still matched the stale owner. The guard now binds the
claimant, so the second meets the first one's fresh lease.

A child's send_message to its parent took no closing flag, so a call in
flight when the Session's close began still opened an outbox entry the
closing sender could only orphan, while the model was told it was
queued. It now refuses like the parent-to-child arm, after a committed
message's replay.

* test(managed-agent): expect send_message among a Shell-lane Session's tools in the Hosted IT driver

The Hosted workspace tool-turn integration driver pins the exact tool set
a Shell-lane root Session advertises. Since this change that set carries
the parent form of send_message beside the Agent tool, so the MySQL
process gate failed on its first model call.
2026-10-10 16:31:38 +00:00
Shaojin Wen
1e1c4a4aee
feat(managed-agent): worktree admission for managed child agents (#13753 I2) (#13841)
Some checks are pending
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
SDK Java Flyway Open PRs / Re-check open PRs against main (push) Waiting to run
SDK Java / Flyway migration version uniqueness (push) Waiting to run
SDK Java / Runtime Broker and Managed Agent MariaDB / Java 21 (push) Waiting to run
Verify Landlock helper / Native Linux CLI boundary (push) Waiting to run
Verify Landlock helper / Native Windows settings and streams (push) Waiting to run
Verify Landlock helper / arm64 (push) Waiting to run
Verify Landlock helper / x64 (push) Waiting to run
SDK Java Contract Open PRs / Re-check open PRs against main (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Hosted process fault gates / MySQL 8.4 / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(managed-agent): worktree admission for managed child agents (#13753 I2)

I2 of the isolation slice (#13753): the hosted Agent tool can run a
child agent in its own child Workspace, the Git worktree I1 (#13781)
built, and the child's work merges back before the parent sees its
result.

The model opts in per launch with `isolation: "worktree"`, the legacy
Agent tool's name and value; without it the child is `shared` as
before. The launch records `workspaceMode: "worktree"`, and admission
accepts it only on a host that serves child Workspaces: the control
plane sends `childWorkspaces: true` on every hosted create and load,
`qwen serve` keeps it on the resident Session without persisting it,
and only then does the Agent declaration show `isolation`. Elsewhere a
worktree launch is refused as `workspace_mode` before anything commits.
The team variant of the Agent tool (H4e-b1) gains `isolation` the same
way, so a member may run isolated.
Startup now requires the durable local-process Broker for child
Workspaces, the only one that closes Workspace Sessions.

The relay requests the child Workspace before it creates the child and
waits on its heartbeat, at no attempt cost, while the scan prepares it;
a `ready` row creates the child bound to it. When the child's Turn
completes, the relay asks for the merge, admits the child's close, and
commits the result only once the row records an outcome. The terminal
receipt then carries `workspace` (outcome, code, conflict paths bounded
to 16 KiB, the result pin when the work did not land), built from row
fields a later discard leaves alone so a replayed commit is byte-equal.
The foreground answer, the recovered wait, the recovery gap fill and
the background notification all show that outcome to the parent's
model, with child-chosen paths quoted and stripped.

A failed or cancelled child's Workspace is discarded; a completed
child's work is never discarded unpinned (an over-bound answer or a
give-up after a completed Turn asks for the merge, and a merge already
asked survives later failures). The close cascade asks for the discard
and never waits for it, keeping the run's scope open while that request
is owed.

I1's finish rules change accordingly: a finish request is recorded
while the bound child Session runs and runs once it is closed; a
discard may replace a merge that has not started, and the step leaving
`ready` compares the request it read; an ended merge keeps its outcome
through a discard that cannot finish.

Design: docs/design/2026-10-10-managed-child-worktree-admission.md
(+ zh-CN); the I1 design notes the amended decision 8.

* docs(managed-agent): qualify the I2 completed-work guarantee by the parent's close

The relay never discards a completed child's work unpinned, but a parent
that closes before the asked merge starts still discards it (decision 9).
Say so in decision 8, in both languages, and mend the sentence the
earlier edit left broken.
2026-10-10 15:39:42 +00:00
Shaojin Wen
6386bb24d0
feat(managed-agent): restart-recoverable foreground child wait (fixes #13708) (#13769)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
SDK Java Contract Open PRs / Re-check open PRs against main (push) Waiting to run
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java Flyway Open PRs / Re-check open PRs against main (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / Runtime Broker and Managed Agent MariaDB / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Flyway migration version uniqueness (push) Waiting to run
SDK Java / Hosted process fault gates / MySQL 8.4 / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(managed-agent): restart-recoverable foreground child wait (fixes #13708)

A foreground child-agent call skipped the Runtime reservation and, with
it, the only durable checkpoint of its wait — agent calls write no
tool.intent. A Hosted process dying into the wait left the checkpoint at
the pre-agent model phase, so recovery could only decline the parked
Turn (model_start), every later Turn wedged at
hosted_turn_recovery_required, and close/delete could never pay the
cascade debt. Introduce the durable agent wait as a first-class
continuation: an await_agent checkpoint phase with an agentWait group
committed by the foreground admission under the same transaction
discipline as the Runtime batch, replay-safe boundaries, and a
resolve that marks each fold consumed and advances to
model_output_committed carrying the all-consumed group so a second
crash classifies the wait instead of reading settled work as a false
terminal.

Recovery classifies the wait (active and passive identically) and never
commits; the wire protocol gains the phase with every outcome always
known, so the coordinator's existing continue/cancel admissions drive
it. The continue and cancel routes re-enter the wait through their own
resume arms: the admitted runs fold through the journaled exactly-once
dedupe, and the parked round's never-reached calls are answered —
ledger-honest, so an admitted orphan (admitted in the gap between an
earlier fold and its own wait commit) drives to its own terminal and
folds truthfully instead of minting a fabricated never-admitted answer.
The cancelled family's discipline (settleParkedTurnCancelled) is
mirrored for the agent wait, and the journaled predicate and both
abandoned texts are shared by every arm.

Verification: core checkpoint 28/28 and harness factory 41/41; CLI
recovery 47/47, child-agent turn 25/25, tool turn 206/206, harness
session 305/305, dedicated wedge suite 8/8 (points 1/2 re-entry and
exactly-once, cancellation takeover, second-death orphan fill); Java
HostedHarnessClientTest 57/57; managed-agent-server verify green;
whole-repo typecheck, ESLint and Prettier clean. Mutation witnesses W1
(admission commit), W2 (journaled dedupe) and W3 (all-consumed replace
guard) each proven RED then restored byte-identical. Design:
docs/design/2026-10-09-foreground-child-wait-continuation.md (EN + zh).

* fix(managed-agent): pin admitted-orphan fold discipline and witnesses

Adversarial-audit round on the #13708 continuation: the cancel route's
gap fill ran without the inline-fit predicate, and an oversized admitted
orphan answer would have errored the recovered Turn instead of folding.
The fill now folds such answers with the same fit+marker discipline the
live arm uses, both route call sites pass their own fit lambdas, and the
wedge suite gains witnesses for the three previously unwitnessed shapes:
mid-flight admitted orphan folding its late settlement truthfully, an
aborted signal abandoning the orphan honestly while its ledger stands,
and an oversized admitted-orphan answer folding with the truncation
marker instead of erroring.

* fix(managed-agent): exact inline-fit predicate for the gap-fill fold

The gap fill's fit test drifted from the record it actually commits: the
fold wrote a model-less record while a per-caller lambda measured other
shapes, so a child answer near the 64 KiB inline bound could pass the
predicate and land oversized — or miss and fold a boundary answer into an
avoidable error. The fill now records with the resumed model string and
measures one exact template whose field widths match the committed
record byte for byte (uuid, assistant parent, ISO timestamp, model); the
per-callsite lambdas are gone and the truncation witness still lands the
marker. Both routes consolidate on the same predicate.

* fix(managed-agent): child-wait recovery — takeover adoption, honest fills, funnel arms

Review round-1 Criticals on the #13708 continuation, all adjudicated
valid and fixed with discriminating pins:

- core (R1-21): resolveAwaitAgent adopts the takeover activation when
  the wait advances past the last fold, so a replacement owner's next
  tool batch is not refused as work of a prior activation; partial
  folds keep the parking activation.
- serve (R1-1): the gap fill keys the parked round on the wait's own
  modelMessageId set, so a later ordinary-tool round can never be
  minted a fabricated agent answer.
- serve (R1-23): each fold is gated on the journaled set while the
  replay-safe marks still run, and the filled count reports only real
  writes, so a replayed fill is silent.
- serve (R1-24): a background-completion orphan folds the live arm's
  started receipt instead of being polled forever.
- serve (R1-3): the cancellation separation refuses to short-circuit
  an agent wait, and the interrupted-turn funnel settles it exactly
  like the takeover cancel; settleCancelledAgentWaitRuns and
  fillParkedRoundAgentGaps relocate into hosted-runtime-recovery for
  the funnel caller, with parkNeedsNoRuntimeSettlement exported for
  the wedge suite.

Witnesses: W1 park gate flipped RED, W2 funnel arm cut RED, W3
background arm unreachable RED (poll-timeout), each restored
byte-identical. Wedge suite 14/14, recovery 64/64, child-agent 26/26,
route suite 338/338, monitor-wake 33/33, factory 43/43; tsc and
scoped eslint/prettier clean.

* fix(managed-agent): child-wait hardening — boundary validation, compat encode, recovery wording

Review round-1 Suggestions (code half), all adjudicated valid:

- core (R1-6): both write-side constructors refuse the shapes their own
  assertPhaseShape rejects — an empty agent-wait group and a
  model_output_committed forwarding an unconsumed carried group.
- core (R1-29): the encode omits agentWait whenever no group rides the
  checkpoint — an absent key parses null on both reader generations, so
  a pre-agentWait daemon can still open every non-agent checkpoint.
- core (R1-30): commitAwaitAgent refuses duplicate childRunId /
  functionCallId and already-consumed runs at the boundary instead of
  bricking the session at the next checkpoint read.
- core (R1-53): commitAwaitAgent's turn parameter is required — the
  absent branch silently skipped both the binding guard and the
  activation adoption.
- core (R1-14): the consumed-run comment now names the group's real
  lifetime (replaced by a later wait or the Turn boundary, never the
  Turn boundary alone).
- cli (R1-25/R1-52): one authority for the exactly-once predicate —
  journaledToolResultIds takes an optional pre-projected journal, the
  gap fill passes its own projection instead of walking the journal
  twice, and answerAbandonedTurnCalls drops its hand-rolled copy.
- cli (R1-49): the never-admitted answer is per-family wording — the
  continue route and the interrupted-turn funnel name the interruption
  (nothing was cancelled); the cancel route keeps its own text. fill
  now takes gapText explicitly.
- cli (R1-51): the cancel settlement decides its two shapes off the
  phase-shape invariants outright (agentWait null or not) instead of
  dead ternaries and implied conjuncts.
- cli (R1-7): the gap fill's fit predicate is the exact template only —
  the messageFitsInline dead switch is gone, and the oversized case now
  drives the production 64 KiB bound itself (65513-byte child result,
  under the 65536 child limit but past the record bound).

Pins: checkpoint suite 32/32 (R1-6 guards, R1-29 key, R1-13
phase-shape + double-consume), factory 46/46 (R1-13 empty batch,
R1-30 duplicates/consumed, restated-swap keeps the set-conflict arm),
wedge 14/14 (fill callers re-keyed to the recovery wording), recovery
64/64 (the undefined-agentWait mock shape stays judged by the carried
null). tsc clean on both packages.

* fix(managed-agent): child-wait test-depth — route witnesses, orphan arms, poll evidence

Review round-1 Suggestions (test-half), all adjudicated valid and pinned:

- routes (R1-8): parkAgentWaitTurn mints the wedge through the
  production admission itself (execute → acceptChildAgent →
  commitAwaitAgent), with the abandoned fold's write sabotaged so the
  owner dies at the wait; the continue case proves the gate admits the
  parked wait (witness W5: reverting it answers 409), the cancel case
  proves the cancelled takeover folds the abandoned answer, advances
  the checkpoint past the wait, and closes the Turn exactly once.
- fill (W4, found while keying the fixture to the production child-run
  id): the fill must never poll the wait's own calls — an admitted but
  unsettled wait run hangs the fill forever; waitedCalls skips them.
  W4 deletes the skip and the interrupted-turn pin times out RED.
- wedge (R1-10): a failed admitted orphan folds its terminal cause
  ('Child agent run failed'), never the never-admitted answer.
- wedge (R1-11): consumption is caller-owned and asserted — the resume
  arm's queue collects the settled run, the abandoned arm leaves it
  untouched, the fill's consume callback and the relay's delivery
  state ('accepted') are pinned; the dead void-collection is gone.
- wedge (R1-36): the mid-flight poll's wait is asserted as a ledger
  read count greater than one, never a wall-clock coincidence.
- wedge (R1-8 folded gaps): the charter names what the suite actually
  covers, CHILD_RUN_ID derives from PROMPT_ID by hostedChildRunIdFor
  instead of the mock broker's runtimeSessionId, the fill callers pass
  the production gapText + children, and the last case is renamed to
  what it asserts.
- recovery tests (R1-32): the await_agent report's runtimeSessionId is
  asserted as hostedRuntimeSessionId(promptId) on both load shapes.

Wedge suite 15/15, route suite 340/340, recovery suite 64/64, tsc
clean; merge-conflict resolution verified marker-by-marker per the
house rule after the H4d-a baseline merge.

* fix(managed-agent): child-wait wire + contract — Java predicate pin, design-doc truth pass

Review round-1 Suggestions (Java/docs close-out):

- java (R1-15): agentWaitWithUnknownOutcomeIsNotReady — an await_agent
  recovery carrying any unknown outcome is neither continuation- nor
  cancellation-ready; witness W6 drops the all-known conjunct and the
  pin REDs (restored byte-identical, suite 63/63).
- docs (R1-16): the broken nested list item in the EN design doc's
  rationale bullet re-joined into one paragraph.
- docs (R1-31 ×5, both languages, synchronized): §1 names the one
  spreading constructor and why it is safe plus the absent-when-null
  encode; §2 records turn-required and the boundary validation; §4
  records runtimeSessionId=hostedRuntimeSessionId(promptId) and its
  inertness; §5 defines the gap fill (inputs, three answer shapes,
  exactly-once discipline, per-family wording); risks now point the
  wire-drift mitigation at the predicate-level tests; the validation
  plan attributes coverage to the layers that actually hold it
  (classification-only recovery, turn arm, routes, dedicated suite)
  and marks close/delete rig-facing with acceptance criterion 3 to
  match.
- tests (R1-58): the carried-group case documents it pins the
  constructor's contract rather than a production transition.

* fix(managed-agent): child-wait round-2 — carried adoption, cancel-sweep abort, lazy mount

Real-stack re-review on 91747afb1f (review 5477409190), all three
findings adjudicated valid and fixed with discriminating pins:

- core (R2-1 P1): the carried all-consumed shape now adopts on replay —
  resolveAwaitAgent on a model_output_committed checkpoint whose group
  is fully consumed commits the parked identity re-stamped with the
  fresh activation (same checkpoint continuity as every other
  constructor's resume), instead of leaving the dead owner's identity
  for the model's next turn-bound batch to collide with. The continue
  route restates the last resolve for that exact shape.
- serve (R2-2 P2): the cancel settlement pre-aborts its gap fill — an
  admitted foreground orphan meets the live arm's own abandoned fold at
  once instead of holding the cancelled terminal (and the HTTP reply)
  hostage to the child's terminal; the child keeps its ledger.
- serve (R2-3 P2): the agent-wait resume no longer acquires the
  Workspace mount through resumeCommittedResults, which has nothing
  Runtime to consume on these phases — the pending file-history
  obligation drops like the interrupted settle, and any later Runtime
  tool acquires on demand through prepareRequests, so a following
  foreground agent call is no longer refused by the parent's own hold.

Witnesses: W7 cuts the factory adoption arm (R2-1 pin RED by prior
activation throw), W8 restores the un-aborted cancel fill (the R2-2
route case times out RED at 8 s), W9 re-unifies the unconditional
acquire (the acquire-count assertion REDs), W10 cuts the route
adoption arm (the R2-1 route case REDs with the exact P1 error text);
each restored byte-identical. Factory 47/47, wedge 15/15, route 342/342
incl. the production-admission wedge, orphan-cancel, and carried
continue route cases; checkpoint 32/32; tsc clean.

* fix(managed-agent): carried adoption surfaces its own read faults (R2-9)

Review 5477644662's lone follow-up, adjudicated valid: the carried
all-consumed adoption's `.catch(() => undefined)` erased a store-read
or authorization fault into `null`, and the continue route's unchecked
return then started the model round against the un-adopted identity —
the exact prior-activation wedge, observed through the real route.

- core: the resolve's read surfaces again; `null` now means only
  "this resolve is inapplicable here."
- route: the adoption is wrapped as a retriable
  HostedToolRecoveryRequiredError before the model round starts, and an
  inapplicable `null` answer throws instead of silently continuing.

Pins: factory case `surfaces the carried-adoption read fault instead
of answering null (R2-9)` — one hiccup on the adoption's own read is
rejected, the retry adopts, and the turn-bound batch resolves
(48/48); route case `a faulting adoption read blocks the continue
before the model round starts (R2-9)` — after the gate, every later
read faults and the continue blocks with the model stub never called
(343/343). Witness W11 re-swallows the fault and the factory pin
REDs on `rejects.toThrow`, restored byte-identical. tsc clean.

* fix(managed-agent): child-wait robustness — funnel fill deadline, shared fit helper, fail-closed read

Approved round's three non-blocking notes (review 5477939950), all
adjudicated valid and fixed with discriminating pins:

- serve (R3-1): the interrupted-turn funnel's gap fill now pre-aborts,
  exactly like the takeover cancellation — an admitted orphan folds the
  abandoned answer at once instead of a deadline-less poll that would
  outlive the settlement and hang the attach/redrive path it runs on.
- serve (R3-2): fitChildResultInline owns the inline-fit predicate, the
  halving loop, the truncation marker and the error text for both the
  live wait arm and the recovery gap fill; the two copies that used to
  drift independently collapse into one helper.
- core (R3-3): the read path is fail-closed on the read-path
  cardinality an ordinary producer never mints — parseAgentWait refuses
  an empty group at any phase, and assertPhaseShape refuses an
  all-consumed group read as still awaited, so a corrupt log lands in
  the typed blocked verdict instead of an `await_agent` recovery report
  with zero executions.

Witnesses: W12 removes the funnel's pre-abort — the R3-1 pin times out
RED at 10 s; W13 ships oversized folds whole — the truncation-marker
pin REDs; W14 re-accepts an empty group on the read path — the
cardinality pins RED on both the new R3-3 case and the legacy
empty-runs assertion. Each restored byte-identical. Core 80/80, wedge
16/16, child-agent 26/26, recovery 64/64; merge-conflict integrity
verified marker-by-marker after the rebase.
2026-10-10 07:09:17 +00:00
Shaojin Wen
c3968ace2f
feat(managed-agent): H6b/H6c automation runtime for persistent definitions (#13598)
* feat(managed-agent): H6b/H6c automation runtime for persistent definitions

The runtime part of slice H6 of #12827 (stage H of #12380), after the H6a
record contract (#13536): definitions, the single-claim scanner, the
occurrence ledger, overlap and catch-up, and persistent execution.

- Definitions are created, revised, read and retired under the
  Workspace-bound Session their creator owns. The control plane relays
  each mutation to the Session's Hosted Harness, whose authority publishes
  the prompt as a Session resource and commits the definition revision;
  the control plane replays by Idempotency-Key and answers 202 with the
  committed resource. The two public automation routes move from planned
  to partial and gain the mutation routes and the detail read.
- The scanner derives each armed definition's due slots on the host tz
  database (a shared slots fixture pins the cron semantics, DST gap and
  fold rules in TypeScript and Java), claims the definition under a lease
  with a fencing token, records every occurrence decision (fired, skipped,
  missed, or unknown after bounded retries) in the V53 ledger, and fires
  one idempotent Harness operation whose claim row was durable first; a
  manual run is the same operation keyed by its Idempotency-Key. A
  scanner that lost its lease can move neither the watermark nor the
  ledger; a manual run holds the same lease.
- Firing commits the run's claim revision and then, in one journal
  transaction, its dispatch revision with the run's input and wake; the
  embedded wake scheduler runs the input as a text turn; the journal's turn
  settle settles the run on the settle path and again on open, and the close
  path ends a never-run input as cancelled with a proven execution.
- Run identity is derived from the definition and the occurrence in both
  languages; the authority and the Java store refuse an underived id or a
  run that does not bind to its live definition at the current revision,
  so two claims of one occurrence meet one committed run.
- Both domains are enabled for submission for the persistent target mode
  only (MANAGED_SESSION_ENABLED_SCHEDULE_SESSION_MODES); per_run and the
  delivery policy stay refused until H4b and H5b/H5c land, and the scanner
  and mutation routes stay behind qwen.managed-agent.automation.enabled.
- Closes the H6a verification follow-ups: promptRef is Session-scoped and
  closed by both stores, the shared fixture gaps are pinned, and the Java
  text validator checks its clauses in the shared order.

Design: docs/design/2026-10-07-managed-automation-runtime.md (bilingual);
the H6 design's status and slice table are updated in both languages.

* fix(managed-agent): close the three runtime findings on H6b/H6c

R2-1 (P1): a mutation relay carried a fresh operationId per attempt and
the funnel deduped on content alone, so a request whose answer was lost
between the Harness commit and the control-plane command row could be
retried into committing its content over a newer revision (create rev 1
→ update rev 2 → retry commits rev 3, replayed=false, in both
languages). The relay now derives the operationId from (tenant,
Idempotency-Key), the funnel defines/retires answer the operation's
originally committed revision from the journal (rebuilt across reopens)
before touching the chain, and the control-plane mirror never regresses
past the revision it tracks while the remembered result is the
operation's own.

R2-2 (P1): runAutomationOperation skipped the Workspace admission every
other new-work verb runs, so a cached attachment kept firing and
mutating after the creator's grant was revoked. It now runs
requireReadyForNewWork before every relay.

R2-3 (P2): a prompt within the raw 64 KiB bound fails its fire when the
wrapped, JSON-escaped input envelope exceeds the 65,536-byte inline
resource bound, stranding an admitted/intent run. Admission now computes
the exact worst-case wrapped input of the merged definition (largest
route-admitted identities, real escaping) and refuses the definition.

One residual is deliberately deferred: a mutation relay that matched
the committed content appends no revision, so it leaves no operation
fact; its lost-answer race with a later commit needs a journal-level
no-event operation record and is out of this fix's scope.

Verified: packages/core managed-runtime 2748/2748; cli funnel 13/13,
hosted-harness-session 238/238, hosted-monitor-wake 18/18; managed-agent-server
1304 tests, 0 failures (109 classes); npm run typecheck/lint, Prettier,
checkstyle and spotbugs green.

* fix(managed-agent): persist no-op mutation honors and bind replay to target and content

R3-1 (P1): a mutation honored without a revision (content identical to
the committed definition) left no operation fact, so its lost-answer
retry against a moved chain re-applied the old content as a new
revision. The honor is now committed as an `operation.replayed` journal
event — the 18th member of the closed kind union, synced across
TypeScript, Java and the store-v1 and event-envelope-v1 contracts —
naming the honored revision's recordRef under the same derived operation
identity, so the retry answers the honored result across restarts.

R3-2 (P2): the operation replay answered by operationId alone, so a
retire re-driven under one id against another target returned the first
definition's cancel (and the control plane even retired the second
one's mirror), and a define under one id with another body got the
original result where a 409 was owed. Every replay now verifies the
record's target, and for defines the merged requested content digest;
mismatches throw AutomationOperationConflictError -> 409
automation_operation_conflict.

Verified: cli funnel 15/15 (two new regressions incl. reopen survival
and both conflict classes), full hosted-harness-session 238/238, core
managed-runtime + contract replays green (three timing flakes re-passed
cold), Java managed-agent-server 1306 tests 0 failures (AutomationScannerTest
23/23; both new service scenarios pinned), typecheck/eslint/checkstyle/
spotbugs green.

* fix(managed-agent): close the review wave, the real-stack findings, and their regression gaps

The second-stage review (33 threads) and the real-stack verification
(F1–F5) land in one round, with regression tests in both languages for
every behavioral change:

Real-stack findings (candidate patch adopted from the real-stack rig):

- F1: the next fire stays suppressed as skipped(overlap) while the target
  Session holds an open public Turn — the wake pump would otherwise start
  a queued automation input ahead of the dispatcher's retried Turn and a
  recurring definition could starve it (128 s / 190 s measured).
- F2: a Session attached before (`harnessBootId` set) and absent from the
  connector cache is re-attached through the same takeover load a Turn
  takes, so a control-plane redeploy never strands automated Sessions on
  hosted_session_already_attached.
- F3: a run whose turn the Harness stopped inside settles once as failed
  with execution outcome_unknown — the H0b pairing rule now admits
  exactly that pair (TS, Java, schema, and the flipped
  failed-with-unknown-outcome fixture) — instead of running forever and
  starving its definition.
- F4: the automation scanner runs on a dedicated
  managedAutomationScheduler, never the one-thread default pool that also
  carries the materializer, retries and recovery ticks (5 ms / 5 ms
  measured).
- F5 was already closed by the N4 round (operation replay markers +
  target/content binding); the client's three wire guards
  (202-only, settled-only, operationId echo) are pinned at the
  qwencode SDK level now.

Review findings:

- Behavior: definitive 4xx route refusals settle the occurrence as
  skipped with the refusal as reason instead of 64 lost-answer retries
  into `unknown` (manual runs answer 409 with the reason first, 202
  replay after); the update's idempotency digest binds session_id so a
  same-key different-session request conflicts instead of replaying;
  CronSlots.compile accepts ASCII digits only (ARABIC-INDIC no longer
  parses to minute 3); the close path skips its whole-transcript
  projection entirely when no notification of the named sources awaits;
  the /automations/operations error classification is typed
  (AutomationOperationConflict, a new ManagedSessionWritesStoppedError
  -> 503, a new ManagedSessionModeGateError -> 409 mode_disabled,
  ManagedSessionConflictError -> 409, plain record errors -> 400),
  deleting five hand-listed message substrings.
- Contracts and specs: store and event-envelope fixtures gain the 18th
  event kind `operation.replayed`; the slots fixture gains Pacific/Apia
  with whole-day date-line jump witnesses; OpenAPI gains the create
  request's required fields, 503 declarations on all automation
  mutations, automation_busy and invalid_automation code mentions,
  UTF-8-byte units for goal/prompt, the TaskState $ref on run.state,
  occurrence_key cursor wording, and bounded catch_up invariants.
- Docs: the H6 slice table no longer claims the domains stay disabled;
  the zh-CN H6 doc recovers its later-slices paragraph; Files-affected
  lists every touched file; the mode-gate comment and PR deferral stop
  pretending the gate checks only the first revision; the backoff cap
  reads 256 s (not 5 min); the slot-CPU wording is result-bounded rather
  than enumeration-bounded; README says the same.
- Test scaffolding: the H2-backed scanner suite deletes rows between
  methods (tick iterates all schedule tables), the Harness fake answers
  an already-committed run before its refusals (production order),
  fixtures type their ChatRecord payloads, the committed input decodes
  through its own decoder, a multi-definition session distinguishes the
  record revision from the domain counter, a workspace-profile load now
  exercises the restore verifier's automation domains, and the route
  settle wiring gains open-path recovery, close-path model-free settle
  and runner-catch coverage.

Verification: packages/core managed-runtime full suite green
(hook-scale timeouts recur only under parallel load and pass cold);
cli hosted-automation 17/17, hosted-harness-session 242/242,
hosted-monitor-wake 19/19; managed-agent-server module full suite
green (AutomationScannerTest 31, QwenHostedHarnessConnectorTest 40,
Qwencode HostedHarnessClientTest 37); npm run typecheck/lint,
Prettier, checkstyle and spotbugs all green.

* fix(managed-agent): enforce the advertised definition goal bound

maxGoalBytes was exported as one of this module's limits but had no
read site: the goal bound arrived only incidentally from the record
contract's generic text bound. assertAutomationDefinition now enforces
its own constant directly, with a refusal named for it (the parse probe
behind it keeps the same invariant if the constants ever diverge).

* docs(managed-agent): sync the EN runtime design's backoff clamp with the code and the ZH twin

* test(managed-agent): admit V53 in the W1c migration upgrade pin

* fix(managed-agent): keep store faults retryable and bind idempotency across operation kinds

Two findings from the fourth review wave on PR #13598:

A store HTTP or transport fault surfaced to the route through the
ManagedSessionRecordError superclass, so a fire_run whose prompt read hit
a transient 503 was answered 400 — and the scanner's definitive-4xx
branch settled the occurrence skipped forever, while redrive only visits
firing rows. assertReplayContent's validation catch likewise wrapped
promptOf's read into a conflict, so an honest retry of the identical
request became a 409. The route now classifies both store fault classes
as a retryable 503 and the content check reads the prompt outside the
validation try; only the request's own shape is still a 400/409.

The mutation replay looked up history by (operation, operationId), so a
cross-kind retry under one Idempotency-Key — a retire(K) after a
committed create(K), when the control plane lost its command row before
remember() — slipped past and really cancelled the definition. The
funnel now consults the journal for any committed operation under the
derived identity and refuses another kind with
automation_operation_conflict, matching the public one-key-one-operation
constraint; the journal rebuild makes the check hold across a Harness
restart, and the fake mirrors the rule at control-plane level.

Regression tests in both languages: funnel cross-kind refusal (same
session and across a reopen), a store fault passing through the replay
content check as itself, a route-level fire answered 503 whose claim
still dispatches after the store recovers, a scanner fire answered 503
staying firing for the redrive, and a service-level cross-kind retry
conflicting with the mirror left live.

* fix(managed-agent): refuse a cross-Session idempotency-key reuse before relaying

The operation identity derived from (tenant, Idempotency-Key) is tenant-scoped, while the journal proving it is Session-scoped: create(K, S1) committed and mirrored, the command row lost before remember(), a create(K, S2) would relay to S2's Harness and mint a second definition under the same scheduleId, which the mirror then shadows behind the first Session's resource. create() now checks the durable mirror for the derived scheduleId first and, when it landed on another Session, answers automation_operation_conflict instead of relaying; identical retries on the same Session still pass through to the funnel's replay. Regression: the lost-row cross-session create conflicts with nothing relayed and the mirror still naming the first Session.

* fix(managed-agent): claim automation operation identities before the side effect

The mirror check added for the cross-Session reuse only sees answers that already landed: a create whose Harness committed but whose answer was lost has neither a mirror nor a command row, and a concurrent request can pass the check before the first one mirrors — both windows then committed the same derived scheduleId a second time, shadowing the first definition.

create() now claims (tenant, Idempotency-Key) with an atomic pre-relay insert into the command ledger: the primary key arbitrates concurrent racing, a claim held by a different request body conflicts idempotency_conflict before anything relays, and the identical request falling back into an unanswered claim redrives it so the funnel replays what it committed. remember() settles the claim row rather than adding a second, and the same-Session mirror guard stays for rows the control plane lost on its own.

Regression: a lost-answer create keeps its claim so the cross-Session retry conflicts while the owner redrives to its committed revision, a foreign mid-flight claim conflicts with zero side effects, and the lost-answer retry path asserts the claim-shaped row.

* fix(managed-agent): guard claim settlements by owner, claim every ledger mutation, release on definitive refusals

The shared remember() settled by (tenant, key) alone while update and retire did not claim first: an update that had passed its replay check before a concurrent create could overwrite the create's completed result_json — the row kept the create's digest and scheduleId, so the create's honest retry was then answered with another automation's resource with replayed=true.

update() and retire() now join the pre-relay claim protocol create() introduced: every mutation using the command ledger binds (tenant, Idempotency-Key) to its request before any side effect. The settlement itself is guarded twice — request_digest and actor must match the row's owner, and only an unanswered claim can be filled — so a late re-driven mutation can neither steal the row nor rewrite a completed result; the guarded settle is pinned red against the pre-fix unguarded form. Since only commit-proven refusal answers exist on the Harness, a definitive 4xx now releases its own unanswered claim instead of burning the key, so a fixed re-send of a refused request succeeds; the retired-definition replay branch settles its own claim the same way.

* fix(managed-agent): release claims only for refusals proven pre-commit

A blanket 4xx release read too much into the answer: invalid_automation_operation answers both a malformed new request — committed nothing — and an integrity failure while reading a previously committed resource, where the original operation may well have landed. Releasing the claim in the second case orphaned exactly the definition the claim was binding, opening the same-key-on-another-Session window the protocol exists to close.

Claims are now released only for 4xx codes that answer exclusively before any commit (not found, mode gate, conflict, stale revision, retired, quota); a proofless answer keeps the claim, so the other request conflicts and the owner still re-drives. The fake gains a pre-commit mutation-refusal seam, and the regression pins the proofless 400 keeping its claim, blocking the second Session and settling on the owner's retry — verified red against the blanket form first.

* fix(managed-agent): release claims only on refusals audited pre-commit

The 4xx-minus-400 partition still read proof into answers it cannot have: a re-attach that finds the Session running a Turn answers 409 hosted_session_already_attached — an attachment state, not a commit verdict — and a takeover that misses the Session answers hosted_session_not_found, equally mute on whether the mutation landed. Releasing the claim on either orphans a definition the original Session may well hold.

The release set is now an audited enumeration of the funnel's own business refusals, each with a single pre-commit answering path — automation_not_found, mode gate, operation conflict, stale revision, retired, count limit — closer to exhaustive proof than to convenience; every other answer keeps the binding. The attach-conflict regression mirrors the proofless-read one: same claim surviving, second Session conflicting, owner re-driving to its own answer; verified red against the loose form first.

* fix(managed-agent): release claims only on request-determined verdicts

A quota refusal is a verdict about the moment, not about the key: the claim row is shared by every attempt under one Idempotency-Key, so delivering an early attempt's automation_count_limit 409 and releasing the row dissolves the binding its later sibling may already have committed against — after which a third body under the key mints a second definition and the sibling's delayed success settles to the wrong mirror.

What may release is now exactly what cannot move between attempts: the mode gate, decided by the definition's sessionMode against the deployment's admitted set, identically every time the same request is sent. Capacity, existence, binding, revision and terminal verdicts all evaluate shared state and keep the claim; the quota regression mirrors the family's other pins — refused attempt, foreign body conflicting, sibling committing and settling — and was verified red with quota back in the set first.

* fix(managed-agent): map wake-turn ids to path-safe Broker sessions, and refuse fires a blocked Session can never run

The round-2 real-stack pass found two walls. Every automation turn whose model calls a Workspace tool died at the Broker's path-safe Runtime Session id rule (`arun_…:input` rejected at acquire), blocking its Session: wake input ids are committed as the turn id, so every Broker handle built from a prompt id now maps a non-path-safe one to a stable `wake-<sha256>` instead (`hostedRuntimeSessionId`); the mapped form is itself path-safe, so the map stays idempotent wherever lease/record paths already carry it. And a recovery-blocked Session kept answering 202 to fires, committing runs that can never start while its slots kept scheduling: `fire_run` now answers 409 `hosted_session_blocked` unless the occurrence's run is already dispatched (a pure replay answer carrying no new fact), the scanner recording the slot skipped with that reason. The run the F3 path settles `failed`/`outcome_unknown` now consumes its crashed input too — a `turn_result` settles its turnId — so a reload stops classifying the same crash as recovery and re-blocking the Session every time.

Regression: the id mapping (stable, idempotent) and a route-level wake turn whose tool call reaches acquire under the mapped id (red on the raw form); a blocked Session refusing a fresh fire while replaying its crashed run, consuming the crash, and firing after reload; the scanner recording `skipped(hosted_session_blocked)` on the new refusal. Real-stack F1/F4/F5 and the claim protocol were re-verified by the report; the dead journal after a missed activation renewal is pre-existing on main and goes to a separate issue.

* fix(managed-agent): settle the runtime park and silence of a crashed wake turn

The round-3 real-stack pass found two new walls past F6/F7. When the Harness crashes inside an automation turn's tool call, the F3 settlement left the checkpoint parked at await_runtime — every later turn on that Session refused (await_runtime is not a model-start phase) and the wake Broker session held the Workspace execution lease for every Session on it. When the crashed turn's prompt was text, its naked user record merged into the next turn's model history, so the crashed instruction ran a second time inside an unrelated run.

In the F3 recovery path: when the crashed turn's checkpoint holds in-progress Runtime items, the existing stop/cancel helpers drive them to terminal, settleParkedTurnCancelled records each cancellation as its cancelled tool_result — so the checkpoint advances out of await_runtime and the history stays well-formed — and the wake session's Broker lease is released. The Session unblocks only once that park is provably settled: a failure to even read the checkpoint, or to stop or release any part of it, keeps the block rather than letting a fire run against an unproven park. The consume write of the crashed input's turn_result gains bounded retries on SessionTranscriptChangedError, so a concurrent route claim cannot drop it and resurrect every reload classifying the same crash as recovery. The fire refusal itself is unchanged: while blocked, anything that would commit a new fact answers 409 hosted_session_blocked.

For the history side, runHostedHarnessTextTurn's assembly now excludes every user record whose prompt id ended with an error or cancelled turn_result (excludeTerminallyEndedUserRecords): a turn that ended without an answer is over, so the next turn never meets the crashed instruction as live work — pinned one level up in the runner's own suite (the crashed prompt is absent from the chat's setHistory while an answered older prompt passes through).

* fix(managed-agent): one aftermath for a crashed wake turn, reconciled and retried until proven

Round 4 of the real-stack pass found the round-3 recovery still wedging in three shapes and the F9 fix dead on the real path. A crash inside a wake turn's Runtime call whose park settlement fails once consumed the input anyway — after any restart nothing classified the crash again, so the Session and its Workspace lease wedged for good (one transient Broker 503 sufficed). With overlap skip, Spring skipped every later slot, so nothing ever loaded the crashed Session at all and its wake Broker session kept the Workspace execution lease while every other Session span on workspace_busy with no upper bound. The first user Turn to load a crashed Session raced the recovery behind the load and failed post-admission. And the round-3 history exclusion read turn_result records from a projection that never carries them, so the crashed prompt still merged into the next turn's model history.

The crashed wake turn now has one aftermath, keyed on the journal and the checkpoint rather than on which settle ran before it: the run fails with its execution unknown; the parked Runtime executions stop to terminal and settle cancelled — each cancelled tool_result committed once so the checkpoint leaves await_runtime with the call and its answer paired; the wake session releases its Broker lease, re-attempted on every pass since the journal cannot record whether an earlier pass landed it; and only then does the input's own turn_result consume it, with a transient journal conflict retried. An unproven settle keeps the block and the input pending, so the wake pump's blocked cycle — re-armed across a dead pass — the next load, and the fire that loads the Session all retry the same sequence instead of hinging a permanent wedge on one transient fault. For the case nothing loads the Session — overlap skip darkens the definition — the scanner, before recording a count-based refusal (never the open-Turn one), asks the Harness a fourth verb, reconcile_run, which commits no new fact, classifies and settles the crashed wake turn, and answers its run; the decision prices the count off those answers rather than the lagging mirror. A prompt arriving while the park is unsettled is refused pre-admission with the retriable 409 hosted_turn_recovery_required. And a turn that ended without an answer keeps its prompt out of every later turn's model history, filtered against the journal's turn.settled events at both history builders — the dead record-layer exclusion is gone, replaced by a pin through the real builder path: with the exclusion deleted, the new test fails.

Witnesses: six new Harness pins (park settle after a Broker fault with cancelled tool_result / lease release / in-process unblock, the reconcile route both polarities, consume retry, history exclusion, pre-admission prompt refusal) and three Java scanner pins (a wedged run reconciled into the very slot that asked, an unanswered reconcile keeping the refusal, an open Turn never triggering a reconcile); the fake gains the reconcile verb. The AutomationScannerTest shared-row hygiene now sweeps the extension record/resource tables, closing a latent stale-mirror revival the new pins exposed.

* fix(managed-agent): relay automation operations through #13163's workspace admission split

#13163 split requireReadyForNewWork by intent (action responses probe the mount, new work takes the full admission). The automation relay added by this branch still called the two-arg form, so main's build broke on the merge: fires, defines, retires and the new reconcile_run all drive new work — take the full `workspaceExecution.authorize` admission, the same check a fire's relay took before the split.

* fix(managed-agent): re-check the turn slot after the wake-park probe before claiming

The prompt route's admission mutex was atomic because no await stood between its `session.active` refusal and its claim. The wake-park probe inserted one: while the route awaited, the pump's wake turn could pass its busy re-check and start, and the route then overwrote the claim unconditionally — two concurrent turns on one journal, the exact wedge the unsettled-inputs guard exists to prevent. Re-read `session.active` synchronously right before the claim, restoring the check-then-claim pair's atomic discipline (the wake pump's own runner follows the same re-read-after-await rule).

A deterministic interleave pin is not offered here: the window needs an injection point inside the route's async span, which the route does not have today; the guarantee rides on the re-established discipline and the file's existing concurrency witnesses.

* fix(managed-agent): price a reconciled refusal off what the mirror has not caught, never doubly-subtracting

Audit round 1 (lane D). Two questions in the rebuilt refusal price after a reconcile:
1. The settlement the reconcile just made the Harness commit IS also the record mirror's correction: the host store's commit path applies the extension record synchronously (ManagedSessionStore. The mirror is not lagging the answer it just answered. Pricing by `countActive − resolved.size()` therefore subtracted a repaired run twice whenever a second run was genuinely live — one corrected count below the policy bound admits a fresh fire the policy forbids (witness: overlap skip with a wedged run and a live one).
   Price instead off the recounted active, subtracting only the occurrences the mirror still lists blocking — in production nothing, in the unit fake (which never writes the mirror) exactly the faked answer, so the earlier witnesses hold under both shapes.
2. Resolution must key on the ASKED occurrence's run going terminal (journal vocabulary 'settled' plus the mirror's 'completed'), never on the route's bare repaired flag: the harness keeps the oldest pending wake input, which can be another occurrence's crashed turn while the asked run stays live — the flag alone would resolve it anyway.

Witness: aSettledRunIsNeverSubtractedAgainWhenTheMirrorAlreadyDropsIt — a wrapped connector applies the failed mirror row inside the reconcile answer (production shape), beside a second live run; the slot 45/46th test stays refused where the old arithmetic fired. Old witnesses re-run green: the wedged run still reconciles into the very slot that asked, an unanswered reconcile still keeps its refusal, an open Turn still triggers none.

* fix(managed-agent): settle the journal side of a deterministically declined wake park

Audit round 1 (lane B). For a wake turn whose park the Broker cannot identify — a Shell item on a hooks/MCP-defined Session, a runtime route committed for another Session — the recovery machinery answers a typed, deterministic RecoveryDeclined, the same verdict a user Turn's takeover gets. The crashed wake turn's aftermath read every such answer as 'pending': one decline wedged the Session in an endless 500 ms retry (pump cycle, next load, reconcile_run) before any reload could classify the crash away. Now an inner decline path settles what the journal can settle anyway: the cancelled tool results are committed once (their idempotent settle is what the stop usually proves first), the checkpoint advances out of await_runtime, the consume write follows its retries, and the aftermath answers settled — the run ends failed with its execution unknown and the Session unblocks in-process. The wake session's own lease, undeliverable without its broker identity, stays the system's re-admitted kind, exactly as the takeover path leaves an undeliverable release documented at runtimeLeaseHeld, rather than pretending an infinity of retries. Witness: a shell-parked fixture wakes the decline; the aftermath answers settled, the execution proves outcome_unknown, the checkpoint leaves await_runtime, one cancelled tool_result pairs with the parked call, the journal consumes the crash. Old witnesses re-run green, including the latched-broker park settle that relies on 'pending' for every transient fault. 317/317.

* fix(managed-agent): refresh-retire a definition gone under the scanner's feet, size the reconcile fan-out to the cap

Audit round 1, lane A's two reconcilable minors. A reconcile that meets the chain's death — the definition retired, unknown, or its Session gone — must do what a fired answer's definitive refusal does: retire the mirror and record the slot skipped(definition_retired). Reading it as 'no answer' and keeping skipped-overlap would now stand every future slot forever, and a retired definition never frees its ledger. And the fan-out bound was a hard 5 — below any configured allow concurrency: wedged runs beyond it second a refusal the next tick could have finished this tick. Size it by the configured bound (at least two), exactly how many runs the refusal itself can still count at `settings.getConcurrency()`. Witness: a wedged minute definition meets its journal-death over a reconcile — the mirror retires, the occurrence reads skipped(definition_retired), the next tick never asks again. The earlier pins (wedged repair, unanswered reconcile, live-turn untouched, mirror-lagging math) stay green at 47/47.

* docs(managed-agent): cover the definitive-4xx settlement and the audit-corrected reconcile math

Lane C found decision 5 silent on the generic definitive-4xx arm the scanner shipped (typed code recorded as the skip reason; the code's name alone records it, hosted_session_blocked included), and the audit round of the same pass corrected how decision 15's reconciliation maths should read: the recount after a reconcile subtracts only what still reads blocked (the settle and the mirror row one commit together), a run resolves only by its own terminal answer (never the session-scoped repaired flag), a dead chain or Session refresh-retires the mirror as skipped(definition_retired), and the fan-out sizes itself by the configured allow bound (2 at least).

* fix(managed-agent): take over the cold Broker before a parked crash's stop and release, and publish wake Shell turns under their mapped identity

Round 5 review, findings 1-2. For finding 2 (the parked crash's aftermath dies against a cold Broker): the Harness and its Broker expire together, so the replacement owns an empty Session table — the status poll on the persisted READY wake session answers 404 runtime_session_not_found, the release 503 runtime_reconciliation_required, and the consume never runs, so the Session stays blocked in whatever loop its pump, next load or reconcile keeps walking. stopParkedRuntimeExecutions now adopts before any status question, as the passive takeover does — re-acquiring a READY session under this identity is idempotent server-side, and a 409 runtime_session_not_acquirable is proof the session is already released, so nothing remains to ask; the aftermath's release block adopts before releasing the same way. The cancelled tool results pick up the same bounded transcript-conflict retry the consume write carries.

For finding 1 (Tool v3 automations cannot publish a wake Shell turn): the Tool turn's publisher and binding references carried the raw prompt id while prepareV3 and encodeToolInput speak the mapped wake-<sha256>, so the real ToolPublicationStore answers 'Broker execution identity conflicts' at reserve. The reference surface across prepare / publisher / /grants now mirrors the Broker's stored identities (this.broker.runtimeSessionId), with the logical turn id kept only where the checkpoint asks for it.

Witnesses: a wake shell turn's publication binding mirrors wake-<sha256> across reference.sessionId/promptId with the raw id never admissible, refuting the mapped assumption; the folded park settle proves an adoption precedes every status and release on the cold surface (acquire < status, acquire < release), beside the settled crash, the released lease and the replayed failing run. Suite 318/318.

* fix(managed-agent): keep the cold-Broker adoption at the wake path's own sequence, out of the shared recovery step

Review round 6 (the red unit lane). stopParkedRuntimeExecutions is shared by the user-Turn cancellation takeover, which already adopts the parked session inside its own passive branch — inserting an adopt step into the shared function made that ownership double: takeover-callers hit their no-server doubles on a real fetch and died outright, and in the two 'outcome is unknown' cases the new call pre-empted the typed refusal with a transport error. The adopt obligation belongs to whichever caller starts from a Session that no harness session ever owned — the crashed wake turn's aftermath — so it now lives there alone, before the stop and before the release; the recovery function itself returns to its base shape. The cancelled tool results keep the bounded transcript-conflict retry. The lane 44/44 backs this in context with the folded cold-surface pin (318/318), whose order pins (acquire < status, acquire < release) continue to hold.

* fix(managed-agent): read the role column for automation access checks after the roles migration

* fix(managed-runtime): close the R3 correctness and pin-hardening findings

- assertAutomationDefinition refuses a non-string goal with the typed
  contract error instead of a raw TypeError (R3-9)
- automationTurnText and the fire route bound firedAt to the renderable
  Date range, answered as invalid_automation_operation (R3-14)
- drop the manualOccurrenceKey export nobody consumes (R3-17)
- pin the two terminal states outcome_unknown must never certify
  (R3-29), the definition digest pre-image byte-for-byte (R3-30), and
  the Legacy control sentence in full (R3-31)
- resolvesTimezone matches the JDK acceptance set for the three short
  ids a full host-tz sweep found diverging (R3-33)
- wallClock aligns to the minute before subtracting, so a sub-minute
  fraction no longer biases the zone offset (R3-34)
- compileCron shape-refuses the atoms the contract and the Java
  evaluator refuse instead of coercing them with Number() (R3-35)
- a fresh-command re-claim of one occurrence meets the committed run
  instead of appending a no-op revision (R3-36)
- pin the one-transaction dispatch span (R3-37), fence the
  operation.replayed answer to the committing Session and journal the
  honored revision's authoritative ref (R3-38, R3-39)

* fix(serve): close the R3 findings in the automation and wake surfaces

- the publisher's foreground register compares the mapped Runtime
  identity on wake turns, never the raw logical id (R3-2)
- an erased checkpoint verdict fails the wake park probe and the crash
  aftermath closed instead of answering over state nobody read (R3-6,
  R3-7)
- the F8d prompt refusal carries hosted_turn_recovery_in_progress, the
  one code the coordinator outlives past its budget until the pump
  settles the park (decision 15)
- pin the already-released adoption, the dead-writer 503, the mapped
  Shell owner, the resume builder's unanswered exclusion, the cold
  surface's acquire ordering, the reserve reference presence, the
  firedAt admission bound, and the mode-gated define publishing nothing
  (R3-20, R3-54, R3-56, R3-53, R3-5, R3-24, R3-14, R3-50)
- one wake-source predicate instead of three copies (R3-25), keyed run
  lookups on the fire and reconcile paths (R3-26), and the quota
  message naming the code the route emits (R3-49)

* feat(managed-agent): split the wake-park refusal code so the user Turn outlives the pump window

A prompt refused because a crashed wake turn is still unsettled clears
under the wake pump's own recovery channel, so the coordinator's
five-retry pre-admission budget is a lifetime for a transient state.
The route now answers hosted_turn_recovery_in_progress for exactly that
window and the coordinator retries it without spending the shared
budget; hosted_turn_recovery_required keeps the durable meaning and
still meets the budget (decision 15; R3-1).

* fix(serve): dedupe the merged records imports and finish the wake vocabulary union

* fix(managed-agent): close the R3 findings across the automation control plane

- the scanner settles skipped only for the route's definitive refusal
  codes; a transient session-state 4xx (hosted_session_blocked most of
  all) defers and re-drives under the bounded backoff (R3-11)
- create() exempts its own landed definition from the capacity check,
  so a lost-answer retry at the 32-cap re-drives instead of being
  refused by the very definition it committed (R3-3)
- retire() retires locally when the Session is not ACTIVE, the fallback
  a forever-skipped definition otherwise never meets (R3-12)
- the automation relay resolves the attachment before reading the
  client, as doContinueManagedRuntime already requires, so an adoption
  landing mid-load can never strand the call on the closed instance —
  and the child relay takes the same order (R3-10)
- the automation-run binding check and the scanner's mirror read both
  converge on ManagedExtensionRecords' terminal set (R3-43)
- automation tunables bind with startup bounds exactly like the sibling
  harness settings, and the shipped YAML's flattened keys are pinned
  (R3-40, R3-41)
- countActive, findBlockingRuns, listOccurrences and findOccurrenceView
  read each run's mirror state by its exact projection PK instead of a
  whole-table join; the mirror refresh sweep gains its
  (domain, created_at, record_id) index in V55 (R3-45)
- the fake's definition field set is one list (R3-48)

* docs(design): sync the actor-roles and automation documents with the landed surface

The public surface is eleven controllers with 39 public + 24 WebShell +
24 internal routes (87 total): the overview and the per-class tallies
read that, both languages (R3-22, R3-32). The 2026-10-04 automation
document's H6a-era domain bullet names what H6b enabled, both
languages (R3-21).

* style(serve): prettier-normalize the merged wake surfaces

* fix(managed-agent): close the review's three P1 findings at 786c319111

- R3-8/P1-3: both disabled-domain fixtures move past every name the two
  landings enabled (schedule by H6b, child_acceptance by H4b) to
  team_state — in the domain vocabulary, never in the plain enabled
  list and not in the extension-record body index; the comments name
  why child_run and monitor_run cannot serve (per-kind exemption, or
  the extension-funnel refusal answering first)
- P1-2: runtime_session_not_acquirable proves nothing about release: a
  RELEASING session refuses the adopt too, and only an explicit release
  completes the condition, so the release pass always attempts it
  instead of skipping on the 409. Pin: a dying worker release completes
  in the very next pass (stops, settles, consumes, no released
  identity ever skipped), and skipping it goes red
- P1-1: the wake Shell publication reference is one pair on two axes —
  sessionId = the mapped Broker Runtime Session (the execution's own
  owner) and promptId = the logical prompt id (the checkpoint's
  identity) — so the reserve's execution compare and the store's
  requireCheckpoint both approve what wave 8 aligned too far

* style(core): prettier-normalize managed-session-resources after the probe cleanup

* fix(managed-agent): close the R4 findings in the wake aftermath and the prompt latch

* fix(managed-agent): carry the logical turn identity through prepareV3 and refuse cron over the declared 400 bound

* test(serve): gate the released-Broker aftermath pin on the consume, not the block window

* fix(managed-agent): release a wake session the live Broker still holds as RELEASING

A worker release that fails on a running Broker leaves the wake session
RELEASING while that Broker still holds it in process, so the
aftermath's adopt is refused with 409 runtime_session_not_ready rather
than the cold Broker's runtime_session_not_acquirable. Both adopt sites
in settleCrashedWakeTurnAftermath accepted only the cold code, so every
pass threw before reaching the release, and the Workspace lease stayed
held until Spring restarted or someone released the session by hand.

Accept both refusals as "released or mid-release", as
hosted-mcp-session.ts already does for its owner release; the Broker's
release of a RELEASING row is idempotent once nothing is active. The
transient-release-failure pin now runs with both Broker answers, and the
live one goes red without this change.

* test(serve): pin the logical promptId on the foreground Shell publisher reference

The wake-turn register test only checked that register ran once. Reverting
the reference's promptId to the mapped Runtime Session passed the whole
tool-turn and broker suites, while on a real stack the store refuses that
reference and the wake Shell's execution goes unknown. Assert the pair:
the mapped Runtime Session as sessionId, the logical prompt id as promptId.

* style(managed-agent): restore the final newline of ManagedAgentProperties

The main merge in 9f809670c7 dropped it, and Checkstyle's
NewlineAtEndOfFile fails the Hosted process fault gates job on it.
2026-10-10 00:26:17 +00:00
Shaojin Wen
8c127de524
feat(managed-agent): H5b/H5c channel runtime for the email reference adapter (#13572)
* feat(managed-agent): H5b/H5c channel runtime for the email reference adapter

Make the two channel domains executable end to end on the managed path.
Inbound: a trusted adapter submits an authenticated platform event to the
control plane, which dedupes it on the four-part ingress identity (the
V47 route row), resolves or creates the route's Session idempotently, and
asks the Harness to commit the route binding revision, the staged
attachments, the input and its wake in one journal transaction; the row
turns admitted only after the Harness answered, so every crash window
replays to the committed admission. The embedded wake scheduler runs the
input as an ordinary text turn. Outbound: a settled channel turn plans its
reply as a channel_delivery record; the adapter pulls claims through the
control plane, each step is a record revision the Harness commits first
and the V47 ledger follows; a claim that outlives its lease settles
unknown and is never resent; an explicit resend opens a new chain. The
three public channel resources move from planned to partial.

Both domains join the enabled list behind an adapter gate (email only)
that the authority checks on every first route revision through the
committed policy; a delivery binds to its committed route at the pinned
revision in both languages. V52 adds the registered connections, the
route catalog and the claim ledger. The email adapter gains a managed
mode (qwen channel managed-email) that reuses the Legacy parsing and
transport helpers and keeps its own locked state.

Part of #12827 (Stage H, slices H5b and H5c). Refs #12380.

* fix(managed-agent): carry the thread id in the channel client test's reply context

* fix(managed-agent): close the recovery and isolation gaps found in H5 review

- enforce the isolated internal listener when channels are enabled: the trusted adapter surface authenticates by the tenant header alone, so the broker refuses to start without qwen.managed-agent.internal-server.port
- converge the delivery ledger with the committed record before a lease reconcile settles: a receipt whose answer was lost lands as delivered with its provider id, a committed claim moves the ledger to sending, and only a still in-flight claim settles unknown
- discover owed deliveries through the extension-record projection instead of the newest 64 bindings, so a thread beyond the window is still claimed
- plan the reply segment before publishing it so a long turn honors the inline resource bound, and exclude thought parts
- persist a segment receipt before reporting it, replaying the persisted receipt on restart instead of a blanket unknown
- clamp the upload bound to the wire base64 limit and treat a deterministic 400/403/409 refusal as permanent, never re-driving a poisoned admission

* fix(managed-agent): close the H5 review round's production breakages

- gate planReply on the journal turn.settled event: the production sink commits turn results there and never projects them, so record-projection gating planned nothing outside tests
- reconcile replies per turn, so one unplannable turn (a re-keyed route pin) no longer aborts the loop on every open
- replay a segment's persisted claim when the control plane re-offers it, instead of sending a second copy

* test(managed-agent): expect V52 in the upgrade-from-V47 migration set

* fix(managed-agent): close the remaining H5 review criticals

- re-gate a rebuilt pending event on the current sender policy, dropping it visibly instead of driving a forbidden Session
- report a reaped state lock through onCompromised and stop, like every other lock owner in the tree
- add --cwd to qwen channel managed-email and thread it through channel config resolution, matching the Legacy daemon's state directory
- refuse an out-of-range receipt before any revision commits
- answer a settle against an already terminal chain with the committed chain instead of looping the caller on a 4xx
- bound the reply context in the channel operation parser before staging could side-effect
- run the channel adapter gate on every route revision, not only the opening one
- give runChannelOperation the same workspace reauthorization and generation adoption as every other new-work dispatch, and translate plain daemon failures into the retryable envelope

* docs(managed-agent): correct the design pairs to the shipped H5 runtime

- mark H5b/H5c done in the slice table and qualify the domain-enablement statements as H5a-scoped (H5 design pair)
- name the actual rebase point (e92b60a282) and current main migrations in the current-state section
- file the qwen_managed_channel_claim table in decision 1, with what it carries and why nothing deletes it
- name the real refusals of the quota table's text and envelope bounds
- drop the never-effective wake-pump clause: every hosted Session owns a channel funnel

* fix(managed-agent): step the OpenAPI version out of #13545's way (1.35.0)

* fix(managed-agent): close the re-review's retry and bound gaps

- track the control plane's ack of the account generation: a lost registration (epoch roll whose register died, server wipe showing 404 or 409 generation_unregistered) becomes a poll-level re-register, never permanently skipped mail
- clamp the adapter text to the wire 32,000-character bound even when maxTextLength is configured wider
- rebuild a pending event only while it still fits the current message bound, so a shrunk maxMessageBytes refuses instead of truncating through
- measure the reply envelope on the serialized JSON: escape-heavy answers shrink to fit the 64 KiB inline bound instead of failing every publish
- keep a deterministic harness 400 as 400 through the control-plane translate, so the adapter's permanent-refusal branch can recognize it

* fix(managed-agent): shrink replies on code-point boundaries with the notice kept

- a further-reply shrink now re-runs a bounded UTF-8 cut (new exported truncateChannelTextUtf8 + CHANNEL_TRUNCATION_NOTICE) instead of slicing UTF-16 halves: the truncation notice survives and an emoji at the cut point drops whole rather than splitting into a lone U+D83D that SMTP would render as U+FFFD

* fix(managed-agent): close the real-stack blockers and the outbound re-drive gap

- create a channel route's never-attached Session at its first channel operation (the Harness never saw it before); a 409 means a locally attached Session is re-taken passively — Spring-only restarts no longer drop mail (candidate F1/F3 applied and verified, Java suite 55 green)
- default the scope's nullable members before the closed parser: fastjson2 drops null map values on the Java wire, so the Harness no longer 400s every first email (F2 applied with wire-shape tests)
- re-drive persisted outbound receipts on every poll, not only at restart, so a receipt whose HTTP answer died converges far below the claim lease (F4)

* feat(managed-agent): settle interrupted channel wake turns terminally (F5)

- a channel turn that died mid-flight now settles cancelled on the wake pump's prior-attempt arm instead of freezing the Session forever: a text turn has no checkpoint to continue and minting its user record twice is forbidden, so the funnel's terminal settlement is the one honest answer inside this slice; the turn never plans a reply and the thread resumes immediately
- the classification is injected per source into the wake scheduler: channel turns take terminal settlement from their funnel, monitor turns keep the recovery-blocked freeze their own fleet owns

* fix(managed-agent): close round 4's F6/F8 and the F5 tool-call wedge

- F8: translate() crosses the Harness's self-healing 409s —
  managed_session_writer_conflict, workspace_busy, workspace_unavailable,
  hosted_session_closing — as the retryable 503. A dead predecessor's
  writer lease outlives it for the lease duration, and the adapter drops
  every 409, so mail landing in the window was skipped with no retry.
  Deterministic channel 409s keep their class.
- F6: reportOrphanedOutbound() drops a receipt the control plane refuses
  deterministically (400/403/409, 404 delivery_not_found) instead of
  throwing every tick: a held entry ran before every outbox pull and
  stalled all later deliveries on the channel.
- F5 follow-up: a channel wake turn killed inside a tool call settles its
  Runtime wait before its terminal record lands. The new
  settleInterruptedTurnRuntime proves the parked executions stopped,
  settles them cancelled so each functionCall meets a functionResponse,
  and drops the dead turn's pending file-history obligation, letting the
  checkpoint leave await_runtime — settling the record alone wedged the
  Session on the next requireModelStart. A wait that cannot be proven
  stopped — or a pending approval — keeps the turn with the recovery
  fleet instead of settling into a wedge.

Carries candidate-r4.patch from the real-stack verification with its
tests (F6+F8); the F5 tool-call leg is witnessed by five new
hosted-runtime-recovery tests.

* fix(managed-agent): close the R4 P1s in interrupted channel Write recovery

- the load gate no longer refuses a channel turn interrupted inside a
  Write/Edit: its pendingTurn is pump-owned, so every attachment shape
  was refused before the wake pump that owns its recovery could start;
  the load now passes and the pump proves the stop and settles right
  after attachment
- settleInterruptedTurnRuntime refuses to settle over a checkpoint the
  authorization could not verify: a faulting Store read erases into
  blocked/missing_state, which is never evidence of "no Runtime wait" —
  the turn stays on the retryable recovery path instead of being
  reported cancelled behind a live execution
- a turn interrupted while awaiting approval is held, not settled and
  not blocked: the pump stops without blocking, so the approval's own
  resolve route stays usable
- the file-history cleanup no longer hides behind the phase gate: a
  retry whose first attempt already left the durable wait still drops
  the dead turn's pendingTurn marker instead of stranding it at
  before_model, where no load could ever clear it

Witnesses: the Write interruption recovers end to end over the HTTP rig
(load 200, proven stop, cancelled tombstone, cleared marker; 409 without
the gate exception); recovery tests pin the held arm, the
unverifiable-checkpoint refusal and the retry cleanup; the wake turn and
scheduler keep a held turn usable without a busy retry.

* fix(managed-agent): close the R6 P1s in interrupted channel approval recovery

- an interrupted turn parked in await_action now watches its durable
  Action's final state: while requested it holds (the resolve route
  stays usable), and once decided, expired or cancelled the wait is
  advanced like the close path does, every abandoned functionCall meets
  a cancelled functionResponse, and the terminal record can land — a
  reattached Session with an empty waiter registry no longer holds the
  thread forever (R6 P1); the pump re-derives a held turn on a slow
  cadence (retryMs x 30) so the decision is observed without an outside
  kick
- a transiently faulted recovery attempt no longer latches the Session
  blocked on the first try: a durably declined wait still freezes for
  the recovery fleet, but an ordinary fault (a faulting Store read
  erased into missing_state with a message, a Broker hiccup behind a
  verifiable checkpoint) returns busy and the pump retries, pinned by an
  HTTP witness whose one-shot Broker fault retries to a proven settle
  (R6 P1)

Witnesses: recovery tests park a real await_action checkpoint and cover
held / decided / expired / cancelled incl. answer-once idempotency;
createMonitorWakeRunTurn gains the busy pass-through the retry arm
needs; the scheduler's held arm re-derives on the slow cadence without
a settle-verify.

* fix(managed-agent): step the channel migration out of main's V52 (#13163) and rewire runChannelOperation

- main landed V52__managed_mutation_attempt_sequence; the merged tree
  held two V52s and Flyway refused every start. The channel
  instance/binding/claim migration moves to V53, the upgrade-from-V47
  expectation gains "53", and the design pair now cites V53
- #13163 also extended requireReadyForNewWork with its actionResponse
  arm; runChannelOperation re-answers as ordinary work (false)

Locally: flyway uniqueness check (53 unique); ManagedChannelServiceTest
13, QwenHostedHarnessConnectorTest 48, ChannelContractTest 3,
ManagedExtensionRecordStoreTest 27; hosted-harness-session 299/299;
checkstyle, build, typecheck clean.

* fix(managed-agent): expire and hand back interrupted approvals (F10/F9)

- a recovered approval whose durable deadline passed is expired by its
  recovery owner: the interrupted owner's waiter timer died with it, so
  the wake pump's slow re-derive now ends the ask and runs the ended-Action
  settlement; a still-live deadline keeps holding (F10)
- the ended-approval arm returns the turn's promptId-named runtime session
  with its ready verdict for the caller to release, exactly like the
  await_runtime arm: the turn acquired the Workspace before it asked, and
  a settled-but-held holder waits every later tool call in the Workspace
  forever (F9)

Witnesses park a real await_action checkpoint: expiry fires and settles
with a cancelled response, a live deadline holds, and the three ended
shapes each hand back the promptId-owned runtime session.

* fix(managed-agent): close the R8 round's nine recovery and durability findings

- handback durability (P1): an interrupted settle's runtime handback no
  longer depends on the wait still being visible — every ready verdict
  whose checkpoint names the dead turn hands back its promptId-named
  runtime session, and every abandoned functionCall is answered on every
  pass; a settlement split across attempts can no longer re-leak the
  Workspace lease (R8 P1 retry shape)
- channel ownership (P1): re-registering a moved Workspace/actor/cwd is
  refused while any binding remains, so new visibility can never inherit
  the old owner's Sessions, routes or receipts
- generation atomicity (P2): the monotonic check rides in the UPDATE
  itself (account_generation <= ?), rowcount-proven — an overlapping
  registration can no longer regress the row between SELECT and write
- attachments (P1): a managed-input commit now closes over the envelope's
  embedded refs, so reattachment bytes survive reopening instead of 404ing
- email outbox (P1): one unreportable receipt no longer aborts the
  claimed batch — the entry stays outbound with its outcome persisted and
  re-drives by receipt only; the remaining claimed replies still send
- mailbox ownership (P1): the remote disconnect completes while the
  mailbox lock is still held, so a replacement's registration cannot be
  revoked by an unfenced old-process disconnect
- wake intake (P1): a faulting envelope read (turnText or monitor
  contentRef) retries on the pump cadence instead of latching the
  resident Session blocked; malformed content still terminates
- reply planning (P2): planReply failures retry with backing-off delays
  while the owner stays resident
- reconcile starvation (P2): an expired claim whose ledger never left
  planned settles unknown instead of cycling at the sweep's head
  forever, so newer claims are no longer starved behind it

Witnesses: recovery split-handback/answer-once, store-level stale-snapshot
generation guard, 50-claim crowd vs sibling-channel sweep page, batch
isolation with no SMTP duplicate, ['remote','lock'] disconnect ordering,
envelope closure traversal, transient-read retry.

* fix(managed-agent): stop resident reply-plan retries when the Session seals

* fix(managed-agent): word a refused register as refused; unrnake the Write recovery witness

- ensureRegistered catches every register failure with "did not answer":
  with a deterministic 4xx the log then claims latency where there was a
  verdict — say "was refused (<status>)" for 4xx (round 6's niggle)
- the Write recovery witness asserted the Broker release synchronously
  after waiting only for the durable settle, so it raced the asynchronous
  handback 3/4 locally; the assertion now blocks on its own waitFor

* fix(managed-agent): hold the mailbox ownership until the remote close lands (R10)

runLoop()'s finally released the state lock before disconnect()'s remote
disconnect ran, so the R8 order comment was null in the production
(pollLoop: true) shape: a replacement could take the freed lock and
register while our unfenced disconnect was still on the wire. The
run-loop teardown now only stops; disconnect() alone completes the
remote close and releases afterwards. The witness asserts ['remote','lock']
in the production run-loop shape and reproduces the inverted order
before the fix.

* docs(managed-agent): resync the design pair and OpenAPI label with the shipped head (R2 suggestions)

- R2-1: attachments ship inline as managed-channel-attachment Session
  resources (<=64 KiB, larger ones listed omitted) — no O2/O3 Artifact
  staging exists outbound of the envelope; restate decision 3, the H5b
  row, the exit gate, the staged-byte quota and the later-slices note in
  both languages
- R2-2: rebase point and Flyway facts move to 95f6aa9c0f with main at
  V52 (#13354, #13163) and this change at V53; the stale 'open H4b
  holds V51' claim drops from both languages
- R2-3: the reconciler decision and pipeline row 10 record the R8
  starvation arm — an expired claim whose ledger never left planned
  settles unknown — in both languages
- R2-4: the mutation-check promise scopes the adapter gate and resend
  filter to the Harness authority, the only place they exist
- R2-5: decision 13 records the channel_ownership_conflict refusal for
  an ownership move over retained bindings, in both languages
- R2-6: the contract's newest changelog entry moves v1.34 -> v1.35, so
  it names the same release as info.version 1.35.0 and leaves v1.34 to
  #13545

* fix(managed-agent): close the R10 round's five findings

- cancel() gains the terminal-run guard settle() already carries (P1): a
  delivered/rejected/cancelled chain answers unchanged instead of a
  flag-only flip that the successor rule then refuses forever
- the channel text guard's byte verdict now lands before any attachment
  byte is staged (P1): the envelope is encoded with each publishable
  attachment's fixed-width identity, so an over-bound multibyte input
  refuses without leaving stranded resources — '€'.repeat(30_000) only
  sees the 64 KiB refusal
- planReply checks the envelope's route pin before publishing anything
  (P2): a re-keyed pin now returns no plan and no orphan reply/segment
  pair behind each refused attempt
- the wake pump's settle-verify re-read takes the same
  MonitorWakeTransientReadError arm as the intake read (P2), so one
  Store flap cannot latch the resident Session after a verified settle
- the ownership-move refusal rides inside the UPDATE itself (P2): the
  move's subquery keeps an interleaving bind from slipping past — the
  write collapses to zero rows and maps to channel_ownership_conflict
  rather than racing through unchanged

Witnesses: terminal cancel unchanged (no revision, no flag flip);
over-byte refusal pre-staging observed by publish-call count; re-keyed
pin yields undefined and zero publishes; scheduler retry through the
verify-read flap and no blocked latch; a latch-held bind racing the
register ends flat with the original owner intact, none of the move's
rows committed.

* fix(managed-agent): close the remaining ledger findings (F2/F6/F10/F13)

F2: the email adapter bounded inbound text by characters only, so a
multibyte body under the 32,000-char line could still overflow the
Harness's 64 KiB *byte* envelope verdict and refuse deterministically.
Clamp admitted text by bytes (ENVELOPE - 4 KiB headroom) with a
code-point cut on both the live poll path and the re-drive path, so the
inbound is admitted truncated instead. Witness: an 汉 x22,000 mail is
admitted with its text under the byte line, and the adapter keeps
sending afterwards.

F6: a settleInterrupted hook that itself faulted (e.g. a rejected
terminal-record write) escaped createMonitorWakeRunTurn as a generic
wake failure and latched the Session. Catch it, log, and answer 'busy'
so the next wake attempt re-drives the settle — the turn's own
transient account. Witness: first hook call faults -> busy, Session
unblocked; second call -> settled, attempts == 2.

F10: the channel operations route message-matched every record-error
into a deterministic 400, so a transient Session Store flap
(ManagedSessionStoreTransportError / ManagedSessionStoreHttpError
masqueraded as a record refusal and bounced the adapter off the ring.
Exclude both durable-store classes from the record-validation verdict:
they fall through to the retryable 503. Witness: publish flap -> 503
channel_operation_failed; a byte-limit violation of the submitted
record still answers 400 invalid_channel_operation; the healthy path
admits the same input afterwards (202).

F13: the runtime handback waited for a checkpoint naming the
interrupted turn, leaking the Workspace lease of a turn that died in
the acquire-first window — before any checkpoint bound its identity.
The handback gate now also fires for an initial authorization and for
a runnable checkpoint whose identity is still unbound; a release
against a never-acquired runtime session only answers 404, which the
caller already tolerates. Witness: session that died mid-acquire,
durable file-history marker as its trace, joins as replacement ->
ready verdict with the broker handed back for the promptId-named
runtime session, marker consumed.

---------

Co-authored-by: wenshao <nigolaschao777@gmail.com>
2026-10-09 11:39:41 +00:00
Shaojin Wen
b69338b143
feat(managed-agent): H4b child Session runtime (#13550)
* feat(managed-agent): H4a child agent and child acceptance record contract

Define the H4 delivery map as six slices and land the first: the
managed-child_run body for kind child_agent and the
managed-child_acceptance body, both dispatched inside the
managed-child_run/child_acceptance schema version 1 records, with
commit-time cross-record consistency and resource closure rules and
dual-language shared fixtures. The record-body registry's static task
kind becomes a function of the record so child_agent projects the
existing child_agent task kind while child_acceptance projects no
task. No domain is enabled for submission and no runtime path changes;
enabling child_run and child_acceptance is H4b's explicit,
server-first-deployed step.

Refs #12380, #12827 (slice H4a)

* fix(managed-agent): Tighten the child agent record contract from review

Answer the three review findings on the H4a contract: the result and
its terminal receipt now change only together and only in the revision
that settles the run, so a half-result can never wedge the chain; the
child Session identity is required once the execution proves creation
(running_attached or settled), not merely forbidden before it; and a
terminal child agent record freezes everything but its delivery line,
so a stop request can never be added to terminal evidence. Fixes are
mirrored in TypeScript and Java with new shared fixture witnesses and
eight killed targeted mutants across both validators.

Refs #12380, #12827 (slice H4a)

* fix(managed-agent): Close the H4a real-stack verified Java parity gap

The real-stack verification on the PR head found the Java child_agent
validator skipped the identity checks the TypeScript one makes on
childRunId, ownerScopeId and rootSessionId: bodies the writer would
refuse were stored (HTTP 200), and a Session holding one could not be
reopened - exactly the disagreement the server-first enablement order
depends on the store rejecting. Apply the verified candidate intact:
three id() calls at the end of requireChildAgent plus three shared
fixture cases, closing the differential to zero in both languages.

Fold in the regression-protection gap found with it: the store-level
acceptance binding test (run kind, completion call, receipt digest and
the launch input's resource closure) and the two authority witnesses
for the receipt binding and the closure, each proven red by its
matching mutant in both languages. The H4b delivery-direction
observation (delivery can outrun or contradict an acceptance) is
recorded as such in the design's follow-up section, in both languages;
it is not blocking for this slice.

Refs #12380, #12827 (slice H4a)

* fix(managed-agent): Gate shell capture admission on the shell-only parser

Round-2 real-stack verification found the remaining shell-only child_run
consumer missed in the parser widening: LocalShellStreamResultSession
proves a background Shell's start before admitting an output capture by
looking up the record by the capture's executionCallId, and since H4a
the widened parser would also parse a child_agent record as that proof.
A child agent with a matching call id would be admitted as a proven
background Shell start. It now parses with parseChildShellRun like the
cli callers, and a new witness refuses a child_agent record at
admission; the same-domain monitor confusion suite passes unchanged.

Fold in the remaining regression-protection gaps the same review round
named, one authority case and one store chain closing the settling
revision's result and receipt resources, mutants J7b/T7b killed by
each. the three F1 id removals JF1-JF3 and the reverse-parser witness
are proven red as well.

Refs #12380, #12827 (slice H4a)

* fix(managed-agent): Close the H4a child-agent contract's enforcement gaps

Real-stack review of #13505 (bot R1 round plus human P1/P2 round) found
the child_agent validator certifying records its own design forbids, with
the terminal freeze then locking every omission in permanently:

- require the dispatched definition pin and the Runtime binding that hosts
  an attached child Session, in both languages (the successor rules make
  both unaddable once dispatched);
- refuse Windows drive specs in workingDirectory in both languages;
- read resultVersion with a finiteness guard in Java, so 1e400 refuses
  instead of throwing NumberFormatException;
- bind a first-level child's rootSessionId to the journal's Session at
  commit time on both sides, and drop two successor clauses the terminal
  freeze made unreachable;
- teach verifyWorkspaceRestore to skip child_agent records instead of
  misparsing them as shells, give the shell-only parser its own refusal
  clause, dedupe the shell stop-reason logic onto the shared helpers with
  the same clause order in both languages, and derive ChildRunKind from
  the union instead of a dead parallel spelling.

The shared fixture corpora pin every new rule in both languages and the
74 pre-existing entries keep their merge-base formatting; the replay
harnesses now fail loudly on an unresolvable template; six successor
fixtures that never reached their named clause are restated or dropped;
eleven unwitnessed validator terms gain cases; the taskKindOf probes run
on real records; and the design docs correct the nine stale claims their
review round measured (citations, the unknown-delivery wording, the
depth-cap relationship, V48+).

* feat(managed-agent): H4b child Session runtime

Slice H4b of #12827 (Stage H of proposal #12380), stacked on H4a's
child/acceptance record contract: the child Session runtime — the four
producers of the H4a bodies, the first cross-Session dispatcher, the
close cascade, and kind-selective enablement, on the server-first order.

Parent side: the Agent tool joins the Hosted Workspace Shell-lane
declaration set behind a per-kind gate; startChildRun commits the launch
intent with the definition pin and the derived record key before any
side effect; the foreground arm waits on the committed chain alone and
answers from evidence, the background arm returns a task handle and
takes its result through a durable notification input plus wake, with
accepted delivered after the answer and consumed after the consuming
turn settles — two independent, separately observable facts.

Control plane: V51 lineage columns, the delivery-pending index and the
relay ledger qwen_managed_child_result_relay (the reserved task-event
name stays reserved); createChildSession under the parent's exact
binding stamps parent/root lineage idempotently from the launch record
key; the relay drives creation, attaches, copies the terminal result
and receipt digest-bound, submits the acceptance, marks orphaned at a
closing parent and unknown past bounded retries, never re-executing.
The close cascade in SessionLifecycleCoordinator cancels non-terminal
children through the ordinary close pipeline before the Harness close.

Gates: child_acceptance joins the plain enabled list; child_run gains
MANAGED_SESSION_ENABLED_CHILD_RUN_KINDS=['child_agent'] so H3's shell
stays disabled — and the owed delivery-direction decision landing as
the reverse commit-time check in both languages (delivery reaches
accepted/consumed only with its acceptance record, and never returns
to unknown/rejected after one). Public OpenAPI, Flyway-adjacent schema
and WebShell surface do not move.

Validated: core 2310/2310 and cli serve sweeps green (timing-sensitive
flakes documented in .qwen/e2e-tests/managed-child-session-runtime.md,
including one pre-existing failure reproduced on the base tree);
managed-agent-server surefire green except one pre-existing integration
failure reproduced on the base tree; 12 targeted mutations (7 TS + 5
Java) all killed; dual-language contract replays green.

* fix(managed-agent): Close the H4b review round's relay, cascade and wake-consumption gaps

The review round found five child-runtime gaps this commit closes:

- findChildLineage returned a null row for a root Session through
  Stream.findFirst() and threw, breaking every Hosted Session create.
- The relay binding probe read the tool-session Runtime record, which a
  text-only child never acquires; it now reads the physical Runtime
  binding that warm() creates when the Hosted tool turn is constructed,
  and the tool-session read path is retired with it.
- The close cascade treated a child created but not yet attached as
  unstarted; it now resolves the child through the relay ledger before
  judging, and closes it through its own Session lifecycle (admitted
  idempotent close, owed until terminal Session status) instead of a
  bare daemon DELETE — child creation inherits the parent creator's
  workspace grant so that admission passes the creator checks.
- Journal debt (stop or terminal revisions faltering) re-arms the
  whole close instead of being dropped, with the acceptance-answered
  early-out restricted to arms past delivering so the relay still owes
  its own mark_accepted.
- The wake-turn pump treated its blocked sentinel branches as settle
  proof and consumed the delivered child result without a Turn; the
  consumption wrapper now requires a genuine settle.

Terminal-classified relay rows also leave the discovery set so a
grown ledger cannot starve the fleet scan, the funnel routes conflict
and record refusals as typed errors instead of message matching, the
turn-settle flush keeps the unconsumed remainder owed when it dies
mid-commit, and the design documents carry the corrected semantics in
both languages.

* fix(managed-agent): pass the MariaDB-lane Java checkstyle gates

The pushed H4b review fixes left a trailing-whitespace javadoc line, two
imports eroded by the relay binding probe change, and an empty switch
default written in a shape both checkstyle rules reject; the lane runs
checkstyle:check that the local mvn test path does not.

* fix(managed-agent): reconcile the H4b create→attach window, lost delivering advance, and attach-less settle

The second review round proved three gaps in the first:

- A child whose create answered (Session and lineage committed) while its
  ledger advance died was judged unstarted by the close cascade: the
  cascade now consults the relay ledger and then the committed lineage
  row, so a missing ledger id never proves not_started.
- Settling such a ledger-only child committed close_scope(started=true)
  over a body whose childSessionId is null, which the record parsers must
  reject: the cascade first replays the dispatch_started/attach chain
  from the proven evidence — the bodys own committed dispatch facts, or
  the physical Runtime binding the child warms with — so the settling
  revision always commits over a chain the parsers accept.
- An accept committed while the delivering advance was lost left the
  ledger watching with the answered acceptance short-circuiting every
  scan: answered acceptance short-circuits only arms the relay never
  owed, and a watching row reconciles through the same idempotent walk
  (replay the result, the acceptance, then the advance).

* fix(managed-agent): tolerate repair journal failures and replay committed child results

The review round on the create-attach reconciliation found two more:

- The repair ops' refusal/transport-ambiguous throws are plain
  RuntimeExceptions (DaemonHttpException, MutationOutcomeUnknownException),
  which escaped the IllegalStateException guard and skipped the child's
  own physical close — the reconstruction's journal debt no longer
  preempts the physical effect.
- A commit_result whose acceptance reply was lost minted a second terminal
  revision over frozen result references: settleCompleted now answers the
  committed copies after byte-equality with the published resources, so a
  replay returns the original result instead of a revision the terminal
  rule must refuse.

* fix(managed-agent): expect the hook-listed Agent tool in the hosted tool-turn E2E driver

H4b advertises the Agent tool on depth-0 Shell-lane Sessions behind the
child kind gate, so a shell-profile fake-model assertion pinning exactly
[edit, monitor, read_file, run_shell_command, write_file] legitimately
now reads agent first; the other lanes keep their exact vocabulary as
designed (files-driver unchanged).

* fix(managed-agent): commit tool-arm consumption only after durable Turn settlement

The tool-arm consumption flush fired before the turn_result commit: a
crash between them, or a turn-result write that keeps failing through
the outer retry, left child records durably consumed while the consuming
Turn never settled. The flush now runs only after the Turn settlement
commits — the wake turn's requirement applied to the tool arm too; and a
mid-flush death still keeps the remainder owed in the queue, so the
recovery drive can finish each committed side.

* fix(managed-agent): wire the real H4b stack end to end

A real-stack verification rig of head proved three blockers and three
follow-ups, every one confined to seams mocks could not reach:

- The relay got a null Runtime Broker in the real Spring app: the owned
  service never became a bean, so scan never walked a single pending
  row. Its owner now exposes service() and the configuration registers
  it with destroyMethod = "" (lifecycle stays with the Broker), and the
  relay tolerates missing wiring as a retry instead of a null drive.
- terminalResultText filtered part_type = 'text' while the only writer
  stores assistant text as 'output_text'; the query and its fixture now
  agree with the written vocabulary.
- The sent-arm notification escaped its full text into the bundled
  input, so an XML-hostile 20 KiB result rejected accept at the inline
  bound: the notification now carries the design's bounded summary —
  escape-then-bounded below the inline cap with a marker — and full
  bytes stay on the acceptance record. Its <task-id> is the task_ id the
  launch itself returned.
- A foreground child + any tool deadlocked on the shared Workspace mount
  with its waiting parent: the mount is taken only for a batch with a
  non-agent tool, never held across an agent-only wait.
- A done child kept an ACTIVE Session and its worker forever: the relay
  now admits the child idempotent lifecycle close (shared with the close
  cascade through a new ChildLifecycleAdmissions component).
- A child still RUNNING no longer eats the attempt budget — deferred
  without attempt++, scheduling is not failure — and the discovery set
  resurfaces delivering ledger rows whose record raced past accepting,
  so their owed mark_accepted/classify-done step always completes.

* fix(managed-agent): close the round-3 seams the second rig and bot proved

The re-verified real stack and the bot's CHANGES_REQUESTED round found
the rest of the same family:

- The bundled notification still wedged: the bound measured the XML
  text, but publication JSON-serializes the envelope — and the wake
  turn's own message envelope would blow next. Bound now: packaged
  JSON bytes against a 48 KiB budget (the design's summary, never the
  whole answer), computed over the multiline-friendly stripping that
  keeps newlines instead of gluing lines.
- Refuse a foreground agent call when the parent turn already holds the
  Workspace mount or the batch has a non-agent sibling — the deadlock
  cannot start rather than burn to the deadline.
- A redriven (recovery-continuation) turn consuming child results now
  drains the same per-id consumption flush; previously its declare-only
  path would silently pass the queue to the next unrelated turn.
- Discovery resurfaces also the watching arm behind consumed/accepted
  deliveries, and deferral/leased rows no longer occupy the bounded
  fleet window ahead of real due work.
- classify can't be rewritten by a stale claimant (owner guarded in the
  WHERE), and the Runtime binding probe answers only live READY rows —
  newest READY, not the lexical max across inert generations.
- A done child's close admission runs before classify(done): the row
  stays owed and retried until admitted, so a falter keeps the close
  recoverable rather than an invisible ACTIVE orphan.
- Gradle-proof witnesses everywhere the round was blind:
  RuntimeBrokerConfigurationTest pins the bean's existence, and
  RuntimeBrokerServiceBindingReadTest pins the READY-only filtering.

* fix(managed-agent): answer the bot's full round review, first batch of fills

Review round answered inline by thread:
- Replay halves of the funnel locked into the one serialization:
  settleCompleted (guard + publications + commit) and accept (check,
  notification, commit) can no longer mint on a racing re-drive.
- Notification text: multiline strip with code-point slices (no glued
  lines, no unpaired surrogates), bounded by packaged JSON bytes under a
  48 KiB budget per the design's summary shape.
- Foreground agent already refused while the mount is held or the batch
  shares with a non-agent tool — deadlock never enters v1.
- Discovery pulls the watching arm when consumed ran ahead; deferred and
  leased rows no longer squat the bounded window; classify is owner-
  guarded; pending discovery also serves the watching arm behind a
  committed acceptance.
- Binding probe answers only READY (newest READY), never the
  lexical-max across inert generations.
- Tool-arm completion witness pins the no-notification shape; coordinator
  cascade test now asserts ordered sequences and the derived dispatchId.
- New witnesses: discovery columns, consumed pre/post-acceptance halved,
  child_agent wake intake, config bean pins, binding-read, authority gate
  commit, multiline/JSON size bounds.
- RuntimeBrokerService delete-safe javadoc row and creation-key delegation;
  docs: six ops belong to managed-child-operations, decision 5 sent-arm
  pinned as the bounded summary + complete acceptance, and the plan says
  the reverse gate is pinned at authority/store level in both languages.

* fix(managed-agent): refuse a malformed accept-notification as 400

Present-but-wrong-shape notification values were silently dropped by
`object()`, so an accept op arrived with nothing. A non-object or a
missing-description notification is now a typed refusal instead of a
silent no-op.

* fix(managed-agent): expect V51 in the workspace migration golden list

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

* fix(managed-agent): answer the round-4 review and the R1-5/R1-9 recovery gaps

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

* fix(managed-agent): pin the child terminal-result query's two decisions (R1-90)

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

* fix(managed-agent): recheck mount ownership after hook side effects and settle the fixture wake before close

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

* fix(managed-agent): translate the oversized launch envelope into its byte_limit refusal (R1-8)

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

* fix(managed-agent): close the R1-round's deferred Criticals and pin their regressions

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

* fix(managed-agent): fire PostToolUse for admitted agent launches (R1-62)

* fix(managed-agent): keep the give-up's close-and-settle debt alive and read durable dispatch evidence on resume

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

* fix(managed-agent): renumber the H4b migration to V52 past main's V51 lifecycle

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

* fix(sdk-java): give the unconfirmed lifecycle-detach heartbeat room on busy runners

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

* fix(managed-agent): reconcile attach from the parent record at give-up and admit claimed child cleanup past the lifecycle fence

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

* fix(managed-agent): give the fence its claim kind and keep stale authority out of the client

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

* fix(managed-agent): authorize claimed child cleanup through the lifecycle commit gate

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

* fix(managed-agent): settle Shell-lane parents and children without Workspace close

SessionLifecycleService admits protocol 1 only for the Files profile;
the Shell lane keeps the protocol-0 close whose child cascade passes
ordinary authorization, so a H4b parent close/delete no longer ends
RECOVERY_BLOCKED workspace_lifecycle_profile_unavailable.

ChildResultRelay no longer holds a parent's settlement hostage to a
close the host cannot dispatch: without Workspace-close support the
relay still settles failed and over-bound child records and retires
the ledger row instead of retrying forever.

The harness-session route test now pins the lifecycle authority
stamped on the Store client at verb-commit time.

* fix(managed-agent): retain the child-close obligation as a discoverable close debt

On a host that cannot close Workspace Sessions at all, retiring the
relay row alongside the parent settlement discarded the only durable
record that still owed the child Session its close: the child stayed
ACTIVE with no ledger scan and no close cascade able to find it again,
even after close capability returned.

The settlement now still commits on time, but the row parks as
close_debt — claimed, named by the child id, due on the heartbeat, and
surfaced by the discovery page's cancelled arm — until a capable scan
admits the close and only then retires. A parked debt outlives the
orphaned early-out and never re-enters the give-up chain, whose
settle-and-classify already happened for this row.

* fix(managed-agent): renumber the child lineage relay migration to V53

#13163 took V52 for the mutation-attempt sequence while the branch was
in flight, so the merged tree carried two V52 migrations and Flyway
refused every context. The golden-list IT now expects 48-53 and passes
against a live MySQL on the merged migrations; the module's 1439 tests
pass with the renumbered layout.

* fix(managed-agent): reconcile the close obligation before orphaned classification

The settlement of a child run can be committed while its close_debt
ledger write never lands — a crash or a lost reply between them. If
the parent then closes or deletes before the next scan, the settled
task is already invisible to the close cascade's live-scope walk, and
the orphaned classification erased the ledger row that was the only
durable holder of the owed close: child ACTIVE, zero CLOSE operations,
zero future discovery.

The non-ACTIVE-parent branch now reconciles before it classifies: a
settled delivery whose child Session still stands (row id or committed
lineage, short of CLOSED/DELETED) parks as close_debt instead and
discharges through the ordinary admission. Unsettled rows classify
orphaned exactly as before — the cascade owned their stop.

* fix(managed-agent): read the close capability once in the give-up chain

The admission order and the retention flag each re-read closeSupported();
a TOCTOU flip between the two could mark the debt settled without ever
admitting the close. One cached read now feeds both.

* test(managed-agent): pin the give-up chain to a single capability read

Sequencing closeSupported() true-then-false must still admit the close
and call hte accessor exactly once — the TOCTOU window between a double
read is what lost the debt silently in the first place.

* docs(managed-agent): renumber the design's migration cites to V53 and record the close debt

The renumbers (V51 took main's lifecycle, V52 #13163's mutation attempt)
left both language versions citing a migration id that no longer exists,
and the close-retention machinery added since the doc landed had no
ledger description. Both versions stay synchronized: V53 everywhere plus
the close_debt / orphan-reconciliation paragraph in decision 8.

* fix(managed-agent): retain the close obligation at task settlement, from every intermediate ledger state

Task settlement leads delivery settlement: the run record settles and
projects task completed while delivery still says accepting, so the
close cascade's live-scope walk never sees it — the retention predicate
must read task-settled evidence, not terminal delivery alone.

The discovery page's settled arm now admits every non-terminal ledger
state too: a give-up whose settle committed but whose close_debt write
failed sat in binding, outside the old arm-2 list, permanently
undiscoverable even before a parent close. Arm 2 now mirrors arm 1's
terminal-only exclusion; the in-flight work sets stay disjoint by
delivery, and the due/lease predicates still own eligibility.

* fix(managed-agent): never re-enter the walk for a settled-cancelled record

Widening arm 2 to every non-terminal ledger state made the settled arm
discover a creating/binding row whose create-or-fail reply or terminal
classification write was lost. On the next scan work() replayed the
record into create() and actually launched a child already recorded as
creation_failed / not_started_proven.

The walk now reconciles such rows instead: a standing child owes exactly
its close (parked close_debt, ordinary discharge), and with none the row
classifies unknown. The settled arm keeps reaching them; only the
creation/execution arms close.

* fix(managed-agent): keep retained close debts from starving new child work

On a host that cannot close Workspace Sessions, every finished child
parks a close_debt row due again every five seconds, and the discovery
page ordered by created_at always served those oldest rows first —
about 150-200 delegations in, the freshest launch is never reached and
no new child Session is ever created (real-stack evidence on the macOS
rig).

Breaking the starvation breaks nothing of the retention: due debts now
sort after every other owed row on the page, and a host that cannot
discharge a debt at all revisits it every 300 seconds instead of every
heartbeat — nothing could land before a restart returns the capability,
and a capable scan still discharges on its very next visit.

* fix(managed-agent): fence relay creation recovery against settled evidence at every seam

The same lost-write interruption can hide behind three seams, and each
authorized the wrong outcome on its own snapshot:

- The claimed walk read the discovery page's captured delivery. A record
  settled between the page and the claim retired or relaunched on the
  stale snapshot; retirement authorization now re-reads the committed
  projection (falling back to the page only when the row is gone).
- A stalled creator whose relay lease lapsed minted the Session anyway
  after another worker's give-up committed creation_failed and retired:
  the creation admission never read the run's terminal verdict. It now
  fences that mint inside its own transaction with FOR UPDATE on the
  run's task line, so a settled record refuses and unanswered replay
  still answers.
- A give-up with a lost relay session id resolved no attach and read
  no lineage: it claimed creation_failed while a real child ran. The
  child now resolves from the committed lineage before the proof, and
  a vetoed attach replay consults the child's own committed Turn
  before started=false may be claimed.

* fix(managed-agent): never admit children queued behind a cancelled wait

A batch whose foreground wait was abandoned by cancellation returned
control to the batch loop, which then admitted every agent call queued
behind it — work the cancelled turn never started, including a
successful 'started in the background' answer. The admission gate now
checks the abort signal before describing, bounding or stamping
anything; keeping an already-launched child running stays documented.

* fix(managed-agent): close the launch-admission edges codex flagged behind the quota line

Three adjacent admission edges all authorized against the wrong party:

- Quotas counted the replayed child: a re-driven batch whose siblings
  filled the active run quota got count_limit for the launch it was
  already running. The record lookup now precedes the quota verdict,
  and children.admit's replay identity answers as before.
- The background workaround was recommended where it deadlocks: a
  Session-owned mount (Hook catalog or MCP owner) survives the Turn
  and releases only at Session close, so a background child launched
  under it parks on workspace_busy. The background arm refuses there,
  and only turn-sourced holds keep the foreground hint.
- A wake turn's id embeds its commissioning run id, so chained
  background launches grew one suffix per hop into the 128-char
  lineage bound by the third hop. The launch key from a wake turn is
  now a sha256-truncated digest of the same stable name — replay
  deterministic, hop-bounded, and lineage-parser-legal at every hop.

* fix(managed-agent): never let a verdict race past a still-committing settlement

Two seams in the same family both read pre-collapse evidence and then
acted durably on it:

- Between the walk's read of the projection and the orphaned
  classification commit, another worker's settlement can still land —
  and the classification would orphan the child that settlement just
  made owable. The verdict now locks the record row FOR UPDATE inside
  one transaction for the re-read: losing that race only says the truth
  settled, and exactly one fresh evaluation assigns the close debt
  instead of retirement.
- A give-up's  verdict committed on the no-child proof
  while the stalled creator resumed and minted between the proof and
  the failure commit. At the commit seam, the lineage is re-read: new
  child evidence arrives after the proof owes one more bounded wait,
  never the confidently wrong pairing.

* fix(runtime-broker): read every page for the newest READY binding

findLatestBindingByHarnessSession returned from inside the first page
that held any READY row (and advanced the keyset cursor only when it
held none), so a greater-id READY binding on a later page was never
considered — the live Runtime identity for a child's first controlled
binding could pin a stale generation on generation-lite bindings. The
whole bucket now traverses, keeping the greatest READY id seen, and
returns after the traversal ends.

* fix(managed-agent): retention defaults on the standing child, never the racing verdict

A classification now orphaned only children that no longer stand —
whichever side wins the settle-vs-verdict race, a child that still
owns its Session keeps its one discoverable holder, ending the
orphan-burial shape in both directions at once (forward settle-then-
verdict and inverse verdict-then-settle). The atomic
classify-when-unsettled from R20 folded away in favour of the simpler
default; orphaned remains for the child who genuinely stands no more.

* fix(managed-agent): stamp out three launch-side certification gaps codex pinned

- A non-plain-object lineage field on session create now knows it:
  arrays (which object() maps to null) no longer stand in for accepted
  ancestry; only absent/null lineage is root. Anything else is the
  debuggable 400 invalid_hosted_lineage, never a 200 root that carries
  a malformed ancestry silently.
- The restarted-wake-turn recovery probe asks the durable record about
  child dispatch under the same collapsed key the launcher wrote —
  acceptChildAgent's wake-turn key derivation is now shared with the
  PostToolUse re-query (sha256-truncated digest of  identities), so resume finds it instead of losing
  ded though no test pinned its wake shape before.
- A lifecycle claim stamped by the children/operations middleware now
  restores on EVERY exit of the route — including the early-refusal
  arms (invalid operation, unavailable profile, blocked session) that
  previously ran before the try and leaked the claim onto the shared
  store client for the rest of the process where a future claim's
  generations match no longer comparison.

* fix(managed-agent): prove close debt by fact and address the fence by key

Two avoidance-class truths respected:

- A close debt whose child no longer stands is discharged by fact,
  not retried by suspicion — dischargeCloseDebt now re-reads the
  child's status before it admits, so an already-closed/deleted child
  never faces a permanently-refused admission, and an unproven-null
  read does not discharge against an unconfirmed child that we know
  exists. Default mock fixtures now answer ACTIVE for a child whose
  recovery would need it.
- The child-creation terminal-truth fence addresses the parent's real
  launch row by primary key (session scope + record key, the same key
  the idempotent admission derives), so its FOR UPDATE never becomes
  a cross-tenant table scan against MySQL's default REPEATABLE READ.

* fix(managed-agent): settle close-road blockers by evidence, not retry or wire-code faith

Three segments, each adjudicated prior to code:

- On a host that cannot close Workspaces, the close cascade's child
  close admission is fake-owed, and the delivery taxonomy gets a plain
  IllegalStateException it cannot file — the parent retries forever
  under an unnameable retry instead of the typed blocked code it
  already uses when the cascade had nothing to do. On such a host the
  admission is simply skipped, and the parent close proceeds into that
  typed gate.
- A lineage-named child with no body facts, no ledger creation key
  and no READY binding has no replay source: that evidence settles as
  creation-never-attached with started: false once, not an eternally
  re-armed debt on empty proof.
- A give-up's creation_failed verdict is now proved off the record's
  own transition field: intent (legally holding no attach or past
  dispatch successors) is answered never-started were nothing physical
  contradicts; dispatch_started or past is answered started; a
  physically-ran child with an intent record whose binding cannot
  name its dispatch answer gets deferred instead of invented verdicts.

* fix(managed-agent): let a never-started verdict name its minted Session

The create→attach window leaves exactly one durable fact — the minted
child Session's lineage — and the terminal revision that retires such a
run could never record it: the validators required the Session to stay
null until a dispatch admitted it, to have a Runtime binding, and for
creation_failed in particular to be anonymous. A retired run then left
its mint behind with nothing able to discover it again.

The contract now lets a not_started_proven settlement carry
childSessionId with no binding — bounded to that one terminal, the only
state with no dispatch to host from. The fail/close_scope operations
accept an optional childSessionId, the builders restate the same id
replay-safely and refuse a rename with child_operation_record, and the
shared fixtures pin the newly-lawful shapes in both languages.

Pins: core record/operation suites (190) replay the amended fixtures
and the naming/rename rules; the harness-session route pin drives the
wire contract end to end (named 202, replay 202, rename 409, malformed
400). Prettier, ESLint and tsc clean in cli and core.

* fix(managed-agent): arbitrate terminal verdicts against the mint at commit

R22's give-up wedge: executionState read the runtime_state projection
(unbound/provisioning/ready) while the caller compared execution enum
values, so intent and dispatch-started rows never matched — every row
looked started, the refused started verdicts repeated forever, and the
owed work never retired. The proof now reads the committed record body
through its inline resource; an unreadable body owes a bounded retry,
never a guessed verdict, and a self-proven not_started_proven execution
pairs the unstarted verdict directly.

R21's inverse race (a give-up verdict evaluated over an empty lineage,
then the mint lands) closes at commit time: ingestion of any
not_started_proven terminal revision locks the run's own row FOR UPDATE
— the same row the creation fence reads — and requires the verdict to
name exactly the Session the lineage proves minted, or refuses the
commit. The refused verdict defers the relay, whose retry re-reads the
lineage and names the child; every give-up verdict now names its child
whenever the child is known.

Mutation witnesses, all reverted byte-identical afterwards: a
runtime_state read fails the execution-body pin (an intent fixture
answers unbound); a no-arbitration gate fails the two refusal pins and
passes the two admission pins; dropping the verdict's naming fails the
relay's intent-family naming pin.

Suites: relay 36/36, relay store 13/13 (new execution-body pin and the
never-started pairing), verdict-reconcile decision table 4/4, creation
fence 3/3, and the full managed-agent-server verify green (checkstyle,
SpotBugs, 117 suites).

* fix(managed-agent): bind the close cascade's started call to any-state evidence

A creation key is an attempt id, never a start, and a READY binding's
absence says nothing — R22 reproduced both halves at this head: the
cascade wedged a parent in CLOSING behind a ledger-known child whose
binding had retired (the repair demanded warmth the retired row could
never offer again), and it committed not_started_proven over a child
whose own Turn had durably completed. A start is now proven by the
body's committed dispatch facts, by a binding row in ANY state (the new
findLatestBindingByHarnessSessionAnyState — a retired RELEASED/LOST row
still proves its dispatch and names the repair's identity, while the
relay's repair-for-warmth read stays READY-only), or by the child
Session's own durable Turn.

With none of the three, the never-started pairing settles started:false
and names the Session the lineage minted; with any, the repair rebuilds
dispatch and attach from that evidence and the settlement commits
started:true. The READY-read Javadoc is also reworded to name the rule
the method implements (the greatest READY binding id across the full
traversal; generation is not consulted), closing the R1-16 follow-up
note that the old comment argued against the rule.

Mutation witness, reverted byte-identical afterwards: dropping the
binding/Turn evidence from the started call fails both new cascade pins
with the exact R22 shape (started=false over proven execution). Suites:
coordinator 11/11 (two new reproductions plus the named unstarted
baseline), broker binding-read 2/2, and the managed-agent-server
verify green with the broker chain reinstalled. Design amended in both
languages.

* fix(managed-agent): close the arbitration on the production topology, the retired binding, and the pre-admission Turn

R23 reran the preserved schedules on the arbitration head and broke each
side of it in one class apiece:

- The verdict/mint seam reopened on MySQL 8.0.46 REPEATABLE READ: the
  committing transaction's snapshot was established by its ordinary reads
  long before the verdict's row lock, so a plain lineage SELECT missed a
  mint committed after that snapshot — the unnamed never-started verdict
  still committed, exactly the interleave the seam exists for. The
  lineage read now runs FOR UPDATE as well: a locking read always sees
  the latest committed data on either InnoDB default, and takes the same
  extension-then-session lock order the mint uses. Witness on live
  MariaDB REPEATABLE READ with two connections: a plain read goes RED
  ("Expecting code to raise a throwable" — the gate accepted the mint-
  after-snapshot verdict), the locking read stays green; the H2 decision
  table is unchanged. The temporary MariaDB used for the witness is torn
  down after the run.
- The give-up's physical-child repair required READY, so a child whose
  binding retired (RELEASED) before the record recovered wedged the
  attempt budget behind an answer that could never come — no failure, no
  close admission, no settlement. Terminal reconciliation needs the
  historical dispatch identity, not warmth, so the repair replays from
  the any-state read too. The relay-side pin drives dispatch/attach/fail
  off a retired row whose READY answer is empty; its RED mutation witness
  (READY-only lookup) leaves the same row parked in binding, exactly the
  repro's shape.
- A Turn no longer proves a start by existing: only its G3 dispatch pair
  (submission mark or harness epoch) does, mirroring the cancellation
  store's own predicate. A Turn the coordinator failed before warm-up or
  submission is a pre-admission failure and settles the named
  never-started pairing instead of sending the repair after a binding
  that never existed — R23's parent-CLOSING wedge, pinned in the relay
  (creation_failed settles named, no repair attempt) and the cascade
  (close_scope started:false named, parent completes). The RED mutation
  witness for both qualifications (dropping the dispatch pair) wedges
  all three pins exactly as reproduced.

All mutation witnesses were reverted byte-identical (sha256/cmp checked).
Suites: relay 38/38, relay store 13/13, coordinator 12/12, gate 4/4, and
the full managed-agent-server verify green. Design amended in both
languages.

* fix(managed-agent): fence the never-started verdict against runnable Turns, and keep its refusal rollbackable

R24's two probes held against the arbitration head, both in fresh code
from this branch:

- A live ACCEPTED child Turn waiting for its coordinator shares the
  undispatched shape with the terminal pre-admission failure, so the
  give-up's new dispatch-pair qualification minted creation_failed over
  a Turn the coordinator then claimed, submitted and completed (the
  close-incapable host has no admission guard to catch it). The relay
  now defers bounded on any Turn that neither dispatched nor terminally
  failed pre-admission; only the terminal undispatched half settles the
  named never-started pairing. Pin: an enqueued ACCEPTED Turn sends
  nothing and parks at binding until the admission settles FAILED, when
  the named verdict commits; its mutation witness (deducting the
  qualification) fires the verdict at once, exactly the reproduction.
  The close cascade needed no sibling branch — the active-Turn close
  admission already continues past the same window there, and the
  terminal truth settles on the next walk (verified at coordinator
  12/12 without touching its flow).
- The gate's 409 reached the Hosted writer as a generic record
  rejection, which the HTTP client's refusal classification did not
  recognize: ManagedSessionAuthority latched writeFailure and the
  relay's corrected named retry could never commit on the resident
  parent. The gate now answers its own child_run_lineage_minted code,
  and the client maps it to ManagedSessionCommitRejectedError — the
  rollbackable non-commit class the authority rethrows without the
  latch. Pinned at all three levels: H2 decision table and the MySQL IT
  assert the exact wire code, and the TS client suite drives a one-shot
  409 into a typed rejection (writesStopped false) followed by the
  corrected retry committing (two attempts, one landed); its mutation
  witness (mapping removed) rethrows a plain HttpError.

Witnesses were reverted byte-identical (sha256/cmp). Suites: relay 39/39,
store 13/13, coordinator 12/12, gate 4/4, TS http store 65/65, full
managed-agent-server verify green. Design amended in both languages.

* fix(managed-agent): treat the withdrawn submission mark as evidence-neutral, never as non-admission proof

R25's probe held against the intent reconciliation: G3's
withdrawSubmissionAttempted after a lost admission reply leaves the Turn
CANCELLED with neither dispatch mark nor epoch — the identical field
shape as a genuine pre-admission failure — while the historical binding
(READY → retired) remains. The terminal-shape check certified that as
never-started, converting an unanswered admission into a proved
non-admission: the relay then committed creation_failed over a child
whose admission outcome nobody had disproved.

The rule is now ordered by what can actually prove the verdict. An
outstanding Turn (live, neither dispatched nor terminal pre-admission)
always owes the bounded defer; the proven pair or the historical binding
row takes the dispatch replay with the same identity — a terminal status
never upgrades G3's reset to a proof. Only their joint absence plus a
terminal pre-admission failure settles the named never-started pairing.
The legacy space closes the same way it always did: no Turn, no binding,
no pair.

Pin: a CANCELLED Turn with no marks drives the dispatch/attach replay
from the binding's identity and settles child_failed named, never
creation_failed. Mutation witness (dropping the binding from the
dispatch-evidence disjunction) fires the never-started verdict in the
exact repro shape and is reverted byte-identical afterwards. Suites:
relay 40/40, store 13/13, coordinator 12/12, gate 4/4, full
managed-agent-server verify green. Design amended in both languages.

* fix(managed-agent): renumber the lineage relay migration to V54 after main's V52/V53

The uniqueness gate flags the duplicate version: main landed
V53__managed_workspace_roles.sql after the last incorporation, so the
relay's V53__managed_child_lineage_relay.sql collides with it. Same
remedy as the V52 collision: the ledger moves to V54, its two code
comments and the migration-rank golden list (WorkspaceMigrationMySqlIT,
now "48".."54") move with it, and both design languages name V54 where
they named V53.

Verified green at full depth: runtime-broker + qwencode reinstalled
chain-first (the shared ~/.m2 alpha jar had been overwritten by a
sibling worktree's main build, which lacked the any-state read), the
stale target/classes copy of the old V53 file removed (Maven's
incremental resources copy otherwise ships it alongside the rename and
re-applies the whole ledger as a duplicate version), and the full
managed-agent-server verify passes with 117 suites, no failures.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-09 06:04:29 +00:00
易良
d735e20f21
test(managed-agent): tolerate scheduling slack in the detach heartbeat window (#13658)
lifecycleDetachStopsHeartbeatOnlyAfterConfirmedAbsence asserted that two
heartbeats arrive within 200ms after an unconfirmed detach. On loaded
macos-latest runners the 20ms fixed-delay heartbeat chain can stall past
that window, so the continuation side flakes while the stopped side
keeps passing. Keep the 200ms no-heartbeat window for confirmed detach
and give the continuation side 5s.
2026-10-08 07:35:30 +00:00
jinye
fe4d4e345c
feat(managed-agent): add reliable ACTIVE Workspace deletion (L3) (#13354)
Some checks failed
pnpm store producer / Save pnpm store (push) Has been cancelled
pnpm Worktree Smoke / Install (macos-latest) (push) Has been cancelled
pnpm Worktree Smoke / Install (ubuntu-latest) (push) Has been cancelled
Verify Landlock helper / x64 (push) Has been cancelled
pnpm Worktree Smoke / Install (windows-latest) (push) Has been cancelled
Verify Landlock helper / Native Linux CLI boundary (push) Has been cancelled
Verify Landlock helper / Native Windows settings and streams (push) Has been cancelled
Verify Landlock helper / arm64 (push) Has been cancelled
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Flyway migration version uniqueness (push) Waiting to run
SDK Java / Runtime Broker and Managed Agent MariaDB / Java 21 (push) Waiting to run
SDK Java / Hosted process fault gates / MySQL 8.4 / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(managed-agent): add reliable ACTIVE Workspace deletion

* fix(managed-agent): align lifecycle CI fixtures

* fix(hosted): authorize detach after durable lifecycle recovery (#13354)

* fix(hosted): scope lifecycle fence locks and authorization errors

* fix(managed-agent): resolve L3 migration version collision (#13354)

* fix(managed-agent): address PR review feedback (#13354)

* fix(managed-agent): address PR CI fixture failures (#13354)

* codex: address PR review feedback (#13354)

* codex: address PR review feedback (#13354)

* fix(managed-agent): preserve main credential and migration contracts (#13354)

* codex: address PR review feedback (#13354)

* fix(managed): restore original lifecycle Hook owner before dispatch

* codex: address PR review feedback (#13354)

* test(managed-agent): repair lifecycle receipt journal fixture

* fix(managed-agent): fail closed on stale lifecycle capability client

* fix(managed-agent): align native lifecycle test contracts

* codex: address PR review feedback (#13354)

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-10-08 03:06:10 +00:00
Shaojin Wen
fcc366427d
test(managed-agent): harden pinning witnesses and add renewal-arm pinning witness (#13388 follow-up) (#13401)
* test(managed-agent): harden pinning witnesses and add renewal-arm pinning witness (#13388 follow-up)

The #13388 pinning witnesses sized their caller fleets with
ForkJoinPool.getCommonPoolParallelism(), which drifts from the real
virtual-thread carrier count whenever jdk.virtualThreadScheduler.parallelism
is overridden — the witness then parks fewer callers than carriers and the
red state can no longer form. Read the carrier count from the same property
the virtual-thread scheduler reads instead.

Also add the BindingRenewal-arm sibling witness: carriers+2 virtual threads
each warm their own durable binding and park inside the renewal guard at a
latched first resource-handle write, while an unrelated virtual-thread probe
must still run. Restoring the synchronized BindingRenewal turns it red
(probe starves, callers never all arrive) while the SessionContext-arm
witness stays green, and drop the no-op assumeTrue(true, ...) triage nit
from BrokerVirtualThreadPinningTest.

Refs #13333

* test(managed-agent): read carrier count base-10 and land the renewal witness on its probe assertion (#13401)

* chore(managed-agent): re-arm verification after clearing stale JaCoCo lint residue (#13401)

The previous commit was rejected at npm run lint: its own mvn clean test
runs left gitignored JaCoCo HTML reports (jacoco-resources/*.js) under
packages/sdk-java/{runtime-broker,qwencode}/target, and the repo-wide
eslint sweep (flat config does not honor .gitignore) linted them — 278
errors, all generated files. No committed file caused the failure and no
in-footprint change can prevent it: the durable one-line ignore for Maven
target/ output belongs in the workspace-root eslint.config.js next to the
existing packages/desktop/src-tauri/target/** entry, which the round
boundary reserves for maintainers. This empty commit preserves the
rejected fix unchanged and re-arms deterministic verification on a tree
with the build residue removed (mvn clean equivalent); npm run build,
npm run typecheck, and npm run lint all pass on it.

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

* test(managed-agent): carrier-size the session witness latch and pin the base-10 carrier read (#13401)

* chore(managed-agent): re-arm verification after clearing regenerated JaCoCo lint residue (#13401)

The previous commit was rejected at npm run lint: its own mvn test runs
regenerated gitignored JaCoCo HTML reports (jacoco-resources/*.js) under
packages/sdk-java/qwencode/target, and the repo-wide eslint sweep (flat
config does not honor .gitignore) linted them — 139 errors, all in
generated files. No committed file caused the failure; the committed
fixes are preserved unchanged. No in-footprint change can prevent the
residue: the report goal is bound to the test phase by the module poms,
and the durable one-line ignore for Maven target/ output belongs in the
workspace-root eslint.config.js next to the existing
packages/desktop/src-tauri/target/** entry, which the round boundary
reserves for maintainers. This empty commit re-arms deterministic
verification on a tree with the build residue removed; npm run build,
npm run typecheck, and npm run lint all pass on it.

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

* test(managed-agent): align carrier reads with the JDK and pin the scheduler lane (#13401)

Round-3 review follow-up on the pinning witnesses:

- CarrierCount and the qwencode twin claimed to read the scheduler
  property "the way the JDK does" while trimming, catching
  NumberFormatException, and clamping — three things the JDK's bare
  Integer.parseInt in VirtualThread.createDefaultScheduler never does
  (verified against javap on OpenJDK 21.0.12.1). On any malformed value
  the JVM dies at scheduler init, so the lenient branches were
  unreachable in any JVM that can run a witness. Both copies now read
  exactly like the JDK; the trimsBeforeParsing and "abc" assertions
  become assertThrows(NumberFormatException) pins of the measured
  behavior.
- The carrier-sized arrival latch opens with two callers short, and no
  surviving assertion noticed a caller blocked before the guarded call
  (measured: wedged suite green in 61 s). Both broker witnesses now
  assert every caller finished after the join, restoring the lost proof
  (same wedge red in 31 s).
- Nothing in CI set jdk.virtualThreadScheduler.parallelism, so the
  carrier sizing was unobservable and a revert stayed green. A second
  surefire execution now re-runs the three pinning witnesses with
  -Djdk.virtualThreadScheduler.parallelism=4 on the fork argLine
  (@{argLine} keeps the JaCoCo agent; combine.self=override keeps the
  fault-gates profile's groups filter from emptying the lane). Reverting
  the sizing reds both broker witnesses there in 60 s (waiting=6); head
  passes in ~1 s each.

Mutation probes: Integer.decode swap reds both base-10 pins
(expected: <100> but was: <64>); semaphore wedge reds on the new
liveness assertion and greens without it; sizing revert reds the new
lane. All reverted; the committed tree is green everywhere.

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

* fix(managed-agent): gate the pinned-scheduler witness lane on no -Dtest filter (#13401)

* chore(sdk-java): re-present the tree on a lint-clean checkout (#13401)

Same-run verification repair for 5eab7d39b1. The deterministic gate
rejected that commit because npm run lint linted untracked, git-ignored
Maven target/ directories (JaCoCo and javadoc JavaScript under
packages/sdk-java/*/target) that the round's own verification runs had
left in the shared checkout. No committed content caused the failure,
so this follow-up is intentionally empty: the target/ directories are
removed from the working tree and npm run lint, npm run build, and
npm run typecheck all pass on the otherwise unchanged tree.

* fix(managed-agent): stand the pinned witness lane down while a groups filter is set (#13401)

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

* test(sdk-java): drop the stale Candidate marker from the renewal witness comment (#13401)

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

* test(managed-agent): drop the pinned-scheduler lane, preserve wedged-caller diagnosis in both broker witnesses, size the subscriber witness correctly

Follow-up verification round on PR #13401, landing its four measured
review findings:

1. Drop both pinning-witness-lane profiles and their stand-down
   counterpart from the qwencode and broker poms, plus their pin test:
   across the measured matrix the lane goes red only where default-test
   is already red (unique kills 0), a correct guard makes fleet size
   unobservable at any parallelism, and it costs 165 pom lines, an extra
   surefire fork per module, and 4.6-12.1 s of CI per run. The JS pin
   case asserting their merge discipline goes with the profiles. (R1-3,
   author's call after the reviewer's measured analysis.)

2. Keep the primary starvation failure when callers wedge before the
   guarded write, and report the wedged callers as a suppressed error
   with a shared 30 s join budget instead of each caller's 30 s: the
   caller-liveness assertion inside the finally otherwise replaced the
   pending awaitArrived IllegalStateException outright, hiding waiting=N
   and the first caller's exception from the surefire report. Measured
   by the candidate in four cells (green at 1.28 s, mutant still red at
   ~31 s with the same probe-starvation message, 3 wedged callers yield
   waiting=9 + Suppressed: 3 caller(s), 2 wedged callers with a running
   probe still red at 31.38 s). Applied to BrokerRenewalPinningTest
   verbatim and ported to BrokerVirtualThreadPinningTest, which had the
   same finally shape. (R1-1)

3. Size SessionEventHubPinningTest's subscriber fleet from
   Integer.getInteger(

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>jdk.virtualThreadScheduler.parallelism,
   availableProcessors()) like its broker/qwencode siblings —
   getCommonPoolParallelism() under-sized the fleet on JVMs where the
   virtual-thread scheduler count diverges from common-pool
   parallelism. (R1-2)

R1-4 (one-line fork timeout insurance for the dropped lane) is moot
after the lane is deleted.

Verified: runtime-broker 738/0F, qwencode 185/0F, managed-agent-server
1077/0F + HostedConcurrentTurnBurstMySqlIT 6/6, checkstyle 0 on all
three modules, scripts/tests 32/32 (hosted-process-ci + sdk-java
workflow), and the BindingRenewal mutant still turns the renewal
witness red at 31.40 s with its starvation message intact.

Refs #13333

* chore(sdk-java): restore qwencode pom formatting to main's exact shape after the lane drop

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

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-autofix <qwen-code-autofix[bot]@users.noreply.github.com>
2026-10-07 19:49:47 +00:00
Shaojin Wen
4970bfa172
feat(managed-agent): adopt the next Hosted Harness generation (G3) (#13174)
* feat(managed-agent): adopt the next Hosted Harness generation (G3)

A Hosted Session no longer dies with the Harness process generation
that first served it (#12952 G3, Steps 1+2). On a generation signal
the control plane renegotiates once through the connector instead of
failing every bound Session until its own restart; pending Turns
re-attach through the takeover load as their retries come due.
Takeover refusals split into a typed terminal decline (deterministic
in the journal) and the existing retriable code; the takeover load is
idempotent against a lost reply and gets its own timeout; negotiation
requires the journal-contract feature token so a rolled-back Harness
is refused once; a marked-but-never-admitted Turn withdraws its
submission mark, backstopped by journal commandId idempotency.

The failover runner gains a Harness-only restart arm on all three
scenarios (harness_boot_id moves with no Java restart) and a
SIGSTOP/SIGCONT frozen former-owner fencing arm, wired into
hosted-harness-mysql together with the previously local-only basic
mode. Design docs included in English and Chinese.

* fix(managed-agent): tolerate the bootstrap 404 of a restarting Hosted Harness

The serve delegating app answers every routed path with a bare
pre-contract 404 while its runtime is still starting. The Harness-restart
arms raced that window: a prompt submitted milliseconds after the new
process bound the port could meet the bare 404, which the client read as
a generation contract violation and ended the Turn as
hosted_harness_protocol_error without any retry. A missing boot header on
404 now maps to a transient transport error so the coordinator retries
through the window, and the failover runner waits on deep health
(?deep=1) before driving turns into a restarted Harness. Terminal turn
failures now also log their underlying exception.

* fix(managed-agent): freeze only the Harness writer in the former-owner fencing arm

SIGSTOPping both original owners left the original Spring's workspace
binding unreclaimable: the reclaim proves death from /proc liveness, and
a stopped JVM still reads as alive there, so the replacement's reconcile
timed out (runtime_broker_reconcile_timeout) and the Turn never
completed. The arm now stops only the original Harness (reclaiming its
workspace binding requires the Spring to die anyway — the continuation
mode's crashProcess), completes the Turn with a full replacement, then
wakes the frozen writer and asserts the journal, transcript, and
session binding are untouched. Design D7 updated in both languages with
the discovered boundary: resource-level takeover of a surviving owner is
structurally out of reach today, which is exactly what this issue's
sequential-generation scope excludes.

* ci: retrigger sdk-java workflow after event delivery stall

* fix(managed-agent): pin the fencing proof on writer identity, not head revision

The first merge-base run of the freeze arm failed its own gate: after
waking the frozen Harness the journal head moved (rev 30 -> 45) while
the writer generation, boot binding, transcript, and terminal count all
held. The moved revisions were the replacement's own legal churn, not
old-writer mutation, so the head-equality assertion conflated the two.
The exit-check claim is identity: no transaction out of the old writer
generation after it wakes. Count that straight off
qwen_managed_session_journal_tx (which carries writer_generation), keep
the binding/transcript/terminal assertions, and align design D7 in both
languages. The error string now also reports the old-writer tx count.

* fix(managed-agent): qualify the journal tx schema in the freeze-arm gate

The identity-based old-writer count queried qwen_managed_session_journal_tx
unqualified; the runner's MySQL connection has no default schema, so the
arm died with ERROR 1046 instead of evaluating the fence. Qualify it as
qwen_managed_agent.qwen_managed_session_journal_tx like every other query
in this runner. No behavior change to the assertions themselves.

* fix(managed-agent): harden takeover correctness from the R1 review

- settle turn_settled checkpoints through the load route's own tail
  instead of a terminal decline, and keep transient store verdicts
  (missing_state/missing_checkpoint) retriable on both authorization
  reads
- exempt only takeover-shaped 409s from the pre-admission retry budget;
  every other failure kind meets it
- scope the 120s load-timeout to recovery loads so plain attaches under
  the CHM bin locks stay on 30s
- adoptGeneration: move map churn out of the connector monitor
  (lock-order inversion), evict only stale-boot entries on equal-boot
  mismatches
- answer a duplicate submit of an already-settled prompt with a 202
  replay anchored at its input.accepted sequence
- withdrawSubmissionAttempted: guard on the same status window as its
  peer CAS methods; complete capability mismatch terminally in both
  remaining coordinator layers
- runner: freeze-arm pre-freeze liveness assertion, /health-proven
  wake non-vacuity, teardown SIGCONT, exact-string dispatch-generation
  bounds
- CI: job ceiling 95 with corrected ceiling math; pin all seven
  failover arms in hosted-process-ci.test.js
- sync both design-doc languages with the shipped behavior

* fix(managed-agent): close the R2 review round on the replaceable-host-harness slice

- withdraw + cancel interference (R2-1, Critical): a CANCELLING Turn
  whose never-admitted mark was withdrawn is fast-cancelled instead of
  being re-dispatched into a second live execution at the adopted
  generation
- the generation-mismatch catch escapes the pre-admission budget only
  on a bound Session, like every sibling arm (R2-3)
- bound the remote-supplied decline reason to the error_message column
  so the terminal write itself never throws (R2-5)
- the last fail(...) in the catch ladder is failTerminally with the
  exception logged, like its siblings (R2-6); the withdrawal itself is
  logged too (R2-7)
- freeze arm: the replacement Spring inherits the original port so the
  woken writer meets a live fencing control plane, not a dead socket
  (R2-2); the wake block asserts the head actually advanced past the
  frozen owner's writer generation, drops the wrong heartbeat claim
  (R1-8 follow-up), separates the two disk-audit meanings, and asserts
  the retained Harness home (R2-8)
- the hosted /capabilities feature list derives from the registry
  instead of a hand-maintained literal (R2-9)
- docs: D7 scoped to the journal half of the Q2 exit check, the binding
  half and the pre-contract-404 classification recorded in Boundaries
  (R2-4, R2-10)

* docs(sdk-java): correct the load-timeout coverage comment in the client test

* fix(managed-agent): reference the frozen Harness through the children registry in teardown

The R1-4 teardown SIGCONT referenced the try-block-scoped `harness`
binding from the finally block, so every freeze-arm run died with
ReferenceError after all fencing assertions had already passed (CI run
37009843147). Reach the child through the children registry by its
start() name instead.

* docs(managed-agent): scope the adoption sentence to this slice's takeover reach

* fix(managed-agent): close the R3 review round at its five shared roots

- retract the turn_settled settle tail to a typed decline: the tail
  answered 200 with no recovery watermark (Java then failed the bind,
  orphaned a completed journal turn), fabricated state:'completed' on
  phase alone, ran mutating work under a passive load, and retained no
  snapshot for the lost-reply retry. D10 already owns
  rebind-and-keep-reading as the Step 3 row (R1-25-re, R3-2 .. R3-5)
- the load route's restore guard applies the same durable-vs-transient
  verdict read, because the bundle collapses both into one blocked bit
  and the durable decline was unreachable from production (R1-26-re)
- the pre-admission exemption keys on lease-shaped 409 codes only:
  configuration-shaped 409s (e.g. hosted_tool_profile_conflict) meet
  the budget, and the same 409 exempts nothing on an unbound Session
  (R1-1-re, R1-30-re, R3-13)
- equal-boot adoption keeps pendingRecovery: the markers were minted by
  the same live client (R1-29-re)
- lifecycle capability mismatch: revert completing-as-unconfirmed —
  that skipped the drain, flipped the session, and recorded a clean row
  with no FAILED vocabulary to tell it apart; the reason-loud retry is
  the honest end of the line until an operator realigns versions, and
  the README sentence says so. Action-response outbox keeps its honest
  FAILED-with-code terminal (R1-45-re, R3-5, R3-15)
- test hardening: concurrency close-once now surfaces worker assertion
  failures via Future.get and pins that no real client build happened
  (R3-12); loadTimeout-vs-requestTimeout discriminated at the call site
  (R3-9); single-walk acceptedInputSequence (R3-18)
- runner: 5-column execution queries pin runtime_session_id unchanged
  under --harness-only (R3-14); frozen-arm teardown looks the child up
  by a shared label constant and fails the run on a drift (R3-17)
- CI: ceiling 105 covers the nine summed step ceilings (92) plus the
  uncapped setup steps (R3-6); the seven-arm pin also asserts each
  script's flags and the install step's existence (R3-16)
- docs: both languages re-synced (D5 retraction, D9a code allow-list,
  D7 teardown, D8 runtime identity, honest validation digests, the four
  missing changes-table rows, README retry clause)

* fix(managed-agent): replay takeover snapshots only to a same-shape request

A passive load only asks broker.status(), so its snapshot keeps
phase='await_runtime'; replaying it to a drive load handed the
coordinator a checkpoint that isContinuationReady() can never accept,
terminally failing an otherwise continuable Turn. Record the request
shape with the snapshot and replay only on a mode match; the clear
conditions stay promptId-only.

Also make the adoption race test deterministic: a package-private
createClient() seam lets the test inject the replacement instead of a
real /capabilities call (proven flaky on CI), worker assertion failures
now surface through Future.get, and the pendingRecovery fixture seeds
its marker through the recovery path.

* fix(managed-agent): close the self-audit round on the replaceable-harness slice

Three rounds of undirected plus adversarial audit over the whole PR diff
(31 files), with three parallel deep-dive passes over the Java, TypeScript
and runner/docs surfaces. Every finding was re-verified against the tree
before acting; five reported defects were rejected on evidence.

Correctness:
- the load route read the blocked-restore verdict AFTER managed.close();
  sealing the journal makes every later store read fail as "writer is not
  active", which the authority erases into missing_state, so the durable
  branch this PR added could never fire and a corrupt checkpoint stayed in
  the retry-forever class. Read the verdict first, and pin the order with
  a witness that goes red when the read moves back (the local authority's
  seal does not disable reads, so only the ordering assertion catches it).
- drop hosted_session_already_attached from the lease-bounded 409 set: the
  daemon removes an attachment only on an explicit detach or delete and this
  control plane never detaches, so a Spring restart against a surviving
  Harness retried a permanent refusal forever instead of ending the Turn.
- adoptGeneration evicts by boot identity in both branches instead of
  clearing, so a marker minted by a concurrent rebuild can no longer be
  erased between two adjacent clears (which left a live attachment with a
  null recovery report), and markers follow their attachment.
- the post-withdrawal fast-cancel reads the row again: a cancel landing
  after the claim is invisible in the claimed snapshot, and acting on it
  submitted a live execution only to cancel it below.
- a durable verdict re-read after settling now declines as
  checkpoint_blocked, matching the identical pre-settle classification.
- an unrecognized blocked reason throws instead of declining, so a reason
  core adds later cannot silently become a terminal Turn failure.
- the operator-settable load-timeout is validated at connector
  construction; a bad value previously threw on every client() call and
  surfaced as an endless transient retry naming only the exception class.

Tests: already_attached and lost-withdrawal-CAS witnesses; the fast-cancel
witness now pins the re-read (cancel after claim); the concurrent-adoption
test surfaces worker assertions through Future.get and injects the
replacement through a package-private createClient() seam instead of
standing up a live /capabilities call; the CI pin gains each arm's mode
flag and a ceiling-sum invariant.

Docs (both languages): D1 now describes the boot-identity eviction and
lists what this slice really added to HostedHarnessClient; D3 no longer
claims the bind-refusal path stops being reached (it is D4's trigger); D6
records the same-shape replay clause; D7's fence rationale no longer blames
heartbeats (lease renewal touches writer_lease_until only); D9a names the
two exempt codes and why already_attached is not one; the validation digest
states which claims are pinned and which are not; the CI arithmetic reads
12 + 8x10 = 92; the stale workflow nit is dropped.

* chore(ide): regenerate NOTICES for the merged dependency set (email channel)

* fix(ide): replace the workspace-polluted NOTICES with the pristine regeneration

The file committed in f7a960c559 was generated under a polluted module
layout left by an aborted install: its resolution hoisted
is-fullwidth-code-point@5.1.0 and produced 663 total dependencies, so
CI's own regeneration (pristine: 662) mismatched and the up-to-date gate
failed. Replace it with the pristine worktree's output, which the gate's
own regeneration produces to a no-diff state.

* fix(managed-agent): close the R4 review finds on the takeover and refusal paths

- classify missing_checkpoint and a bare missing_state as durable verdicts
  so a permanently unrecoverable session declines instead of being retried
  past the lease window; only a missing_state carrying the erased store
  error's message stays retriable
- map a fenced predecessor's managed_session_writer_conflict to the coded
  retriable 409 in the load route so the wait provably ends with the lease,
  and allowlist that code in LEASE_BOUNDED_409_CODES (dropping the inert
  hosted_prompt_recovery_required, which can only arrive post-mark where
  the exemption gate is already bypassed); null-guard the membership test
  against codeless error bodies
- replay a takeover snapshot only to a request whose parsed store identity
  matches the attached session's, never to any takeover-shaped caller
- fetch the harness client AFTER resolving the attachment at the six entry
  points, so an adoption completed during a create/load round trip cannot
  strand a call on the closed client
- pin the reply watermark of the journal-replayed prompt admission, the
  construction-time loadTimeout guard, the marker-eviction half of
  adoptGeneration, and the withdrawal CAS ordering with witnesses, and
  widen the load-timeout test's durations so the capabilities negotiation
  keeps headroom on contended lanes
- narrow the README's typed-decline sentence to the surviving row and
  generation binding, and mirror the exempt-code list in both design docs

* fix(managed-agent): answer the round-5 waits without terminalizing them

- a takeover now splits three ways: drivable answers recovered, a
  deterministically unrecoverable drive keeps its typed decline, and
  anything whose wait belongs to someone else (a requested approval, or
  the whole non-drivable space on a cancellation-only load) answers
  inapplicable so the route falls through to the plain attach — a
  user-cancelled Turn cancels instead of failing as
  managed_runtime_recovery_blocked, and an approval wait survives its
  generation rather than dying while the answer is still deliverable;
  the coordinator keeps the await_action guard as a retriable path even
  if a Harness still declines it
- an unconfirmed cancellation re-drives with its snapshot restored: the
  cancel admission's rollback is now symmetric, so the retrying takeover
  load replays instead of being refused already-attached
- an unrecognised blocked reason at the final re-read stays retriable on
  the drive side like the pre-settle read, instead of terminalizing on
  unresolved_after_settle; only durable verdicts may end a Turn
- the hosted persona's curated tag set is declared once in the registry
  (hostedPersona descriptor flag) instead of being filtered by a third
  hand-maintained list at the route, pinned by the docs-contract test
- the harness-only arms pin a surviving worker through the runtime
  session row's own liveness metric, not the insert-frozen id column
- witnesses: declined reason on the wire, cancel-report replay after a
  lost answer, inapplicable shapes at the recovery kernel (both loads),
  the drive disjunct of the load-timeout predicate, the one rebuild per
  adoption race, and coordinator corners for cancelling/await_action

* fix(ci): qualify the runtime-session heartbeat query with its schema

The harness-only arms' liveness beat read qwen_runtime_session bare;
mysql runs without a default database, so the Harness-restart in-flight
arm died on ERROR 1046 with 'No database selected'. Every other probe in
the runner already addresses qwen_managed_agent.<table> — this one now
does too.

* fix(managed-agent): close the round-6 shape and wedge findings

- the pre-kernel no-tool refusal and the restore guard now honour the
  same shape rule the kernel does: a cancellation-only load answers
  inapplicable and attaches plain (or the baseline retriable refusal
  where no attach is possible) instead of terminalizing a Turn the user
  asked to cancel; a takeover that answers inapplicable no longer trips
  the bare-load refusal on its empty recovery
- the lease-bounded exemption is keyed on the writer lease's own wire
  code only: hosted_turn_recovery_required covers arbitrary takeover
  failures, and exempting it wedged durable refusals in an unbounded
  pre-admission retry; a refusal carrying that code now meets the budget
  like every other body shape while managed_session_writer_conflict
  keeps its D9a window
- witnesses: the no-tool plain-attach coincidence for cancel vs the
  typed model_start decline for drive, the durable restore verdict on
  the cancellation arm, and the budget-hit for a durable 409 on the
  bound path (the old takeover-409 premise rewritten, its writer-
  conflict twin already covered)

* fix(managed-agent): settle a cancel first on a plain-attached replacement

A real-stack round-4 wedge: cancelling a Turn whose Harness died mid
model wedge (Work realized after the merge) failed as
hosted_harness_generation_mismatch — the plain attach leaves the bind
CAS unwinnable (boot-old != boot-new), the cancel was never issued, and
everything queued behind the Turn then retry-forever: later prompts
409, a close 409, no public-API escape.

runClaimed's recurrence without recovery now cancels before it binds:
a CANCELLING Turn does not submit, and a bind exists for submissions —
the next Turn's takeover adopt moves the generation when something
actually continues. cancelledOnAttach suppresses the tail's later
re-cancel. The R5 witness now also pins: bindHarness is never asked,
withdrawSubmissionAttempted is never touched, and the Turn is neither
recovery-blocked nor generation-mismatched. Design docs D3/D4 gain the
bypass rule in both languages.

* fix(managed-agent): close the inapplicable aftermath the round-8 review found

- an inapplicable takeover must not latch session.blocked — a 200 answer
  that refuses every settlement route is a lie (R5-2's latch regression:
  nothing outside settleCancelledHookTurn clears it for a non-hooks
  session). The blocked latch now splits the same shape rule: only a
  genuinely unsettled takeover parks there, not an inapplicable answer.
- a plain attach of a bound Session after adoption rebinds the Session
  row with its own CAS (owner + live lease + expected = the row's boot),
  since bindHarness provably refuses a turn that already posted its epoch
  — the Turn is neither generation-mismatched nor stranded (R8-1).
- a Turn written turn_settled completes, so it is never stamped as
  managed_runtime_recovery_blocked: the kernel answers inapplicable on
  BOTH load shapes (the plain attach lets the daemon's own projection,
  not a declined verdict, write the terminal record), retaining the
  rebind-and-keep-reading rationale for Step 3 (R8-2).
- the #13388 pinning test's mock endpoint now advertises
  managed_session_journal_delta_v1 like every other Harness fixture of
  this branch — a merger's follow-up angry about the gate (D9c), not an
  upstream flake.
- witnesses: turn_settled pinned as inapplicable on both shapes at the
  kernel; plainAttachAfterAdoptionRebindsBeforeItSubmits asserting the
  store CAS adoption; R5-2's corner witnessing 200 without
  recoveryRequired; coordinator 46/46, hosted client + pinning tests,
  TS session+recovery fully green.

* fix(managed-agent): close the inapplicable aftermath the round-9 review found

- the no-tool arm now refuses with the baseline retriable one again
  instead of minting a plain attach whose parked Turn nothing may
  resolve (R5-2'), and a takeover load without broker options shares
  that refusal;
- the journal prompt replay answers 202 only when the request body's
  digest proves identity against the admission its input.accepted
  holds; carrying the same Id with different bytes is a real
  prompt_conflict, never a silent swap of watermark (R9-1);
- a plain attach after adoption moves the Turn's stream epoch through
  recordRecoveryAdmission to the attach's epoch, on both the binding
  ladder and the cancel-on-attach arm — a stream opened with the
  claimed epoch would be rejected on the new generation as epoch
  mismatch (R8-1').
- witnesses: the no-tool cancel arm refusing retriably while drive
  declines with its typed reason, the digest mismatched replay timing
  out as hosted_prompt_conflict, and R8-1 streaming the adopted
  generation's epoch on both arms (plainAttachMoves* and
  cancelOnAttachStreams*) with recordRecoveryAdmission pinned.

* deflake: carry #13403's pinning fixture under the journal-delta gate + harden the 13328 mount-holder waitFor

The merge of #13403 turned the Java CI lanes red with
DaemonProtocolException: Endpoint does not advertise
managed_session_journal_delta_v1 on every burst caller: this branch's
D9c capability gate (from #13388's pinning work) rejects endpoints
without that token, and #13403's new /capabilities fixture did not
advertise it. Same fixture-parity fix as #13388's — the token is a
fixture property, not production behavior; the deliberate negative
stub (capabilitiesJsonWithoutJournalToken) stays as the gate's
witness.

The ubuntu Test lane red on the same head was the issue-13328
mount-holder vi.waitFor defaulting to its 1 s window against the
admission pipeline on the loaded runner ("expected undefined to be
defined", repeated across both retries). Give it an explicit 10 s
timeout, assertions unchanged — the same deflake pattern as
#13411/#13323/#13430.

* fix(managed-agent): close the R11 review round (inapplicable pays only what it can)

R11 (qwen-code-ci-bot review, 5 threads, 3 new + 2 re-checks) landed on
the shared consequence of R10's fixes: an inapplicable/plain-attach answer
stays honest only where a settlement route can pay.

- R11-1: the prompt route's session-scope guard now answers the
  SESSION-level wedge code (hosted_turn_recovery_required); the
  prompt-scoped code names only the requested prompt's own unsettled
  duplicate, so the duplicate-admission adopt branch can no longer be
  forged by a refusal about someone else's parked Turn — and the budget
  treats the session-level code as pre-admission whenever the Turn
  provably recorded no epoch, recording the wedge's own code on
  exhaustion (provenance-gated on an existing submission mark).
- R11-2 / R10-1 re-check: both takeover arms compute the bare branch's
  settle projection inline (`settleProjectablePromptId`): approval waits
  keep the plain attach (resolve pays), turn_settled is settled by the
  load itself via the extracted `runSettleProjection`, and an unpayable
  park keeps the baseline retriable 409 instead of a healthy-looking 200.
- R10-2 re-check: the kernel's passive inapplicable is narrowed to those
  two payable states; initial / durably blocked / model-start / unknown
  phase / wrong-checkpoint throw into the retriable refusal.
- R11-3: createOrLoad's final attachment put restores the pendingRecovery
  marker for snapshot-carrying passive re-attaches, or the cached branch
  of a later recoverManagedRuntime answers "nothing parked" over a
  snapshot it holds.

Witnesses: 245/245 CLI across the three hosted suites (2 new route
witnesses + updated session-code assertion + 2 recovery-suite flips),
QwenHostedHarnessConnectorTest 34/34 (marker witness),
HarnessCoordinatorTest 52/52 (session-busy witness; R6 budget witnesses
pin the load side as recorded). Docs updated in both languages.

* fix(managed-agent): settle the no-tool cancellation takeover instead of refusing forever (Arm B)

The round-6 real-stack wedge: a Turn with no Runtime work parked mid
model round, its Harness generation dead, cancelled — the no-tool arm of
the takeover branch answered the baseline retriable 409 on every
cancellation-side redispatch, and with the Turn still carrying its
submission mark, the retry budget never fired. Turn 2 stayed CANCELLING
forever and every later Turn met the same wall.

The fix keeps the wire honest and the settlement explicit:

- LoadHarnessSession carries `cancellationTakeover`, set ONLY by
  recoverManagedRuntime's cancellation arm. A plain passive re-attach
  shares the passive wire shape and deliberately never sets it — nothing
  may mint a canned CANCELLED record for a wait whose owner could still
  exist.
- With the signal present, the no-tool arm of the takeover branch
  settles the ownerless park into the daemon's journal itself
  (turn_result/cancelled, landing turn.settled); the writer fence on the
  very load is the safety boundary (it proves the producing generation
  can never write again). On write failure the arm keeps the baseline
  retriable 409. Without the signal the arm keeps its merge-baseline
  answers — retriable 409 on cancel, typed model_start decline on
  drive — so the collision guard holds by construction.

Witnesses: `settles the cancellation of an ownerless parked no-tool Turn
on the takeover load` (200 plain attach + durable settle tag + cancel
reads settled-at-tail as 204 + next prompt admits; the revert flips the
verdicts red) with the collision-guard passive-only rerun still at 409,
plus wire-side `carriesTheCancellationTakeoverFlagOnlyOnTheCancellationLoad`
asserting the flag travels only with the signal.

Verification: TS hosted suites 259/259, qwencode 32/32,
QwenHostedHarnessConnectorTest 34/34, HarnessCoordinatorTest 52/52,
typecheck, eslint, prettier. Design doc updated in both languages.

* fix(managed-agent): converge P1-1 (tool-config vs unpaid Runtime work) and P1-2 (stale approval copies)

Round-7 re-review of 275f934824 found two remaining wedge families
inside the cancellation settlements this PR introduced. Both converge on
durable proof, never on the stale checkpoint copy.

P1-1: a takeover cancellation must separate "the Session is configured
for tools" from "the Turn owes unpaid Runtime work". A files-profile
session whose owner died BEFORE its first tool call carried no unsettled
Runtime work (checkpointless or a bootstrap checkpoint naming no Turn) —
and the kernel's different-Turn check threw on the bootstrap's null
turnId, so the load answered the retriable 409 forever. The load route
now hoists that split ahead of the kernel: with the explicit
cancellationTakeover signal (and only with it — the collision guard
stays), any ownerless park without unpaid Runtime work settles into the
journal itself; work in flight follows the recovery-cancel report; the
no-signal shape keeps its baseline refusal unchanged.

P1-2: the durable action record — not the checkpoint's stale copy of the
approval — decides an owner-died approval. When the record says the wait
ended, the recovery kernel no longer maps it to the inapplicable plain
attach: expired/cancelled is transient (route refusal), decided is
advanced through its own durable gate ONLY on a drive load (a
cancellation never crosses it), and every other shape (still requested,
or undefined) keeps the plain attach a resolve route can pay. The
cancel/expired arrive by the same separation (settle with the signal;
decided is settleable with the signal too — a CANCELLING takeover does
not resume approved work no matter which side the user took).

Witnesses: `settles the cancellation of an ownerless parked Turn on the
takeover load (profile=null|FILE_PROFILE)` parameterized exactly as the
re-review demanded; three recovery-kernel witnesses for the cross-read
(resolves-inapplicable only while requested; throws for ended-without-a-
decision; drive declines checkpoint_blocked on an undrivable decided
wait); route-level `settles a cancellation takeover whose durable
approval already ended (state=cancelled|decided)`. Design doc updated in
both languages. Suites: hosted-harness-session 222→(+10 new),
hosted-runtime-recovery 36→(+3 new), issue-13328 33→, all green;
typecheck and eslint clean.

* chore(vscode-ide-companion): regenerate NOTICES for the merged dependency layout

* fix(managed-agent): read owed Runtime work off the work itself and pay coded plain-cancel refusals with the cancellation takeover (R9)

Round-9 real-stack measurement found the cancellation separation still
wedging on two families: every cancelled Turn after the first, on both
profiles, and every cancel row of P1-2 — where no load ever left the
coordinator, because a plain-cancel retry can never settle a parked
Turn and no redispatch was forthcoming.

- The park gate now reads owed work off the work itself: a checkpoint
  naming an earlier Turn whose tool items all settled and were consumed
  owes nothing to a cancelled Turn that never reached a tool call; and
  a no-tool Session cannot owe Runtime work by definition, so its
  cancellation arm settles unconditionally again (the round-8 behavior
  the gate had displaced onto a blocked restore basis).
- The admitted-cancel path turns a plain cancel's coded 409
  hosted_turn_recovery_required into an immediate cancellation
  takeover load over the same attachment (recoverManagedCancellation),
  and the connector sends it even with a healthy cached attachment —
  the shortcut would swallow it and the load would never leave the
  process. A recovered Runtime park it reports is cancelled through
  its checkpoint admission; the already-running stream lands the
  settle either way.

Witnesses: hosted-harness-session settles the second-Turn cancellation
park on both profiles (the round-9 rig witness adopted); the
coordinator drives the takeover load exactly on the coded refusal
(plain park and recovered park) and on no other answer; the connector
always sends the cancellation load over a cached attachment. Suites:
hosted 229, HarnessCoordinatorTest 55, QwenHostedHarnessConnectorTest
35, ManagedAgentServerIntegrationTest 29.

The decided-approval drive arm of P1-2 (rows where the user answers
allow or the approval expires unanswered) remains open: it needs the
plain-attached replacement to drive the parked Turn itself once the
durable wait ends, and lands as its own slice.

* fix(managed-agent): pay the cancellation signal identically on an attached re-answer (R9-2)

A cancellation takeover load the coordinator forces onto a Session
ALREADY attached in this daemon used to skip the entire settle
separation: the attached branch called the passive kernel directly,
which never advances a durable wait passively — so a park whose
approval ended after the attach (the round-10 allow, then cancel
repro) was thrown back as an unknown phase and refused retry-invariant,
while the first-load branch settled the identical shape. Hoist the
separation above the attachment split: the attached branch now reads
the signal too and runs the same no-tool arm, owed-work gate and
durable cross-read, settling through settleCancelledHarnessTurn into
the journal the already-running stream lands.

Witnesses parameterize both ended states: attach plain while the
durable approval says requested, end the wait after the attach with
the stale checkpoint copy untouched, then redrive with the cancel
signal — 200 with the redriven-load settle log, and the plain cancel
route reads settled-at-tail. Suites: hosted session 231, recovery 44.

* fix(managed-agent): close the durable wait before the cancelled terminal lands (R9-3)

settleCancelledHarnessTurn only wrote turn_result/cancelled into the
journal, and the record sink's turn.settled only advances next-turn
checkpoints at a model-start phase — so a cancelled Turn parked at an
approval wait ended in the journal while its checkpoint stayed at
await_action, and the Session's next prompt met
`ManagedHarnessBlockedError: await_action is not a model-start phase`
(the round-11 repro: old Turn completed cleanly, new Turn failed as
hosted_turn_failed before the model was ever called, closing 1 P1).

The cancelled settle now closes the ended wait first through the
wait's own durable gate (resolveDurableWait → model_output_committed,
a model-start family phase) whenever — and exactly when — the durable
approval already ended, mirroring the cross-read the caller's gate
performed. The decision remains the USER's and nothing resumes: the
Turn dies immediately after. Runs of requested waits or of in-flight
Runtime work keep the kernel's existing paths.

Witness (both answers): requested approval attaches plain on the
passive takeover, the record ends after the attach with the stale
copy untouched, and the attached cancellation takeover settles —
200 with the redriven-load settle log, plain cancel reads 204, and
the settle provably commits the model_output_committed boundary
ahead of the terminal record. Suites: hosted session 231, recovery
44; typecheck, prettier, eslint clean.

The unit suite cannot stage "approval-first park + literal owner
death" today (DELETE refuses active Turns, and abort unwinds the
wait cleanly), so the end-to-end relief-Turn acceptance stays with
the real-stack probe; the annex-level witnesses cover the settlement
payment and the wait-close commit.

* chore(vscode-ide-companion): regenerate NOTICES from a clean-room install

The previously committed file was generated in the long-lived
worktree whose pnpm layout had drifted (is-fullwidth-code-point@5.1.0
+ get-east-asian-width@1.5.0 + fdir/picomatch slot differences) even
though pnpm-lock.yaml is byte-identical to main. Regenerated in a
fresh worktree with a frozen-lockfile clean install: 662 deps, and
the output matches the lint lane's renewed file byte for byte, which
the polluted layout could not reproduce.

* fix(managed-agent): pace forced takeover loads per Turn and make the cancelled settle idempotent (R9-4)

Two round-10 verdict items:

1. The coded-409 escalation (R9-P1-2) ran on every ~500ms lease-renewal
   tick, so a Turn cancelled while its wait still owns a live durable
   record paid ~4 requests/s of daemon work forever (21 forced loads and
   20 cancels in the verdict's captured window). cancelAdmittedTurn now
   paces the forced cancellation takeover load per Turn (5s minimum
   interval): the cheap plain-cancel retry keeps carrying the wait, and
   a wait that ends inside the interval still settles on the next paced
   attempt — far below the approval timeout's 45s budget.

2. A settle that already landed was not idempotent: the coordinator's
   next forced load inside the stream's landing window met
   `event id turn:<id> is already committed` (3/3). The cancelled settle
   now re-reads the journal's own turn.settled for this prompt before
   writing, so the redriven load is a re-answer, never a second
   turn_result racing the event-id CAS.

Witnesses: three paced coded-refusals (t0 / t+500ms / t+5s) yield three
plain cancels but two forced loads (HarnessCoordinatorTest 60); the
round-9-2 witness pair now races a second identical load inside the
landing window and asserts one cancelled record and no
takeover_unavailable line (hosted suites 275).

* test(managed-agent): commit authority-valid deltas in the restore byte-budget test

How: holdsRestorePagesInsideThePerPageByteBudget now builds each dense
transaction from 210 well-formed message.delta lines (text at the
reader's 4096-byte cap, ~1 MB per transaction) plus the commit marker,
through a shared TurnEventLines composer, and asserts that revisions
1..9 fit the 8 MiB page budget while a tenth does not. The reader replay
IT now also replays that delta shape. The class turns off MockMvc
print-on-failure.

Why: #13348 added the test with placeholder lines ({"subtype":"event_v1"}
plus a padded commit_v1 line), and #13355 (merged later, never run
against it) made the commit gate refuse any non-event line inside the
event range, so every commit after genesis answered 409 and main's SDK
Java lanes went red. The failure was masked as timeouts: Spring Boot
printed the failed test's ~1.3 MB request body as one log line, which
stalled the Actions log pipeline for ~45 min, so the Hosted Verify and
O4 steps blew their ceilings and the MariaDB lane hit its job timeout.

Test: ManagedSessionStoreIntegrationTest and ManagedExtensionRecordStoreTest
green; full managed-agent-server surefire (1077) and checkstyle green;
HostedCommittedEventLineReplayIT green through the real TS reader and
red when the delta text exceeds 4096 bytes; dropping the page byte
budget in ManagedSessionStore fails the test (10 rows instead of 9)
with no log line over 400 chars.

* fix(managed-agent): propagate the settle's own wait-check fault instead of degrading it to a successful cancel (R9-5)

The cancellation settle's authorization re-read was wrapped in
`.catch(() => undefined)`, so a transient store fault surfacing
between the caller's gate and the settle's own wait-check degraded
into 'nothing to close' — the cancelled terminal landed over a wait
the fault hid, and the Session's next prompt then found the same
obsolete checkpoint that family wedges on (the round-11 fault
injection at this exact boundary: authorization flips
runnable → blocked/missing_state → runnable once, the settle still
mints turn_result, and the run dies on `await_action is not a
model-start phase`).

A read failure is a retriable store fault, not 'nothing to close': it
now propagates into the caller's try-catch, which keeps the baseline
retriable refusal — the fault's own retry ladder owns the retry, and
the replayed shape then settles identically.

Witness mirrors the probe: the wait-gate reads fine, the settle's own
check faults once — load refuses hosted_turn_recovery_required with no
Cancelled record and an honest 409 on the plain cancel, then the same
shape settles after the fault clears (settle log, the
model_output_committed boundary, plain cancel 204). Suites 276/276
(hosted session 232, recovery 44), typecheck/prettier/eslint clean.

* fix(managed-agent): refuse the cancelled settle on fault-shaped authorization verdicts too (R9-5')

R9-5 propagated THROWN authorization faults into the caller's retriable
refusal, but the authority never throws the same class of failure: the
real path takes a retry-exhausted ManagedSessionStoreTransportError and
converts it in place into `{status: 'blocked', reason: 'missing_state'}`.
From there the cancelled settle still marched on — it skipped the wait
advance and wrote the cancelled terminal anyway, spending the Session's
next prompt on the obsolete `await_action` checkpoint the verdict hid
(the round-11 verdict at `964b87a50a`, reproduced with the production
error type: 200 on the takeover, the terminal lands, and the next turn
dies `await_action is not a model-start phase`).

Fault-shaped authorizations are never "nothing to close":
`missing_state` (the TransportError conversion), `opaque_state` and
`invalid_state` now all answer the same retriable refusal — the settle
refuses rather than minting what it cannot prove. `missing_checkpoint`
stays payable on purpose: it is the Arm B park's honest form (a no-tool
Session with history provably has no checkpoint) and its settle is the
already-verified behaviour.

The witness covers both faces of the store fault
(`it.each(['exception', 'missing_state'])`): same park, same stage,
either a thrown fault or a folded `blocked/missing_state` — the load
refuses with zero settle and an honest plain cancel 409, and the replay
settles the moment the fault clears. Suites 277/277 (hosted session
233, recovery 44), typecheck/prettier/eslint clean; design docs note
the boundary in both languages.

* docs(design): split the R9-5' verdict paragraph into its own bullet

The R9-5' text rode mid-bullet after R9-5's run-on, which Prettier
then flattened to column zero — breaking the list structure the lint
lane rejects outright. Both languages carry the split now, and the
lint-facing check passes again.
2026-10-07 03:38:25 +00:00
Shaojin Wen
29fd4c1ab6
feat(managed-agent): broker authentication and broker-provisioned writer credentials (#13210)
* feat(managed-agent): broker authentication and broker-provisioned writer credentials

Close the five gateway-free security gaps from #13180:

- signed auth mode: the broker authenticates the tenant/actor header pair
  itself via HMAC (X-Qwen-Signature + timestamp), with startup guards that
  refuse non-loopback binds, weak keys, a contradictory trusted-actor
  stand-in, and a context path that would bypass the path filters
- writer credentials are broker-provisioned HMAC bindings over
  (tenant, workspace, session) and enforced at every session-store entry
  point; the harness receives them through the attach payload
- /internal/** can bind to its own loopback connector with 404 routing
  between the two surfaces
- sessions record their creator (V28), so hosted approvals are answerable
  over HTTP and workspace approvals bind to the authenticated actor
- the TS client refuses plaintext http:// broker URLs on non-loopback
  hosts unless the broker opts in via the attach payload

Includes the bilingual design doc (docs/design/managed-agent-broker-auth)
and verification coverage: signed-mode E2E, dual-port routing E2E, the
binding sweep over every store entry point, and the hosted ownership
chain.

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

* fix(managed-agent): harden broker guards from adversarial audit

- refuse internal-server.port equal to server.port at startup: the surface
  routing filter classifies by local port, and equal port numbers on
  different addresses would serve /internal/** on the public address
- refuse reusing the signing key as the writer binding key: the two HMAC
  keys protect different domains
- log the resolved auth mode, internal listener and writer binding state
  at startup, as the design doc promises
- log the refused managed session store descriptor in the hosted harness
  so the transport guard's remedy stays discoverable

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

* fix(managed-agent): renumber the session creator migration to V31

main gained its own V28 and V30 migrations while this branch was in
review; move the creator column to V31 to keep the Flyway sequence
unambiguous.

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

* chore(web-shell): regenerate the managed agent API types

The v1.28 Unauthorized response description changed; the generated
client must match the contract.

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

* fix(managed-agent): address the PR review round on broker auth

- sign the request body, query string and Idempotency-Key too, so a
  captured signature authorizes exactly one request (review R1-3)
- resolve the public port to the Spring Boot default 8080 when
  server.port is unset, so the equal-port guard cannot slip (R1-1)
- read the session creator row null-tolerantly instead of 500ing on a
  missing row (triage #3), and make the writer credential policy a
  required injection so it fails closed (triage #5)
- startup guards: allowed-drift >= 1s, loopback store base-url must name
  the internal listener's port, and an enabled harness over plaintext
  non-loopback http is refused (the attach payload carries credentials)
- the routing filter answers the standard error envelope, the internal
  connector checks its address property was accepted, and a TLS public
  listener beside a plaintext internal one is warned about
- docs: OpenAPI scheme descriptions, README recipes and both design docs
  now match the implementation; the ownerless fallback is documented as
  tenant-scoped for anonymous and pre-migration Sessions
- tests pin the drift window, unparsable timestamps, body/query/key
  coverage, the workspace term of the writer MAC, WebShell creator
  recording, the publication-writer credential check, and the producer's
  omit-when-unset payload

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

* fix(managed-agent): renumber the session creator migration to V33

main took V31 (workspace recovery bundle) and V32 (workspace session
close) while the review round was in flight; move the creator column to
V33.

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

* fix(managed-agent): renumber the session creator migration to V34

main took V33 (agent definitions) during the latest rebase. Also make
WorkspaceRecoveryStoreTest compare the session table byte[]-tolerantly:
creator_actor_key is the first VARBINARY column in that table, and byte[]
compares by reference inside Map.equals, which failed the immutability
assertion on MySQL.

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

* test(managed-agent): create the pre-upgrade session with the legacy column set

The retention IT pins the schema at V31 and then creates a session, but
the store insert now always names creator_actor_key (V34), which a
database at the prerequisite version does not have yet. Write the
pre-upgrade session with the V31-era column set directly, then let the
close and retirement flow run through the store as before.

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

* fix(managed-agent): close the signed-mode coverage, buffering and contract gaps

- Cover the bare POST /v1/agents route and normalized path spellings by
  deciding filter coverage on the routed path (shared PublicSurface
  predicate), while the canonical string keeps signing the raw URI.
- Bound the buffered signed body (auth.max-signed-body-bytes, default
  10 MiB) and answer 413 before the signature comparison; refuse a
  repeated Idempotency-Key header with 400.
- Classify the internal surface on the routed path as well, so encoded or
  parameter-carrying spellings cannot cross the listener boundary; the
  routing filter now shares the injected ObjectMapper.
- Refuse a plaintext session-store base URL whose host falls outside the
  client's literal-only loopback set, guard spring.mvc.servlet.path
  alongside context-path, and enumerate the guards allow-insecure-bind
  skips in the startup posture line.
- Qualify the creator-only responder wording in the OpenAPI contract with
  the ownerless fall-through, declare the 401 refusals per route, and pin
  both with contract tests.
- Restore the transaction boundary on the legacy 9-arg insertSessionCommand
  default, share one test signer between the integration tests, and carry
  writer_credential_invalid in the shared error fixture.

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

* fix(managed-agent): renumber the session creator migration to V35

main landed V34 as managed_tool_output_collection; move the creator
column to V35 and update the design docs, the OpenAPI wording and the
test references.

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

* chore: restore NOTICES.txt to the upstream content

The first merge commit swept in a locally regenerated NOTICES.txt whose
content depends on the host's installed tree; CI regenerates it on Linux
and expects the upstream content.

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

* fix(managed-agent): address the R3 review round

- Narrow the replay and body-bound claims to what the code guarantees
  (captured signatures stay fixed to method/path/query/tenant/actor/body;
  the body bound is per request, with the worker pool as the aggregate
  ceiling).
- Presize the signed-body buffer, pin the raw-URI signing split and the
  chunked bound, and add a route-table coverage test for the shared
  public-surface predicate.
- Guard a plaintext non-loopback tool-publication service URL, normalize a
  blank internal address, warn for a non-loopback internal listener
  without TLS, and stop classifying a bare 127 as loopback.
- Declare 413 per operation with a shared PayloadTooLarge component, state
  the ownerless approval fall-through's create-command step, and parse the
  shared fixture structurally in the qwencode test.

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

* fix(managed-agent): address the R4 review round

- Classify surfaces on the router's own parsed path segments, so a
  Content-Type charset cannot make the classifier and the router disagree
  (R4-4).
- Cap the signed-body presize hint at the chunk size; derive the URL host
  from the authority when URI.getHost() is null; validate the publication
  service URL against its consumer's rule; warn on a non-loopback internal
  listener regardless of public TLS; align the ownerless 403 wording with
  the requireOwner conjunction.
- The contract guard enumerates the responder operations structurally;
  read-side operations drop their 403 sibling descriptions and the two
  responders name actor_scope_mismatch.

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

* docs(managed-agent): reconcile the README intro with the signed mode

The page-1 paragraph still described the pre-authentication arrangement
(no auth layer, header-asserted tenant, bind anywhere with
QWEN_MANAGED_AGENT_SERVER_ADDRESS). A non-loopback address now refuses to
start in the default auto mode, so say what leaving loopback requires
(R3-16).

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

* test(managed-runtime): mark the fake-host store test as allowing insecure http

The sealed-renewal test (from main's ffc9772423) builds its store against
the fake host session-store.test; the plaintext-transport guard refuses
non-loopback http unless the descriptor opts in.

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

* test(cli): give Hosted Harness status-settle waits an explicit 10s timeout

The sandbox verification flakiness gate re-ran this file five times and saw
FFPPF: the post-prompt settle polls in the PreToolUse recovery cases exceeded
vitest's default 1s vi.waitFor window under container load, failing at
'expect(status.body.hasActivePrompt).toBe(false)'. The file's dominant pattern
is already an explicit { timeout: 10_000 } (30 sites); extend it to the 27
remaining /status settle waits so the gate observes a deterministic file.

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

* fix(managed-agent): pin signed JSON decoding to UTF-8 (R6-1)

The signature binds the raw body bytes but not the Content-Type charset,
and Spring's Jackson converter decodes a non-Unicode charset through that
parameter: a relayed signed request persisted content that differed from
the signed bytes, which then collided with the original request as an
idempotency_conflict. JSON is UTF-8 by definition (RFC 8259), so the
forwarded request now presents application/json;charset=UTF-8 for JSON
media types; the decoded content depends on the signed bytes alone. Pinned
by an over-the-wire regression test whose replay leg went red (409) before
the fix.

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

* test(cli): cover the remaining default-timeout HTTP polls in the Hosted Harness suite

Verify round 3 re-measured the flakiness gate green (4 files x 5 rounds, no
divergence) and caught the residual: one expression-bodied /status wait and
five /transcript polls still sat on vitest's 1s default. Give them the same
explicit 10s window as every other HTTP settle wait in the file.

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

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-05 04:20:19 +00:00
Shaojin Wen
17c182eda0
fix(managed-agent): stop virtual-thread carrier pinning in the Hosted Harness stream and the Runtime Broker guards (#13388)
The managed-agent server reads every Turn's SSE stream on a virtual
thread (newVirtualThreadPerTaskExecutor). HarnessEventStream.next()
held a monitor across the blocking socket read, so once open streams
reached the ForkJoinPool carrier count every carrier pinned and no
virtual thread in the process ran again — Turns wedged after admission
with zero journal commits, deterministic at >= #cores concurrent Turns
on JDK 21 (JEP 491 exempts JDK 24+). The Runtime Broker repeated the
pattern: BindingRenewal/DispatchRenewal monitors and every
synchronized (context) block in RuntimeBrokerService guarded blocking
repository calls (session/binding/execution findById, compareAndSet,
renewOperation, hasActiveByRuntimeSession) from the same virtual
threads, pinning carriers on contended database rows after the stream
readers were unpinned.

Convert all of them to ReentrantLock with the same guarded regions —
a VT parked on the lock unmounts instead of pinning. Semantics
preserved: same mutual exclusion, reentrancy (next() -> close(),
persistResourceHandle -> closeLocked, SessionContext accessors under
caller-held guards), uninterruptible acquisition, and the
stopped-flag tick-retraction ordering witness
(DurableRuntimeRecoveryTest.stoppedFlagRetractsATickQueuedBehindClose)
stays green.

Witnesses added, both deterministic on JDK 21:
- HarnessEventStreamPinningTest holds carriers+2 SSE bodies open and
  proves an unrelated virtual-thread probe still completes; restoring
  the synchronized next() starves it (32 s, progress=1).
- BrokerVirtualThreadPinningTest parks carriers+2 acquire calls
  inside SessionContext guards at a latched repository call; restoring
  synchronized (context) pins the carriers (only carrier-count
  callers ever arrive, awaiting 60 s).

Verification: qwencode 173 tests, runtime-broker 601 (incl. the two
witnesses), managed-agent-server 567 + HostedConcurrentTurnBurstMySqlIT
6/6 rounds on MySQL 8.0.46, checkstyle clean; full packaged-stack
measurement (16 concurrent turns settling after previously wedging)
reproduced pre-fix during #13365 review and will be re-run on the rig.

Refs #13333

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-04 15:26:21 +00:00
Shaojin Wen
561b606b19
fix(managed-agent): surface the named load refusal from mixed-version takeovers (#13349)
A Harness that cannot open a Session journal (e.g. it predates the
message.delta event kind, after a rollback or during a rolling deploy)
refuses POST /session/:id/load fail-closed with 503 and a machine-readable
code on the wire — that much held since 0.24.7. The Java client then
collapsed every 5xx into MutationOutcomeUnknownException without reading
the body, so the coordinator retried forever with only
failure=MutationOutcomeUnknownException in the log and, once the
pre-admission budget ran out, recorded hosted_harness_unavailable — a
shape indistinguishable from a genuinely down Harness (#13320).

loadSession now reclassifies an ambiguous-status failure whose body names
a refusal code as HarnessSessionRefusedException. The code must match the
snake_case vocabulary the Harness writes (bounded by the turn table's
error_code column); transport failures and code-less or off-vocabulary
bodies keep the outcome-unknown classification, so a proxy 503 or a crash
before the refusal writer is still retried as ambiguous.

The coordinator keeps retrying a named refusal — the Turn still awaits a
Harness build that can read the journal, and a mixed fleet rolls forward —
but the retry logs now name the refusal code, and the terminal failure
records it instead of hosted_harness_unavailable, so the rolling-deploy
runbook can tell a mixed-version refusal apart from an outage.

The failover design doc records the released 0.24.7 refusal shape and the
new coordinator surface in both languages.
2026-10-04 03:46:40 +00:00
Shaojin Wen
728c13de21
feat(managed-agent): Hosted Turn takeover and G1 failover E2E (#13083)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Runtime Broker and Managed Agent MariaDB / Java 21 (push) Waiting to run
SDK Java / Hosted process fault gates / MySQL 8.4 / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(managed-agent): Hosted Turn takeover and G1 failover E2E

Implement the Harness half of Stage G Turn takeover (#12952 G1): a
replacement Hosted Harness loads a Session parked at an await_runtime /
results_ready checkpoint, settles its Runtime executions under their
original executionCallId (never replaying the model Prompt), reports the
recovery snapshot the Java coordinator already parses, and continues or
cancels the turn through the new managed-runtime routes. Assistant text
streams as activation-scoped message.delta journal events so a published
prefix survives its writer.

Ungate --inflight-failover / --continuation-failover against the packaged
stack: public Workspace admission carries the fixed files profile, the
runner seeds the registry/grant and opts into durable local Workers, and
both modes run in the Hosted MySQL CI job. The modes require Linux (the
W0e durable reclaim) and refuse other platforms with an explicit error.
A default-off trusted-actor-header property stands in for the gateway
principal on single-host/E2E deployments.

* fix(managed-agent): let the takeover owner adopt a settled Turn's checkpoint

A replacement Harness feeding the settled batch to the model ran under its
own activation, so the next tool batch was refused as Runtime work of the
prior activation and the Turn stayed RUNNING with no terminal event. When
consumeRuntimeResults runs under a different activation with every item
settled, commit the consumed checkpoint under the current activation.

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

---------

Co-authored-by: probe <probe@local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-01 16:35:32 +00:00
Shaojin Wen
78143fe335
feat(managed-agent): Serve durable permission Actions (D6b) (#13101)
* feat(managed-agent): Serve durable permission Actions (D6b)

* test(managed-agent): Avoid concurrent Action mock stubbing

* 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(managed-agent): address the D6b review follow-ups

A Workspace Session whose connector was built without the Managed Action
store silently fell back to the deployment approval mode and skipped the
Harness mode confirmation -- the case the design declares fail-closed.
createOrLoad now refuses it, so the two remaining null checks in the
connector are gone and an un-wired store fails loudly at admission.

requireOwner let an IllegalArgumentException from actorKey escape as an
unmapped 500. It now answers 403 actor_scope_mismatch, matching the
neighbouring workspace routes.

endedCode maps cancelled to action_cancelled but no test asserted it: a
cancelled Action now fails its response operation with that code and
returns no resolution.

The design docs still named the Actions migration V23 after it moved to
V24; V23 belongs to the Session MCP catalog (#12946).

---------

Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: yiliang114 <jinjing.zzj@gmail.com>
2026-09-30 15:38:38 +00:00
Shaojin Wen
a63157304a
feat(managed-agent): enable G0 public Workspace file turns (#12955)
* feat(managed-agent): enable G0 public Workspace file turns

* fix(managed-agent): preserve Workspace admission refusals

* fix: repair skill listing fixture and Workspace capability docs

* test(runtime-broker): pin durable worker crash window
2026-09-29 16:38:53 +00:00
Shaojin Wen
1d03a07f35
test(sdk-java): close the connection on Hosted Harness no-content responses (#12824)
The Java 11 lane of SDK Java fails intermittently in
HostedHarnessClientTest.coversLoadStatusTranscriptHeartbeatAndLifecycleMutations:
a teardown call raises MutationOutcomeUnknownException wrapping
"HTTP/1.1 header parser received no bytes", while Java 17, Java 21,
macOS and Windows stay green in the same run (seen on #12816).

It is the fixture problem #10472 fixed for DaemonSessionClientTest. On
Java 11 the JDK's own HTTP server drops the connection after a response
without a body, while the Java 11 HttpClient keeps that connection
pooled, so the next request over it reads EOF before any response byte.
The test answers cancel, detach and session delete with 204 back to
back. HostedHarnessClientTest arrived later (#12654) with its own 204
helper, which left the connection persistent.

Mark that helper's responses non-persistent, as #10472 does.

On Temurin 11.0.29 the test failed 115 of 200 rounds pinned to one core
and 25 of 200 unpinned before this change, and 0 of 200 in both after.
All 18 methods of the class pass 50 of 50 rounds on one core after it.
JDK 25 passed 200 of 200 rounds before it.
2026-09-27 06:56:51 +00:00
jinye
585466c646
feat(sdk-java): Add the Hosted Harness private client (#12654)
Add the Java control-plane transport for the hosted Harness private
protocol from #12409. Construction performs capability negotiation,
validates the advertised capability digest against the configured
deployment digest, and pins the process boot identifier; every later
request then carries the protocol and boot-ID fencing automatically,
and a generation mismatch surfaces as a non-retryable conflict rather
than a retry against a restarted Harness. Sessions are created, loaded,
heartbeated, prompted, streamed, and cancelled through typed request
objects; turn submissions carry a precomputed payload digest, and event
streams reuse the daemon SSE reader with replay cursors.

The client has no production caller yet: it is the transport the Spring
control plane will wire in a later slice, in the same pattern as the
attestation client before process adoption.

Related to #12380.
2026-09-24 16:47:15 +00:00
Shaojin Wen
f5beafbdd6
chore(sdk-java): bump fastjson2 to 2.0.65 (#12465)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-22 11:45:31 +00:00
Yu Zhang
8b72f4d456
fix(java): make SDK worker threads daemon (#9925)
* fix(java): make SDK worker threads daemon

Use daemon threads for the Java SDK default executors so background helper pools do not keep example or short-lived JVM processes alive after work completes.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(java): document and test daemon pool lifecycle

Keep examples and docs aligned with daemon executors, and ensure the client module actually discovers its JUnit 5 regression test.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(java): reuse the custom example executor

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* test(java): isolate SDK integration cases

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

---------

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
2026-09-01 18:06:25 +00:00
Yu Zhang
834b66d209
fix(java): read control response subtype from payload (#9924)
* fix(java): read control response subtype from payload

Use the parsed control response wrapper when deciding whether a CLI control response is an error, matching the nested wire format and preserving the legacy top-level fallback.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(java): keep sendPrompt stream aligned after control errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(java): align control error fixture

Use the real control-response error envelope in session tests so the subtype handling coverage matches the CLI wire format.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(java): simplify control response subtype lookup

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* test(java): cover control response warning logs

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* test(java): strengthen control response subtype coverage

Cover missing response payloads, non-error subtype boundaries, and warning payload content so subtype detection regressions are observable.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* test(java): pin control response subtype boundaries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 23:39:06 +00:00
易良
3aa1b14624
ci: stabilize tests under shared ECS host contention (#10552)
* ci: tolerate shared runner scheduling delays

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

* test(ci): inherit ECS contention budgets

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

* test(cli): extend PTY request budget on ECS

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

* test(cli): synchronize ACP archive race

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

* test(ci): isolate visuals publisher fixture modules

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

* test(ci): cover hostile parent package scopes

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

* test(sdk-java): isolate finite SSE fixture connections

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

* test(cli): make timer-range deadline deterministic

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

* test(sdk-java): tolerate shared CI scheduling delays

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

* test(sdk-java): preserve timeout contract on CI

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

* test(sdk-java): include request submission in timeout bound

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

* ci: extend no-AK shared runner timeout

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

* test(ci): guard no-AK integration budget

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

* ci(sdk-java): serialize ECS test execution

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

* test(sdk-java): relax stalled-response setup on CI

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

* test(ci): align no-AK timeout contract

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

* ci: extend Serve A/B shared runner budget

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

* ci: scope extended ceilings to ECS routing

The extended job ceilings (test 60->90, integration_no_ak 30->60) were
unconditional even though runs-on falls back to GitHub-hosted
ubuntu-latest for fork PRs from untrusted authors and when
MAINTAINER_ECS_RUNNER_DISABLED=true. Derive each ceiling from the
runner classify_pr selected so hosted paths keep the pre-contention
bound and a genuine hang there does not burn the extra 30 minutes.

The lane contract now evaluates the real timeout expression for both
routings instead of pinning a bare number, matching the
substitute-then-evaluate technique ci-runner-routing.test.mjs uses on
runs-on.

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

* fix(ci): address closeout review feedback

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-30 14:38:54 +00:00
Yu Zhang
8104071260
docs(java): correct closed session exception (#9859)
* docs(java): correct closed session exception

* docs(java): clarify session control exceptions

Clarify which Java SDK entry points throw SessionControlException directly and pin closed-session behavior with a lightweight unit test.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* test(java): pin unavailable session exceptions

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* test(java): pin session exception wrapping

Add coverage for direct session initialization failures and both QwenCodeCli.newSession wrapping paths so the Java SDK docs stay aligned with the exception contract.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

---------

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
2026-08-29 15:01:02 +00:00
Shaojin Wen
e5838f4145
test(sdk-java): close the connection on body-less fixture responses (#10472)
The Java 11 lane of SDK Java fails intermittently in the daemon client
tests: a session teardown raises DetachOutcomeUnknownException or
MutationOutcomeUnknownException wrapping "HTTP/1.1 header parser
received no bytes". It has hit five different test methods over the past
day on unrelated branches, while Java 17, Java 21, macOS and Windows stay
green in the same runs.

The fixture causes it. On Java 11 the JDK's own HTTP server drops the
connection after a response with no body at all -- a 204 detach, a 204
cancel, a 204 session delete, an empty event stream -- while the Java 11
HttpClient keeps that connection pooled. The next mutation the SDK sends
over it reads EOF before any response byte, and since the request is not
idempotent the SDK correctly declines to retry and reports the outcome as
unknown. Pairing the halves across JDKs shows both must be Java 11: a
Java 11 server with a Java 21 client never fails, nor does a Java 21
server with a Java 11 client, so only the fixture is affected.

Route every body-less response through a helper that marks the response
non-persistent, so the client never pools a connection the fixture is
about to drop. This generalises #10365, which applied the same treatment
to two handlers of a single test.

On Java 11, pinned to one core: the three test methods seen failing in CI
failed 50 of 200 rounds before this change and 0 of 200 after.
2026-08-29 07:27:09 +00:00
易良
c1444bdc40
fix(java): avoid Java 11 HTTP fixture reuse (#10365) 2026-08-28 07:15:38 +00:00
zjgzx1988
435199716b
fix(artifacts): refresh updatedAt when artifact content changes (#9929)
* fix(artifacts): refresh updatedAt when artifact content changes

Content edits and same-size HTML republishes left updatedAt equal to
createdAt, so previews stayed stale. Treat fingerprint and observed
status/size changes as material updates.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): record the size baseline when content hashing is skipped

The workspace size baseline was only written alongside a sha256, so files
too large to hash — and snapshots restored without one — had no baseline
left once the public sizeBytes started tracking the live stat. A single
content change then bumped updatedAt twice and let status fall back from
changed to available on the next refresh.

Also restrict the published content hash to the trusted artifact tool path
and drop the unreachable published branch from the preview freshness key.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): include clientRetained on restore fixture

acp-bridge compiles tests in tsc --build, so a restore snapshot missing the required PersistedSessionArtifact field failed CI prepare.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): do not keep deleted write_file intermediates

write_file auto-recorded HTML used only to print a PDF, then the model
deleted the file and left a missing artifact card. Intermediate writes
can now opt out, scratch paths under .qwen/tmp are not registered, and
a vanished write_file workspace file is dropped on the next list/get.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sdk-java): wait for SSE teardown before detach after a 204

Java 11's HttpClient can reset the follow-up POST /detach when the
unexpected-success SSE stream is still closing. That turned
unexpectedSuccessfulSseStatusIsProtocolFailure into an Error after
the assertion already passed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): forget write_file artifacts only after confirmed absence

Stat errors such as EACCES were reported as missing and then permanently removed. Keep those entries until ENOENT is confirmed, persist vanished removals strictly, and only bypass the refresh TTL while a write_file artifact is still available.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): restamp write_file fingerprints and recheck before forget

Available write_file artifacts were rehashed on every poll after an mtime-only drift, and get()/list() could tombstone a file that came back or disagree after a failed persist. Restamp the in-memory baseline when content is unchanged, re-stat confirmed-missing write_file entries before forgetting them, and return the restored artifact from get() when the tombstone does not land.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): refresh write_file presence before overflow eviction

evictOverflow still used the TTL-only refresh filter, so a write_file file deleted inside 5s could keep a stale available status and push a healthy artifact out. Use the same presence recheck as list/get, and wait for SSE teardown on the remaining Java 11 error exits.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): bump updatedAt on later same-size content edits

A second edit while status is already changed left the preview
freshness key unchanged. Track the last observed fingerprint, and
persist vanish-forget as eviction so a later client record of the
same path is not tombstone-suppressed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): drop stale observation state on re-register

A changed-to-available re-record kept lastObserved fingerprints and
bumped updatedAt on the next refresh. Clear them in merge, keep
stat-error missing artifacts out of eviction, and pin the size
baseline before adopting a live size.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(artifacts): pin remove updatedAt against a fake clock

The createdAt/updatedAt inequality raced on machines where upsert
and remove landed in the same millisecond.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(artifacts): scope get() vanish-forget to the requested artifact

A sibling left in clean-missing after a persist rollback can have its file come back; get() must not tombstone it from that stale observation.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 14:02:11 +00:00