Commit Graph
89 Commits
Author SHA1 Message Date
jinyeandQwen-Coder e104c7026e feat(managed-agent): import Skills from the original Runtime (#16638)
* feat(managed-agent): import Skills from the original Runtime

Discover project Skills only inside the selected Session's original Workspace, fence snapshot reads by owner and generation, and reuse archive publication and explicit Session apply. Keep personal MCP OAuth independent and synchronize contracts, SDKs, UI, witnesses and bilingual design.

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

* fix(managed-agent): align Runtime import admission witnesses

Keep the ordinary cross-tenant oracle intact after separating OAuth and synchronize the existing route fixture digest without changing its capture gate.

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

* fix(managed-agent): advance the contract version past main

Main reached 1.105.0 while this branch still declared it, so the
contract-version guard read the changed document as stalled.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-09 23:35:23 +08:00
jinyeandQwen-Coder d32faa247d feat(managed-agent): add opt-in personal memory backed by Holo (#16488)
* feat(managed-agent): add opt-in personal memory backed by Holo

Bind personal memory stores atomically to sessions and enforce owner visibility alongside workspace access across session surfaces and child context.

Keep Holo credentials and namespace selection in the control plane, and add bounded Hosted recall, approved memory tools, durable operation receipts, and recovery without automatic duplicate writes.

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

* fix(managed-agent): synchronize memory contracts and recovery

Preserve question runtime recovery after rebasing and restore native batch owner checks. Synchronize schema, lock and capability inventories plus bilingual memory configuration references.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-09 09:13:13 +00:00
jinyeandQwen-Coder 0b3acaf6d6 feat(managed-agent): add Skills and MCP self-service (#16500)
* feat(managed-agent): add Skills and MCP self-service

Add daemon-free package preparation and installation, owner-fenced Runtime Skill imports, and actor-scoped HTTP MCP OAuth behind default-off deployment switches.

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

* refactor(managed-agent): bound self-service admission helpers

Keep actor checks inside locked admission helpers and token issuance in its own collaborator without changing public contracts or transaction ordering. Tighten the action/store facade limits and align the facade contract witness.

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

* fix(managed-agent): align self-service contract metadata

Advance the public API version and document both default-off self-service flags in the English and Chinese generated configuration reference.

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

* fix(managed-agent): repair self-service CI gates

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

* fix(managed-agent): parameterize CSI fixture tenant predicates

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

* fix(managed-agent): remove stale CSI schema import

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

* style(managed-agent): format Hosted capability guards

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

* style(managed-agent): format server OAuth configuration

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

* style(managed-agent): order self-service test imports

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-09 06:04:57 +00:00
Shaojin WenandQwen-Coder 0646b57f06 fix(managed-agent): prove A3 rewritten command recovery without Hook rerun (#16116)
* fix(managed-agent): prove A3 rewritten command recovery without Hook rerun

Add the packaged command-rewrite arm and a focused three-generation witness
for two approval-bound extension command steps. Recovery keeps each rewritten
input on its own command Action and original deadline, replays settled
Hook/tool-stage results without physical rerun, and still accepts a relief
Turn and reliable close.

The real MySQL/Linux counterexample exposed small lifecycle gaps that are
fixed here: command_approval_* Action ID grammar, extension recovery resource
closure, commandStep load routing and Hook owner construction, a fast Shell
close race, and command receipt acknowledgement before Runtime retirement.

* fix(managed-agent): refuse Hook broker swap while the mount is held

delegateBrokerToMcp(broker) replaced the broker without reconciling the acquired/mount-claim state, so a config-revision enable that swapped owners after a Hook turn had acquired the mount would orphan the old claim and leave acquired pointing at the previous broker. Refuse the swap while the mount is held (the no-argument handover keeps the same broker and stays valid); a fresh load binds the new owner at construction. Adds a red/green regression plus free-swap ownership and shared-disown pins.

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

* fix(managed-agent): capture extension Shell output before an already-closed handoff

The already-closed Shell adoption resolved closure before any data listener was installed, so a command that wrote stdout/stderr and exited during owners.start()'s membership wait was persisted as a successful empty result (silent data loss) instead of the base's explicit unknown-outcome failure. start() now optionally buffers stdout/stderr synchronously from spawn (prereadOutput), and the Shell detaches, replays those bytes through its capture, and installs live listeners in one synchronous step, so no output window is lost. hook/stdio callers do not opt in and keep their fail-safe behavior. Adds a deterministic pre-handoff replay regression (red before, green after) and a real-owner spawn-buffering regression.

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

* fix(managed-agent): release preread capture on failed extension Shell startup

A failed start() never handed detachPreread to a caller and the catch left the early stdout/stderr listeners attached; since stopOwner keeps the child in the owner map, child -> listener -> buffer retained every preread byte for the instance lifetime (reproduced at 8 MiB per failed start). The catch now detaches the listeners and clears the buffers. Adds a red/green regression that injects a post-spawn journal failure and asserts the retained child has no data listeners. Also strengthens the mount-held swap guard test to construct mountWorkUnproven()=false with the mount held so the new clause itself is pinned (whole-guard removal now reds it), and adds a Linux-runnable real-spawn preread test (cgroup unit mocked) so the spawn-time attach is covered on the OS CI uses.

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

* fix(managed-agent): use ChildProcess type for the preread test cleanup handle

The Linux preread regression typed its cleanup handle as { kill: (signal?: string) => void }, whose kill parameter is wider than ChildProcess.kill(number | NodeJS.Signals). Under strictFunctionTypes the assignment of started.child is TS2322, and the test file is part of the CLI tsc --build regardless of it.skipIf, so clean builds failed on every platform. Use a type-only ChildProcess import. Verified the exact rule with an isolated strictFunctionTypes probe (old annotation TS2322, ChildProcess clean) and a full CLI tsc --noEmit (rc 0); the host OOMs on the full reference-graph tsc --build, so the bundle build is left to CI.

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

* test(managed-agent): pin failed-start preread cleanup on Linux

verify-pr round 2 F7: the failed-start preread detach/clear was pinned only by a darwin-only test, and the macOS CI lane never runs on pull_request, so removing the cleanup (M9) stayed green on Linux. Add a linux-cgroup failure regression (cgroup unit mocked, real child) that rejects the post-spawn saveOwner after a short delay so sentinels are captured, then asserts the retained child has zero data listeners; removing the detach reds it on the platform CI runs. Mirrors the reviewer's Linux probe.

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

* fix(managed-agent): address A3 review R1 findings

R1-1: replace the broker-swap guard with close-and-rebuild so a config revision bump that keeps the Hook catalog no longer parks a mount-held apply at recovery_required; revert the delegate overload. R1-2: cap the spawn-time preread at the shell 1 MiB budget with an overflow flag folded into outputCaptureLimitExceeded. R1-3: add a tool_stage domain-refusal load test. R1-4: batch the package-config retention walk (BFS + IN(...) + delta insert) with a delta witness. R1-5: precompile the Action-id Pattern at four sites. R1-6/7/8: anchor the crash/final evidence matrices and the package hook-command contract. R1-9: drop the inert admission-failure acknowledge. R1-10: read back both command-action decisions in the A3 arm. Verified by red/green regressions, CLI typecheck, the affected suites, and packaged arm owner-r46 on real MySQL 8.4/cgroup v2.

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

* test(managed-agent): pin A3 owner rebuild, preread overflow fold, broker disown

verify-pr round 3 found three test-evidence gaps where the R1 mechanism
was fixed but the regression pinned only half of it. Strengthen the tests
so each named mutation reds:
- N1: fire a second Hook after the revision bump and assert the executing
  broker owner moves ownerFor(1)->ownerFor(2); kills M-close (no-arg
  handover) and M-close2 (close without rebuild).
- N2: assert preread.overflow folds into outputCaptureLimitExceeded and
  [Output truncated]; kills deleting the fold line.
- N3: assert close() does not release the shared broker after disown;
  kills removing ownsBroker=false (double release).

Test-only; production byte-identical to 6d15829e52.

* fix(managed-agent): narrow A3 load-gate takeover and degrade pre-edge snapshots

Round-4 review (bot R2/R3 + maintainer) raised two behavioural Criticals:
- R2-1: the resume-approval gate's extensions arm lost its takeover
  conjunct, so a bare/lifecycle load of an extensions Session could set
  resume, skip the only 409 and drive a parked Turn. Require
  takeoverFlags && !lifecycle (dropping only !session.hooks so a Hook
  catalog still reaches the arm). Harness suite 693 green.
- R3-1: the bundle->contributions closure edge was enforced
  unconditionally on read, so a snapshot committed before the edge
  shipped read as corrupt. Mirror the page-envelope precedent and drop
  an absent contributions child in both the inspection walk and the
  native read; add a read-side fixture proven red/green.

Also address two Suggestions with red/green witnesses:
- R2-3: lift the A3 decision read-back into assertCommandRewriteDecision
  so the contract suite drives it; add disagreeing-revision/option cases.
- R2-6: pin that a lost durable receipt write leaves the native entry
  unacknowledged; moving acknowledge above the write reds the case.

* ci(managed-agent): cap the rewritten-command cgroup delegation step

The failover job's rewritten-command cgroup delegation was the only arm
step without timeout-minutes, so a wedged smoke could spend the whole job
ceiling uncapped. Cap it at 10 like its sibling arms and re-derive the
hosted-failover-e2e ceiling (280->290 summed, 295->305 job, allowance 15)
in both the workflow and the hosted-process-ci meta-test that pins it.

* test(managed-agent): pin the projection dedupe so lost retention reds

R2-8/R2-5: the delta-growth test could not red if the retained seed was
lost, because re-walking an already-projected level left row counts and
gmt_create unchanged. Assert the projection holds every recovery resource
exactly once across the preparing->prepared revisions; emptying the
retained seed now reds it with duplicate resource ids, proving projection
growth is bounded by the delta rather than the revision count.

* fix(managed-agent): repair merge column residue, size ratchet, native tolerance scope

Round-4 review (bot 5757389507) caught three issues introduced by the
round-4 merge / cgroup / native-tolerance changes:
- R4-1: the merge rename missed SUM(terminal) in the plain-arm durable-event
  audit; ma_public_event only declares is_terminal, so MySQL 1054 aborted the
  arm before its verdict. Restore SUM(is_terminal) and add a contract test
  reconciling every single-table ma_public_event column the runner names
  against the V1 schema (reds on terminal, passes on is_terminal).
- R4-2: the rewritten-command arm plus its capped cgroup delegation grew
  sdk-java.yml past its size ratchet; bump .size-baseline 150332->155957 the
  growth is real and the Lint & Static size gate reds without it.
- R4-3/R4-6: scope the native-snapshot contributions tolerance to transitive
  refs collected from resource bodies; a directly declared contributions ref
  stays on the strict isDeepStrictEqual path, matching the inspection walk's
  direct-reference guard instead of silently accepting a missing dependency.

* fix(managed-agent): correct the column guard tokenizer and legacy MCP retention

Round-5 review on the round-4 fixes:
- R5-1: the ma_public_event column guard was wrong in both directions — a
  quoted value (LIKE 'agent.%') posed as a column and red valid SQL, while
  ${sessionFilter} was blanked so a column typo inside it passed unseen.
  Extract a maPublicEventColumns helper that strips quoted literals and
  expands interpolated SQL fragments, with a two-direction witness.
- R2-5: the retention read-then-insert form was applied unconditionally, so
  the legacy MCP-settings path stopped releasing the projection row it
  supersedes and accumulated one row per revision for the Session's life.
  Keep the accumulate form only on the package-config branch; the legacy
  branch replaces its row. Witness: two legacy revisions leave exactly one
  SESSION_RECORD row (reds if the else branch keeps accumulating).

* fix(managed-agent): tolerate a legacy bundle's absent contributions child on commit

R6-1 (Critical, fails-closed regression): isReferenceContainer gained the
extension bundle/contributions kinds, so the commit-side closure walk descends
into a persisted legacy bundle's contributions child. That child was never
staged, so the read 404s and aborts the commit permanently — a legacy Session
loads via the read-side tolerance but can never be reconfigured. Add the
commit-side counterpart of that tolerance: drop only an absent contributions
child from the closure; a present or conflicting one stays fatal. Witness: a
commit whose closure reaches a persisted bundle whose contributions child was
never staged resolves and keeps the bundle in the census; removing the skip
reds it with 'Resource not found.'.

* fix(managed-agent): structural legacy-closure tolerance + command-step takeover arm

Round-7 review, two Criticals on the earlier tolerance/takeover fixes:
- R7-1: the legacy-snapshot tolerance was enumerated site-by-site and
  kind-by-kind, missing a dangling contributions part and a staged bundle
  whose contributions child was never uploaded. Hoist the tolerated kinds
  into one module constant (contributions + contributions-part) and apply it
  uniformly in all four walks (inspection, native page, commit persisted,
  commit staged); the staged arm gains the same async has-tolerance. Direct
  refs and present-but-conflicting children stay fatal. Two witnesses: an
  absent manifest part inspects cleanly, and a staged bundle with a 404
  contributions child commits with the bundle in the census; emptying the
  constant reds both.
- R7-2: the extensions takeover arm admitted every approval continuation, so
  an extensions+Hook Session's native approval reached resumeSavedNativeBatch's
  no-hooks refusal and 503'd the load. Scope the arm to a command-step plan;
  native approvals stay on the no-hooks arm.

* fix(managed-agent): share the extensions command-step takeover with the settle exemption

R8-1 (Critical, fails-closed): an extensions Session that also carries a Hook
catalog never satisfies `takeover` (hooks set), so the file-history settle probe
took canSettleHostedFileHistory and refused on the await_action phase alone. A
batch that owes file history while a command step parks at await_action was 409'd
on every load before the extensions resume arm was ever consulted, leaving the
parked round recoverable only by an out-of-band cancel. Compute
extensionsCommandStepTakeover once and use it in both the settle exemption and
the resume arm, so the parked command-step batch is resumed instead of demanding
a settle a parked approval can never produce. The R7-2 command-step scoping is
unchanged.

* fix(managed-agent): key both cold-load answer guards on one command-step predicate

R9-1 (Critical, regression): the second answerResumedTurnCalls suppression keyed
on resumeApproval, which is only assigned inside the takeover arm and is therefore
undefined on a bare load — so the guard was true exactly on the cold-load path it
was added to protect. A cold load answered the parked command-step round, writing
an irreversible tool_result for the plan's own skill call and permanently
destroying the continuation (or 503'ing via the uncaught hook-recovery throw).
Derive one commandStepPark predicate keyed on the unconditionally-computed
pendingCommandApproval (plus resumeApproval) and use it at both answer guards;
hoist the resumeApproval declaration above it. extensionsCommandStepTakeover is
unchanged (still requires takeoverFlags) — only the two answer guards change.

* refactor(managed-agent): extract MCP settings projection to honor the class-size ratchet

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

* fix(cli): keep the deterministic Hook owner re-acquirable across detach

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

* fix(cli): keep deterministic owners re-acquirable on every non-delete close

Move the terminal-release rule onto the owner instead of a single call site:
Hook and MCP close default to non-terminal and only DELETE opts into the
terminal release, so the shared MCP broker, the v2 lifecycle close and the
enable() owner swap no longer tombstone the id a same-revision reload
recomputes. A non-terminal close hands the mount back via releaseMount and
keeps the owner READY; the MCP drain stays ahead of the handback and skips
markReleased so the reload still derives the same owner id.

Translate the documented release-mount refusals (busy/conflict/writer-unproven)
into recovery-required on both owners instead of letting a raw rejection escape
as a close failure, and drain held Runtimes before the v1 owner close asks for
the mount back.

* fix(cli): re-initialize drained MCP configs on reload and complete the owner close fence

A non-terminal close left every mcp_configuration at 'drained', a resting state
the journal cannot reopen and nothing else consumes, so a same-revision reload
could never re-initialize MCP and stayed recovery-blocked. Advance the drained
row to released (its one legal exit) and reinstall a fresh configuration under
the same runtimeSessionId, so the server restarts, no drained row lingers, and
the deterministic owner id the reload derives does not move.

Add the documented 503 runtime_reconciliation_required release-mount refusal to
the handback fence so a Broker restart beside the Harness is named
recovery-required instead of escaping as a raw rejection that breaks the close.

Let a DELETE terminally release the deterministic owner even on a reloaded,
never-acquired Hook instance, keying the terminal arm off the terminal intent
rather than the transient mount hold, so load->detach->reload->delete no longer
orphans the owner.

* fix(cli): reinitialize released MCP configs and clear superseded drained rows on reload

The drained->released advance stranded the deterministic extension record: the
next reload resolved the same deterministic id back to the released row and
aborted. Treat a released predecessor like a drained one and reinstall a fresh
row under the same runtimeSessionId, picking a revision past every existing row.

A non-terminal close drains every row but only the latest per server is
reinstalled, so superseded same-owner drained siblings held recoveryBlocked (and
mountHandback) forever; advance them to released once the replacing active row is
committed.

The non-terminal discard sites left drained rows under a superseded owner that a
later apply's new owner made the reload reject (owners.size>1). Rather than make
the discard terminal — which would strand an apply that fails and keeps the
previous revision active — release superseded-owner drained rows at cold load,
sparing the active owner's own rows so a failed apply is never stranded.

Also sync ChildWorkspaceServiceTest's busy-storage assertion with the #16369
message reword that landed on main.

* fix(managed-agent): close the MCP/Hook owner lifecycle release gaps

Round-19 review raised three Criticals in the deterministic-owner release
path; all three are fixed:
- R15-3: the live enable callback constructed HostedMcpSession without the
  superseded-drained sweep the cold load already runs, so an off/on/off/on
  extension toggle accumulated owners.size>1 and threw recovery_required.
  Call releaseSupersededDrained before constructing on the live path too.
- R17-1: a DELETE of a never-acquired deterministic Hook owner let the
  Broker 404 (and post-restart 503 reconciliation) escape close(), wedging
  the session undeletable. Absorb both for an un-acquired owner and book it
  released, as releaseOwner does; the un-acquired terminal release itself is
  kept (R15-4). Two regression witnesses proven red/green.
- R17-2: releaseMount refused runtime_session_busy without the drain sweep
  the drain release runs, so a background Shell run outliving its turn wedged
  every detach. Sweep the unproven background rows before the busy check
  (mount handback, owner stays re-acquirable, R13-1).

hook/mcp suites 203 green; cli tsc 0; runtime-broker compiles; bundle builds.
MySQL fault-gate IT verification deferred to the CI full tier.

* fix(managed-agent): keep the mount handback observe-and-hold over live background writers

Round-20 R18-1: the R17-2 releaseMount sweep drained background rows and
then excluded exactly those rows from the busy check with no un-excluded
re-read, so a background process the sweep could not prove stopped stayed
excluded and the mount was handed back over a running writer. drain=true
also stopped legitimate background runs on the two observe-and-hold
handback routes (#13800).

Restore the contract: an observe-only sweep (drain=false) settles provable
exits and stops nothing, the excluded check clears only what settled, and
an un-excluded re-read refuses runtime_session_busy for any row still live
or unproven — the same guard the pre-R17-2 busy check already had. The
method javadoc is rewritten to match.

Witness: mountReleaseStaysBusyBehindASettledExecutionWhoseBackgroundProcessStillRuns
is green with the fix; removing the re-check reds it ("Expected
CompletionException ... nothing was thrown", releaseMount proceeds and
releaseMountCalls flips to 1). RuntimeBrokerServiceTest 199 green;
checkstyle 0 violations; spotbugs 0. MySQL fault-gate IT verification
deferred to the CI full tier.

* refactor(managed-agent): keep RuntimeBrokerService under its size ratchet

Round-21 R19-1: the R17-2/R18-1 releaseMount additions grew
RuntimeBrokerService.java to 4252 lines, past both its ratchet pin and
GLOBAL_MAX_LINES (4216), so ClassSizeRatchetTest failed and the SDK Java
lane went red. The ceilings only ratchet down, so raising them is forbidden.

Move the guarded second half of the mount handback (claim re-check, excluded
busy check, un-excluded background re-read, unproven-capture refusal, then
the transport handback) into RuntimeExecutionRecovery.releaseMountAfterSweep,
beside the observe-only sweep it follows. The facade drops to 4212 lines;
the recovery collaborator is 442 (< 800, so it needs no pin of its own). No
behaviour changes: the sweep stays drain=false, the un-excluded re-read still
refuses, and the claim re-check still runs under the lock.

Witness: ClassSizeRatchetTest reproduced the two 4252-line violations and is
green at 4212; RuntimeBrokerServiceTest 199 green; removing the re-check in
its new home reds the pinned mount test ("Expected CompletionException ...
nothing was thrown"), then restored. checkstyle 0 violations; spotbugs 0.
MySQL fault-gate IT verification deferred to the CI full tier.

* fix(cli): name a raced mount-handback admission_closed as recovery-required

Round-23 R15-2: fencedMountHandback's doc comment says its codes are the
documented release-mount refusals, and the design doc names 409
runtime_admission_closed for the close/archive/delete window
(workspace-execution-mount-recovery-block-release.md). releaseMount's
requireLifecycleRelease throws exactly that conflict when a v2 lifecycle
fence row exists and the harness is not yet draining, but the fence omitted
the code, so a non-terminal detach racing a close/archive/delete rethrew the
raw rejection out of close() and the route answered a generic
managed_session_close_failed instead of the declared recovery-required.

Add runtime_admission_closed to the 409 fence group (additive — no existing
member weakened): the lifecycle operation's own drain frees the mount, so the
detach backs off attached/retryable. Both callers (Hook close, MCP
closeResources) share the fence. Keep 404 runtime_session_not_found out (the
terminal arm books it) and do not fold in 501 workspace_mount_release_unsupported
(a permanent misconfiguration, not a retry-once-it-settles fence).

Witness: add the code to the Hook recovery-required it.each and a MCP
closeResources twin; with the fix both green (hook/mcp 208). Removing the
member reds both new cases with the raw HostedWorkspaceBrokerRejection
("Runtime Broker returned HTTP 409") instead of HostedHook/McpRecoveryRequiredError,
while the unrelated terminal-acquire admission_closed case stays green; restored.
cli tsc --noEmit 0 errors.

* fix(cli): drain held Runtimes before the v2 non-terminal Hook mount handback

Round-24 R22-1: the lifecycle-v2 settle_resources branch issued the owner
close — which for a deterministic owner on a non-terminal close hands the
Workspace mount back — before the branch's only Runtime drain. A
Hook-configured turn shares that Runtime and leaves a live background Shell
(its finish() skips the release), so the release-mount guard refused
runtime_session_busy, the fence named it recovery-required, and the route
answered 503 workspace_lifecycle_resources_unsettled on every retry while the
process lived. The drain that would have stopped the run was never reached.
The v1 close already keeps the opposite order (moved there for R14-2); the v2
branch never got the same treatment when the cleanup-debt drain landed.

Move the drain ahead of the owner close, mirroring v1. This also puts it ahead
of the publisher close, so the drain's stops land their finalize through a
still-open publisher. The owed file-history marker is already retired earlier
in the same route, so the drain's file-history guard is unaffected. DELETE is
unchanged (terminal short-circuits the handback arm), and the handback stays
non-terminal for detach with the deterministic owner id intact.

Witness: a route-level v2 settle_resources close of a session holding a
deterministic Hook owner and cleanup debt, with releaseMount refusing busy
until the descriptor drain runs — 200 with the fix, and reverting the reorder
reds it with `expected 200 "OK", got 503 "Service Unavailable"`. The first
draft of the witness was vacuous (an unrelated lease drain cleared the flag);
only the descriptor-carrying drain now counts. Full harness suite 712 green;
cli tsc --noEmit 0 errors.

* style(cli): apply Prettier to the R22-1 handback-order witness

The Lint & Static lane failed on Run Prettier: the releaseMount spy added with
the R22-1 witness exceeded the print width. Formatting only, no behaviour
change; prettier --check now passes on every file this branch touched.

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

* fix(cli): drain the held Runtime a Hook turn leaves on a v2 close

R22-1 moved the v2 drain ahead of the Hook mount handback, but the drain it
moved only acts on cleanup debt, and a Hook-configured turn writes none: it
acquires through hooks.acquire(), while oweHostedTurnCleanup and cleanupOwed
both sit in the else branch. What that turn leaves is a held Runtime recorded
client-side, and the only drain of that set was the v1 close. So the handback
still went out over a live background run, the guard refused
runtime_session_busy, and the detach parked on 503 for as long as the process
lived — the wedge R22-1 was raised for.

Drain heldRuntimeTurns in the v2 block too, mirroring v1, after the debt drain
and before hooks.close so the R22-1 order holds. Tolerate only 404
runtime_session_not_found; v1's 503 reconciliation tolerance is deliberately
not carried over because the releaseMount that follows would refuse with that
same 503 anyway, so tolerating it would only claim an unproven stop. Every
other refusal throws and the route parks honestly, as before.

The R22-1 witness was vacuous for this shape: it called f.owe() to manufacture
a descriptor the Hook path never writes. The new witness asserts there is no
cleanup debt, seeds heldRuntimeTurns instead, and keys the busy guard to the
held turn's own Runtime — the lease drain also issues {drain:true} with no
descriptor, so an opts-only check would let it clear the flag.

Red before: expected 200 "OK", got 503 "Service Unavailable". Green after.
hosted-harness-session 716/716 (full file, run alone), hook+mcp 208, build rc0,
Prettier clean.

* fix(cli): tolerate an unprovable held-Runtime drain on a v2 close

R24-1's drain tolerated only 404. The reasoning for leaving out v1's 503
tolerance was that the releaseMount right after it would refuse with the same
503 anyway, so tolerating could not unwedge the close. That premise is false:
HostedHookSession.close() guards the handback with
`ownsBroker && (mountHeld || (terminal && deterministicOwner))`, and a reload
builds a fresh instance whose mountHeld stays false until a Hook turn acquires.
After a Harness restart a non-terminal detach therefore never calls
releaseMount at all — the close would have succeeded, and the throw was the
only thing blocking it. Because the entry is deleted only on success or 404,
every retry re-seeded the same id and failed identically, leaving the mount
claimed, the lease pinned and the publishers open indefinitely.

Carry both v1 tolerances now, with the compensation that makes them honest:
503 runtime_reconciliation_required deletes the entry and marks the drain
unproven, and the two v2 monitor-loop stops settle runtime_lost rather than
stop_requested when it is. Other refusals still throw; there is no cascade to
hand those to.

Witness: a v2 settle_resources close whose held-Runtime release rejects 503
must answer 200, delete the entry, and stop the loop with runtime_lost.
Red before (expected 200, got 503), green after. Forcing stopSettle back to
stop_requested kills the test on the runtime_lost assertion, so the
compensation is pinned and not merely present.

hosted-harness-session 717/717 (full file, run alone), hook+mcp 208, build rc0
with no type errors, Prettier clean.

* fix(cli): settle a blocked v2 Session on the runtime_lost line

The v2 stopSettle added for R25-1 keyed only off heldDrainUnproven while its
comment claimed it matched the v1 close, which also folds in session.blocked.
A Session whose own settlement already failed never proved to the Runtime that
anything stopped, so certifying stop_requested there writes a terminal run
state that rebuildFromRuntimeLost refuses to recover, and reports a cancelled
task's writer as settled.

The blocked-Session half of this gap predates R25-1 — v2 hardcoded
stop_requested before it — but the parity claim in the comment did not, and
the term belongs here either way. v2 has no heldDrainFailed variable because
that arm throws instead, so the expression is session.blocked ||
heldDrainUnproven.

Witness: a v2 settle_resources close with blocked=true, no held Runtime (so
the drain is trivially proven) and an injected monitor loop must stop the loop
with runtime_lost. Red before on exactly that assertion, green after; the loop
assertion cannot pass vacuously because a failed injection fails the spy check.

* docs(cli): scope the v2 held-Runtime drain to the shapes that reach it

The R24-1 drain reads session.heldRuntimeTurns, and its comment claimed it
covered a Hook-configured turn's background run. No producer registers that
obligation for a Hook turn: finish() calls broker.release() only under
!mcp && !hooks && !cleanupOwed, runtimeHeld is assigned only in that release's
409 runtime_session_held catch, cleanup() returns early on hooks, and every
add() site keys on runtimeHeld or a cleanup hold. So the loop is empty for
that shape and the close still parks on recovery-required.

Comment-only in production. State the reachable scope (a wake turn's held
aftermath, a cleanup hold), say plainly that the Hook shape is not covered,
and record why the load restore keeps its v2 gate: a restored id can be a
deterministic Hook owner, and this drain releases terminally, which would
tombstone the id a reload recomputes.

Pin the producer fact so the drain is not mistaken for a fix of that shape:
with the same fixture and the same 409 mock, the no-hooks turn ends with
runtimeHeld true while the Hook turn never calls release and ends false.
Retitle the R24-1 case to stop claiming the Hook shape; it seeds the
obligation the way the reachable producers do.

Lifting the gap needs a keep-READY drain, deferred to a follow-up: the
terminal release used here cannot stop a deterministic owner's background
run without tombstoning it (R13-1/R14-1).

Tool-turn suite 573/573, the three v2 witnesses green, build rc0 with no type
errors, Prettier clean.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-08 04:02:29 +00:00
jinyeandQwen-Coder 32d22f950c feat(managed-agent): support durable structured user questions (#16433)
Connect Hosted question actions, structured answers, durable continuation, and the WebShell question form. Cover canonical response replay and pending-question recovery across Harness and Java context restarts.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 09:23:44 +00:00
jinye 07ed0e260e feat(managed-agent): support full model configuration at session creation (#16153)
* feat(managed-agent): support session model configuration

* codex: fix CI failure on PR #16153

* codex: address PR review feedback (#16153)

* codex: address PR review feedback (#16153)

* codex: address PR review feedback (#16153)

* codex: address PR review feedback (#16153)
2026-11-03 04:23:45 +00:00
jinye 41ce46af24 feat(managed-agent): add pinned extension package management (#15490)
* feat(managed-agent): support pinned managed extension packages

* fix(managed-agent): align extension package CI contracts

* test(managed-agent): pin package audit ambiguities

* fix(managed-agent): report unavailable settings and satisfy broker style

* fix(managed-agent): retain queue inventory in extension integration

* test(managed-agent): preserve pinned context across Todo refresh
2026-10-26 01:19:27 +08:00
jinye 0d7518f0a2 feat(managed): add session permission rules and auto approval (#15448) 2026-10-24 14:14:42 +00:00
jinyeandQwen-Coder a147c39712 feat(managed-agent): Add a shared Harness pool for Kubernetes (#15255)
Route sessions to durable instance and boot placements with fenced writer grants, resource-idle ownership release, EndpointSlice discovery, and private mTLS deployment templates.

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-23 17:22:19 +00:00
jinyeandQwen-Coder f0a47611ab refactor(managed-agent): Remove pre-release compatibility paths (#15263)
* refactor(managed-agent): Simplify first-release contracts

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

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

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

Restore schema rationale comments while retaining the V1 baseline.

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

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

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

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

---------

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

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

* feat(managed): manage MCP servers within existing sessions
2026-10-22 14:07:46 +00:00
a37fb3a786 test(sdk-java): pin the idle watchdog tick without a wall-clock race (#15103)
* test(sdk-java): pin the idle watchdog tick without a wall-clock race (#15097)

The idle-abort test used a 2400 ms wall-clock ceiling to catch a half-bound watchdog tick. That calibration left only ~300 ms over the nominal 2100 ms abort, so scheduler latency on a loaded CI runner failed the test even though the shipped 200 ms tick was intact. Read the queued watchdog tick's delay directly instead, which pins the interval without depending on when the scheduler runs, and keep the wall-clock assertions to the lower bound plus a generous fire-at-all ceiling.

* test(sdk-java): pin the idle watchdog period structurally (#15097)

Read the queued watchdog on the scheduler thread, where a periodic task
cannot be off the queue mid-run, and assert it is the only tenant. Sample
the delay after the first tick so a period that outgrew the interval
fails, pin the interval formula on both sides without wall-clock slack,
keep a 3000 ms ceiling for late-fired aborts, and name the test after the
guarantee its body asserts.

* test(sdk-java): pin the fixture tick in the idle-abort test (#15097)

The probe derives its expected tick from the production formula so a
formula mutation could loosen the bound the same test checks. Assert the
fixture's 200 ms value directly as well; the dedicated formula test still
covers the 100 ms floor.

* test(ci): exempt TUI parity tests from the no-AK gate (#15097)

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

* fix(ci): drop redundant TUI parity exemptions from the no-AK guard (#15097)

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

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-10-22 13:51:33 +00:00
qwen-code-dev-bot 9d1e41f55c test(sdk-java): close the body-less pre-contract 404 connection (#15167) (#15172)
The fixture answered the first prompt with a body-less 404 and no
Connection: close. On Java 11 the JDK server drops that connection while
the HttpClient keeps it pooled, so the follow-up submission sometimes
read EOF and surfaced as an outcome-unknown admission instead of the
expected receipt.

Mark the response non-persistent like the other body-less fixtures. A
standalone Java 11 probe failed 21/500 rounds without the header and
0/500 with it.
2026-10-22 15:42:46 +08:00
jinyeandQwen-Coder 59d21e1c69 feat(managed-agent): Safely settle recovery and continue sessions (#15131)
* feat(managed-agent): Safely settle recovery and continue sessions

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

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

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

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

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

---------

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-22 08:09:13 +08:00
jinyeandqwen-code-dev-bot d2f2895fab feat(managed-agent): import enabled skills at session creation (#14930)
* feat(managed-agent): import enabled Skills when creating sessions

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

* codex: address PR review feedback (#14930)

* codex: address PR review feedback (#14930)

* codex: address PR review feedback (#14930)

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-10-21 04:16:25 +00:00
f8b3031f73 fix(ci): repair Java gates and report failures on PRs (#14522)
* fix(ci): surface Java failures on PRs and manual main runs

* fix(ci): address review findings on PR failure reporting

- Gate the PR failure comment on infra_only like the issue path: an
  infrastructure-only death is rerun once, not reported, and the comment
  has no correction path once the retry goes green.
- Tolerate a missing or truncated run-jobs.json in the comment command so
  an API failure degrades to naming no job instead of failing analyze and
  losing both the PR comment and the issue.
- Drop the leftover 'main-branch ... on main' wording from the per-commit
  issue body now that merged-PR failures reach it.

* fix(managed-agent): keep daemon recovery waits re-driven and deflake hosted tests

The daemon refuses a lifecycle phase with hosted_lifecycle_recovery_required
while it settles a previous prompt. The coordinator classified that refusal
as a hook refusal and published the operation RECOVERY_BLOCKED, which the
lifecycle store then refused on every later attempt even after the daemon
recovered. Treat it like the other self-clearing waits: budget-exempt, with
the daemon's own code once past the budget, still re-driven by the recovery
scan.

Deflake the gate's remaining timing-sensitive spots: give the Hosted close
the dispatch backoff's own window, allocate fault-gate ports below the
ephemeral range, widen the action renewal lease, and size the SSE stall test
off a 1200ms idle budget so a loaded runner can still answer inside it.
The E2E freeze loop now waits up to 5s for InnoDB transactions to drain.

* fix(ci): resolve fork approval runs in the Java failure reporter

An approval run records the base repository as head_repository even when
the PR head lives in a fork, so the owner:branch listing filter found
nothing and fork approval failures produced no comment, no issue and no
diagnostic. Resolve by exact head SHA instead, fail soft with a warning
when the listing errors or nothing resolves, exclude closed-unmerged PRs,
gate the infrastructure rerun to destinations that file, and publish the
issue url before the route edit can abort the step.

* fix(ci): preserve PR failure reports across issue and runner errors

* test(sdk-java): wait for the re-registered attachment's heartbeat

The close-by-id success arm asserted a new heartbeat inside a fixed 60ms
window while the heartbeat interval is 10ms. A loaded runner delayed the
periodic task past that window and failed the assertion on main, though
the attachment was still beating. Poll for the next beat with the same
5s bound the other heartbeat waits use.

* docs(managed-agent): name the recovery wait in the public contract and pin the wrapped refusal

The contract's failure_code description enumerates the codes a
recovery_blocked lifecycle operation reports; the daemon-recovery wait
this change published added a third code without touching that text or
the contract version. Name hosted_lifecycle_recovery_required in both
the public and WebShell descriptions and bump the contract to 1.63.0
with a version note; regenerate the WebShell types.

The recovery-refusal test threw the refusal directly, but the SDK wraps
it in MutationOutcomeUnknownException once the non-idempotent detach may
have been delivered — the wrapped shape is why daemonRefusalCode walks
the cause chain. Parameterize the test over the wrapped shape so a
regression that stops unwrapping fails it.

* fix(ci): cover SDK lock holds and refresh InnoDB freeze probes

* test(managed): allow writer grant latency in failover fixtures

* fix(managed-agent): name the refusing store route and pair writer grants with store heads

The Hosted failover lane failed with the store's shared writer-conflict
text and no way to tell which operation with which grant was refused
(review #14522's discriminator: a grant the wake never re-acquired vs a
grant the store already judged superseded).

ManagedSessionStoreHttpError now carries the refusing store route in its
own field (public messages pass through unchanged). On a 409 the writer
annotation captures, sanitized (boot id, never a token), the grant the
writer presented and the store-head generation/revision/committed/state
verdict probed at the same instant — reaching it from a commit retry and
from the renewal path that otherwise swallows the refusal silently. The
daemon's recovery print and turn-failed print render status, stable
remote code, route and that pairing.

Also log the sanitized acquiredWriterGeneration and the restore-head
match pair on acquire/restore, so a healthy takeover proves from both
sides that the previous boot's writer stayed fenced.

* test(managed): establish journal ownership before broker outage

* test(cli): wait for startup and saved-plan recovery before asserting

* test(sdk-java): wait for heartbeats instead of fixed windows

* test(managed-agent): isolate automation fault fixtures and await deadlock recovery

* test(ci): pin the awaitility fail-fast guard, not its exact prefix

`HostedTeamLeadMySqlIT`'s waiting helper now configures
`ignoreExceptionsInstanceOf(CannotAcquireLockException)` before chaining
`failFast`, so the class no longer contains the literal
`await().failFast("The case's @Timeout interrupted it"` and the
hosted-process-ci guard failed on the merged head.

The invariant is that every wait goes through the helper that fails on the
`@Timeout` interrupt; the single-`await()` assertion still pins that. Match
the failFast guard itself so configuring awaitility first stays legal.

* fix(ci): record the current workflow size in .size-baseline

The ratchet is meant to turn workflow growth into one reviewed line, but
main-ci-failure-issue.yml landed at 23219 bytes against a recorded 20935,
so the 2284-byte remainder rode inside the 4096-byte allowance unrecorded
and the grow-by-adding-a-step path stayed invisible.

Record the byte size the file actually has at this head.

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

* fix(managed-agent): normalize boolean literals in schema checks

* test(managed-agent): isolate destructive schema validation

* fix(ci): release cancelled fast PR gate concurrency

* fix(ci): release PR gate when tier selection is cancelled

* chore(web-shell): regenerate managed agent API types

The merge of origin/main brought a new
`hosted_lifecycle_recovery_required` clause into the Managed Agent OpenAPI
description, but the committed `managed-agent-api.ts` was resolved by hand
instead of regenerated, so `managed-agent-api.test.ts` drifted from its
generator and failed the required Test lane.

Regenerated with `node scripts/generate-managed-agent-api.mjs`; the only
change is the aligned description line.

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

* fix(managed-agent): restore the daemon recovery wait in failure_code docs

The e62a4c7 main merge kept main's failure_code descriptions and dropped
hosted_lifecycle_recovery_required from both. Regenerating the WebShell
types from that contract then removed the clause there too, although the
coordinator still publishes this code for recovery_blocked lifecycle
operations. Restore it in the contract and the generated types.

* fix(ci): merge paginated jobs pages before the comment helper reads them

gh api --paginate prints one JSON document per page, while the PR-comment
helper parses the jobs listing with a single JSON.parse. A failed run wider
than one page made the parse throw and rendered a summary naming no failed
job — the links that make the report actionable. Slurp-merge the pages at
the fetch, which also turns a parked error body into the same empty-list
degradation the per-commit body already handles. Bump the recorded byte
size for the prose comment.

* style(managed-agent): fix the OpenAPI version indent so prettier passes

The v1.68 bump in this PR left info.version indented four spaces, so
Lint & Static failed on `node scripts/lint.js --prettier`:

  [warn] packages/managed-agent-server/src/main/resources/openapi/managed-agent-public-api.openapi.json

Formatting only; no contract change.

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-19 08:04:12 +00:00
Shaojin Wen 46bf492d05 feat(managed-agent): bound half-open Hosted event streams behind a keepalive capability (#14135) (#14512)
* feat(managed-agent): bound half-open Hosted event streams behind a keepalive capability (#14135)

The Harness advertises its events-route keepalive as event-keepalive/1,
and the Java client arms the idle watchdog only against a Harness that
advertises it, so a rolling upgrade never aborts the quiet streams of an
older Harness. The abort is now the typed HostedHarnessStreamIdleException:
the coordinator retries it through the existing re-attach path and never
settles or cancels the Turn on silence alone. The watchdog ticks every
tenth of the bound, so an abort lands 45 to 49.5 s after the last byte.

HostedEventStreamLivenessIT runs the probes on the packaged chain behind
a TCP relay that holds the events connection half-open or stalls it, and
the hosted-harness-mysql fork budget grows to 4793 s. A design note
records the numbers, the capability, the outcome and the other Hosted
streams.

Closes #14135

* test(managed-agent): frame relayed requests and see virtual-thread readers in the liveness gate

The event-stream relay classified a connection, rewrote its Host header
and matched keepalives within single reads, so a request or a keepalive
split across TCP segments escaped the stall and half-open injection or
the cadence count. It now buffers each request head, forwards the body
by its Content-Length, matches keepalives across reads, and records and
resets a request it cannot frame. HostedEventStreamRelayTest pins each
split.

The half-open probe checked reader release with Thread.getAllStackTraces(),
which leaves out the virtual threads Turn readers run on. It now reads a
JDK thread dump, and first requires that dump to show the parked reader.

* test(managed-agent): count each keepalive one relay read carries (#14135)

The relay recorded one stamp per read that held a keepalive, so two keepalives in one read counted once, and the split-keepalive case could report a size of 1 whenever the pieces happened to land together. It now counts every match in the read window; the carried tail is a byte shorter than a keepalive, so none counts twice. A new case sends two keepalives in one write.

* test(sdk-java): park the published-window probe mid-interval (#14135)

The watchdog's ticks run from the stream's creation, and the probe parked right after it. A 2000 ms bound is a whole number of half-budget 1000 ms ticks, so that park sat on a tick of the mutant too, and wherever the scheduler ran a few ms late (macOS) the mutant aborted at ~2000 ms and passed. Parking 300 ms in puts the park mid-interval for both ticks: the shipped 200 ms tick aborts at 2100 ms and a 1000 ms tick at 2700 ms, so the limit drops to 2400 ms.
2026-10-18 14:14:37 +00:00
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 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
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
jinyeandQwen-Coder 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-16 18:18:04 +00:00
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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

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

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

* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: 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
jinyeandQwen-Coder af9bd8ed05 feat(managed-agent): add Shell Full Access and session approval switching (#14323)
* feat(web-shell): integrate Managed session workflow

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

* fix(managed-agent): align schema projections and CI probes
2026-10-16 14:12:04 +08:00
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
+2 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

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

* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-autofix <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen Autofix <[email protected]>
Co-authored-by: qwen-autofix <qwen-autofix[bot]@users.noreply.github.com>
2026-10-16 11:32:57 +08:00
jinyeandQwen-Coder 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 <[email protected]>
2026-10-16 02:40:37 +00:00
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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-autofix <[email protected]>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
2026-10-15 12:04:21 +00:00
jinyeandShaojin Wen 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 <[email protected]>
2026-10-15 03:56:11 +00:00
jinyeandQwen-Coder 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
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 WenandQwen-Coder 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>
2026-10-13 05:05:40 +00:00
Shaojin WenandQwen-Coder 21b78c55d5 feat(managed-agent): member board tools ride the session_command channel end to end (H4e-b3b) (#13928)
* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>
2026-10-12 23:53:21 +00:00
haochenandZhangchenhao-111 008c0de746 docs(sdk): clarify empty core-tool allowlists across SDKs (#13949)
Co-authored-by: Zhangchenhao-111 <[email protected]>
2026-10-12 13:19:59 +00:00
jinyeandQwen-Coder f70344345d feat(managed-agent): recover Hosted G3 parked turns (#13777)
* feat(managed-agent): recover Hosted G3 parked turns

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

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

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

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

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

* 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 <[email protected]>

* 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 <[email protected]>

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

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

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

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

* 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 <[email protected]>

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-12 13:05:52 +00:00
jinyeandQwen-Coder 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 <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
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
易良andQwen-Coder 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>
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 <[email protected]>
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 <[email protected]>

* 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 <[email protected]>
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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>
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 <[email protected]>
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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>
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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-11 04:50:38 +00:00
易良andQwen-Coder 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>
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 <[email protected]>
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)
* 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)
* 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 Wenandwenshao 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 <[email protected]>
2026-10-09 11:39:41 +00:00
Shaojin WenandQwen-Coder 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 <[email protected]>

* fix(managed-agent): answer the round-4 review and the R1-5/R1-9 recovery gaps

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

* fix(managed-agent): pin the child terminal-result query's two decisions (R1-90)

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

* fix(managed-agent): recheck mount ownership after hook side effects and settle the fixture wake before close

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

* fix(managed-agent): translate the oversized launch envelope into its byte_limit refusal (R1-8)

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

* fix(managed-agent): close the R1-round's deferred Criticals and pin their regressions

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

* 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 <[email protected]>

* fix(managed-agent): renumber the H4b migration to V52 past main's V51 lifecycle

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

* fix(sdk-java): give the unconfirmed lifecycle-detach heartbeat room on busy runners

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

* 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 <[email protected]>

* fix(managed-agent): give the fence its claim kind and keep stale authority out of the client

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

* fix(managed-agent): authorize claimed child cleanup through the lifecycle commit gate

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

* 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 <[email protected]>
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