Commit Graph
2334 Commits
Author SHA1 Message Date
易良andQwen-Coder 26c3fb7a5a feat(core): add a convergence reminder for read-only exploration (#13601)
* feat(core): add a convergence reminder for read-only exploration

Share an advisory exploration allowance across core and daemon tool loops without adding a halt or changing tool permissions.

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

* fix(cli): keep halted tool batches out of the exploration budget

recordDaemonToolCalls fed the exploration budget at the top of its
per-call loop, before the per-turn cap and global-duplicate checks. A
batch that those checks halt is skipped whole and never executes, yet
its calls still counted toward the budget and still resolved every tool
name through the registry. Move the recording after both halt checks so
only calls the turn actually runs feed the budget, and a capped turn
performs no registry lookups.

Also complete the client-goal Config double with getMaxToolCallsPerTurn
and getToolRegistry, which the new budget call sites require.

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

* fix(core): roll back the exploration budget and turn cap on ModelFallback

A model fallback restarts the attempt from scratch exactly like a retry
(Turn clears pendingToolCalls for both), but the exploration budget's
rollback and LoopDetectionService's per-turn cap rollback both fired on
Retry only. The discarded attempt's read calls stayed counted, inflating
the phase counter by every failed hop of the fallback chain, so the
single reminder (and the cap) could fire before the executed evidence
reached the allowance — the same drift the daemon path avoids by
recording only the surviving batch.

Wire both rollbacks to ModelFallback beside Retry, and pin the core-side
budget wiring (record/commit/rollback/reset, the session-disable gate,
and the duplicate-call-id dedup) with client integration tests that
drive real tool-result continuations; each guard was verified to fail
under its targeted mutation.

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

* style(core): apply prettier to the new convergence-reminder tests

The Lint lane's whole-repo prettier check was short-circuited by the stale
freshness gate, so these lines were never formatted. Formatting only.

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

* fix(core): keep guard evidence across ModelFallback and close review gaps

The review-pr round on 69f2e313 filed one Critical and ten Suggestions;
this addresses the Critical fully, seven Suggestions, and declines two
with rationale.

Critical: the widened per-turn cap rollback cleared the loop detector's
accumulated guard evidence on ModelFallback. A production fallback can
only precede its own attempt's calls (the chain is gated on the failed
attempt yielding no candidate output), so there was nothing to roll back
— but the evidence clears were not inert: the chain is per-request, so a
capacity-limited primary can prefix every round-trip of one turn with
ModelFallback, zeroing the consecutive-identical streak, the
shell-inspection streak and the adaptive cap's repeat tracker on each
round-trip, silently disabling two always-on guards until the 1000-call
backstop. ModelFallback now returns early without touching accumulated
state; the Retry branch keeps its full rollback. Three new
loopDetectionService tests pin the reachable shape (per-round-trip
fallbacks with committed evidence) and redden under the widened branch.

Also: the client budget's lifecycle moves behind a shared
isAttemptRestartEvent predicate (R1-7); its block moves below the
statement the pre-existing comment documents (R1-10); a concurrent side
query no longer resets the running turn's exploration phase (R1-11);
lsp and tool_search classify as exploration reads without retyping
their Kind.Other registration (R1-2a); the unreachable model_fallback
fixture is replaced with a cross-round-trip shape (R1-1 note 3); reset()
gains a commit-floor unit test (R1-4); bridged tool_call args wiring is
pinned in both runtimes (R1-5); the design docs record the threshold
boundary, the subagent-loop exclusion, the budget-local rollback split,
the todo-write cadence consequence and the discovery-tool
classification, in both languages (R1-3, R1-2b); settings.md documents
the reminder at the maxToolCallsPerTurn row (R1-9).

Declined: insertion-site debug logging (R1-6) — the nearest reminder
peer (activeTodoReminder) logs nothing either, and the injected text
already persists in the transcript; reusing the executor's
ensureTool-resolved name for daemon classification (R1-8) — the
asymmetry is self-healing (ensureTool materialises factory targets into
the map the classifier reads), the only divergent shape found is inert
in fork registries, and the perf half is noise by the reviewer's own
measurement. Both are recorded as follow-up candidates.

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

* style(core): apply prettier to the convergence-reminder files

Run Prettier reported style issues in 5 files touched by this PR
(docs/users/configuration/settings.md, client.test.ts,
loopDetectionService.ts, loopDetectionService.test.ts,
tool-exploration-budget.test.ts), which failed the Lint & Static job.
Formatting only, no behavior change.

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

* fix(core): isolate the exploration budget per interaction and close review gaps

* fix(core): commit the exploration floor on an in-round-trip phase reset

`record()`'s non-read branch zeroed `calls`/`reminded` without committing the
new floor, so a `rollback()` later in the same round-trip resurrected the
previous phase's `committedCalls`/`committedReminded`. The retried attempt was
then nagged after a single read, and the resurrected reminder latch was
re-committed by every later `Finished`, silencing the reminder for the rest of
the interaction. Commit the floor here, as `reset()` already does.

Also stop crediting a bridge-refused discovery target as a read: `tool_call`
unconditionally refuses `tool_search` and itself, so a bridged call to either
performs no read and must stay unclassified like a name the registry refuses.
A bridged `lsp` still classifies as read.

The strengthened rollback assertion is red on the previous code and green with
the commit; the new bridge case is red without the refusal guard.

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

* fix(core): drop the duplicate Goal-branch exploration budget reset

The Goal branch's toolExplorationBudget.reset() was the only budget
entrypoint with no ownership or side-query gate, contradicting the
invariant the design docs state for every other one. Goal is a member of
startsInteraction, so the interaction-start block already reset the budget
and claimed ownership for every non-side-query Goal submission, and nothing
records into the budget between the two points - making this a second no-op
reset. For a Goal submitted as a concurrent side query the
interaction-start reset is correctly skipped, but this one still fired and
zeroed the running turn's read-only phase, so that turn never converged.

Deleting the duplicate removes the ungated entrypoint entirely rather than
adding a second copy of the guard.

Also pin the Stop-hook restart reset, which no test exercised: the nested
restart is submitted with SendMessageType.Hook, which is not an
interaction-starting type, so that reset is the only thing clearing the
read-only phase across a hook restart.

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

* fix(core): drop the inert ModelFallback arm from checkAlwaysOnSafeties

The arm returned false with no mutation between it and the fall-through
`if (event.type !== LlmEventType.ToolCallRequest) return false;`, and
ModelFallback is not a ToolCallRequest, so it already returned false at the
same point with zero mutations. It could not change behaviour.

Delete it and re-point the two references that cited it as a detector-side
pin: the isAttemptRestartEvent docstring (now names the non-ToolCallRequest
early return that actually carries the no-op) and the client.test.ts fallback
comment. The three loopDetectionService.test.ts ModelFallback cases stay: the
invariant they assert (a per-round-trip fallback must not clear committed
streak evidence) is real and documented in the design doc, and they now pin it
at the tier that carries it instead of at dead code.

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

* refactor(core): delegate the exploration-budget phase reset to reset()

record()'s non-read branch was a byte-identical copy of reset()'s body,
with a comment asserting the equivalence instead of the code expressing
it. The two copies already diverged once: reset() committed the zeroed
floor while the in-round-trip copy did not, and that divergence was
R3-1, a Critical in which a rollback after a mid-round-trip phase change
resurrected the previous phase's count and its delivered-reminder latch.
Delegate so a future phase field cannot be added to one site only.

Addresses review finding R4-1.

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

* test(core): cover the goal-permitted Stop-hook exploration-phase reset

The goal-permit branch restart reset at client.ts:5354 had zero test
coverage. Its !goalPermit sibling at :5463 is pinned by "gives a blocking
Stop hook continuation a fresh exploration phase", but no test installed a
goal runtime alongside a blocking Stop hook, so deleting this line changed
nothing any test observed. The nested restart is submitted with
type: SendMessageType.Hook, which is not an interaction-starting type, so
the in-branch reset is the only thing that clears the phase.

Extends the existing fixture rather than building a new one. Verified
discriminating: deleting client.ts:5354 turns this case red
(expected 2 to be +0) while the !goalPermit sibling stays green.

Addresses review finding R3-5.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-13 09:02:46 +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
易良 75470716ba test(managed-agent): land the deferred findings from the definition-execution review (#13980)
* test(managed-agent): land the deferred findings from the definition-execution review

Seven review Suggestions deferred from #13530 by its size fuse:

1. Design §5.3 claimed Shell profiles must pair with the 'default'
   approval mode, but the shipped compiler refuses only 'yolo' (three
   value vocabulary). Align both the EN and zh-CN §5.3 bullet with the
   combination rule and decision 5 the code already implements.
2. §8 lists 'no partially updated read under a concurrent update' as a
   verified property, but the FOR UPDATE read had zero test references.
   Add a two-thread H2 test holding the row lock through commit; the
   contender's locking read blocks until the holder commits. Reverting
   either FOR UPDATE to a plain read turns it red.
3. The instructionsRef guard's kind and schemaVersion legs were
   unexercised; add both negative cases next to the byteLength one.
4. The stored-definitions test was the file's only top-level it(), so no
   describe-scoped beforeEach ran for it and an isolated -t run wrote
   journals into the vitest CWD. Give it its own describe with temp-root
   setup and teardown.
5. The credential-failure half of the model_unavailable promise had no
   automated witness: a matched, unambiguous model whose switchModel
   rejects must fail the Turn before inference with zero requests.
6. The W1 recovery branch's 64 KiB cap and fatal UTF-8 decode were
   unpinned; the only negative case tripped the generic digest check.
   Add an over-cap row and an invalid-UTF-8 row, both with
   self-consistent digests.
7. metadataNeverAffectsExecution planted a metadata mode identical to
   the deployment mode and expected result, so it could not observe
   metadata being ignored. Plant a differing literal.

Fixes #13840

* test(managed-agent): drive item 2's concurrency witness through the production read

The stage-2 review found the item 2 test exercised H2's FOR UPDATE
semantics through inline SQL rather than the store method it was named
after — swapping production's latestForUpdate for latest kept the suite
green, so the witness did not cover the code under review.

Both threads now run the production store read inside a transaction over
the shared DataSource bean: the holder takes latestForUpdate's row lock
and holds it through a 300ms window, the contender's identical read must
block on that lock for most of the window. Dropping FOR UPDATE from the
production query turns the test red (verified by mutation); eight
consecutive runs stay green, and the assertion is on the blocked duration
rather than cross-thread nanosecond ordering, so the commit boundary
cannot race the assertion.

Also takes the review's non-blocking suggestion: the item 3 kind/
schemaVersion cases and the item 6 invalid-UTF-8 case now assert the
throw messages, matching the over-cap case's existing form.
2026-10-13 08:10:02 +00:00
ChiGaoandShaojin Wen 415726c28c fix(cli): window the OpenTUI transcript to fix the blank-screen resume (#13669)
* fix(cli): window the OpenTUI transcript so a long resume cannot blank the screen

@opentui allocates one native TextBuffer per mounted text element, and the
OpenTUI transcript mounted every history item at once. Resuming a session of a
few thousand records exhausted the process, and the resulting throw was silent:
it lands inside a React host-instance creation, @opentui/react's own boundary
catches it, its red-text fallback calls a jsxDEV the bundle does not provide and
throws too, and React unmounts the whole root, leaving an empty alternate screen
with a live process still accepting input.

Mount only the items near the viewport, budgeted in rows rather than items, with
spacers standing in for the rest so the scroll geometry still spans the whole
session. Estimates mirror the values the ink transcript already uses, real
heights are measured per item id and survive unmount, and the scroll position is
sampled on the renderer's frame event so an idle terminal costs nothing.

Also wrap each mounted item in an error boundary whose fallback renders nothing.
A failed item now costs one row instead of the whole UI, and the error reaches
the OPEN_TUI_TRANSCRIPT debug log.

The session picker's preview pane is clipped and does not scroll, so the view
takes an initial anchor and the preview passes 'top'.

* fix(cli): stop the transcript window moving the reading position

Three defects in the windowing's scroll handling:

The height table was cleared on a width change, which renumbered every row
offset underneath a scroll position that is itself counted in rows, so a
resize moved the visible turn by more than a hundred. The entries a resize
actually invalidates are the mounted ones, and the next frame re-measures
those.

A height correction was written back through the sticky-aware scrollTop
setter. No correction is owed: an item the measuring loop can see is already
painted at the height just read from it, and both spacers derive from offsets
a change inside the window leaves alone. Writing one moved the reading
position and, at the tail, took the shell's bottom pin off for the rest of
the session.

The position was sampled only on the renderer's frame event, which fires
after the paint, so an absolute jump — a scrollbar track click or a thumb
drag, which can land anywhere in the transcript — painted one spacer-only gap
first. The scroll bar emits a public change event synchronously before that
paint; subscribing to it covers every scroll path.

Also: the host-less session preview now follows the height prop across a
resize instead of seeding its viewport rows once at mount; the item failure
log carries the component stack and the error stack, matching ink; the two
windowing options no caller ever set are gone, so the cap and the overscan
are pinned by the tests that exercise them; and the cap's binding condition
is stated correctly in the code and in both design docs.

Adds a scroll-host harness so the frame-driven half is reachable from jsdom
at all, and five tests over it, each mutation-proved against the defect it
pins.

* docs(cli): record the two windowing follow-ups the review named

The bundle-level jsxDEV escalation Decision 8 compensates for at the item is
still live for every other subtree, and a window-only move re-executes every
mounted item's render path. Both are named in the design rather than left to be
rediscovered.

* fix(cli): measure transcript items by identity and settle pulled-in rows

Each mounted item now registers its own node under `(kind, id)` instead of
being recovered from its position among the transcript root's children, so
the tool and task cards one subagent call mints no longer overwrite a single
height slot, and the item key no longer collides as a React key either. The
rows recorded are Yoga's computed height rather than the reported one, which
is clamped to a row and would permanently charge a row to an item that
paints nothing.

An upward window move pulls in items whose spacer rows were charged at the
estimate, so the frame that first measures them now subtracts what they
turned out to cost from the scroll position — guarded to fire only strictly
above the tail, where the reader's own scrolling has already released the
shell's bottom pin. Measuring an item that was already mounted still owes
nothing. Measuring also runs when no scroll host is found, so the session
picker's preview records real heights too.

* docs(cli): state the measured mutant victims in the windowing design doc

* fix(cli): settle a transcript row whose recorded height went stale off-window

The settlement for a row the window pulls in above the viewport only fired
for an identity the height table had never seen. A row measured earlier,
evicted, and then re-measured at a different height moved the offsets under a
scroll position nothing re-anchored, so the page lurched on every wheel tick
after a terminal resize or ctrl+O.

Settle on the charge rather than on first-ever measurement, spend the charge
on the measurement that answers it, and never charge an identity the previous
commit already had mounted — which is what a `task-end` splice above the
window would otherwise do, since it shifts the index space the charging loop
walks while the identities on screen stay put.

* docs(cli): keep the windowing design doc true about the stale-height settlement

Decision 5 said the frame that *first* measures a pulled-in item settles it,
and that the charge is always the estimate; the settlement now spends a charge
on the measurement that answers it and never charges an already-mounted
identity. The mutation ledger named ten tests on the scroll-host harness and a
suite total that both moved.

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-10-13 07:49:01 +00:00
72316e6608 feat(web-shell): Reuse standard UI for Managed Agents (#13973)
* feat(web-shell): Reuse standard UI for Managed Agents

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-10-13 07:30:12 +00:00
55dcb50b35 feat(managed-agent): give a Monitor auto-stop its remote exit evidence (#13896) (#13929)
* feat(runtime-broker): split Runtime Session release into routine and drain arms (#13856)

The H3 enablement's first design call: a Runtime Session release refuses
409 runtime_session_held while a background run is provably live, and the
run holds its Runtime until its exit is proven — the turn-settle drain
that SIGTERM'd live producers is gone from the routine arm. The drain arm
(the Session close, and the control plane's close cascade through
releaseSavedSession) keeps the status → terminate → status sweep, and the
public :release route gains the optional "drain" flag that names it.

The Monitor watch joins the hold: is_monitor payloads admit their own
ledger row (background_v3_monitor), the sweep and the re-acquire scan ride
the worker's pre-built monitor-status/monitor-stop maintenance route
through a new ManagedMonitorProtocol (the mirror of ManagedShellProtocol),
and the cancel guard covers both hold families.

Design: docs/design/2026-10-11-managed-h3-enablement.md (+ zh-CN).

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

* feat(cli): hold the Runtime across turns and project platform stops (#13856)

The harness side of the H3 hold: a turn whose routine Runtime Session
release is refused 409 runtime_session_held settles clean — the designed
hold, not a wedge — and the Session records the owed drain. The Session
close drain-releases every held Runtime ahead of the loop stops and the
publisher close, and a drain that cannot prove a stop fails the close
honestly (503, reopenable) with monitors parked runtime_lost, the same
unproven shape as a blocked settlement. The recovered-lease handback at
close drains too; the cancellation path's fire-and-forget handback keeps
routine semantics (a cancel route never stops a live run).

A8: a platform-driven stop now projects as cancelled/stop_requested
instead of a bare exited. The worker registry marks any stop it was asked
to make (terminate/stopSession/stopAll), the marker rides the capture
finalize's process evidence (present only when set, so the foreground
lane's closed key set never sees it), and the publisher settles the record
through requestStop + settleStopRequested; the monitor lane answers the
same marker through the observation loop's onExit. An unproven end still
settles failed, marked or not — the marker never mints a stop nobody
proved.

Witnesses: the publisher background suite settles stop_requested with the
marker and exited without; the monitor loop suite covers the platform-stop
arm; the hosted close-drain test watches the turn hold and the close's
drain release; the turn suite pins the held/busy classification split.

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

* feat(core): enable the H3 monitor_run domain and the shell child_run kind (#13856)

The gate lift itself, after the Runtime hold landed and the physical
acceptance pass ran: monitor_run joins MANAGED_SESSION_ENABLED_DOMAINS and
shell joins MANAGED_SESSION_ENABLED_CHILD_RUN_KINDS beside child_agent.
The turn's background admission reads the kind gate now — the child_run
domain never joins the plain list, so the old plain-domain read was a
latent refusal the lift would otherwise have shipped.

The pinned-disabled suites flip deliberately, following #13846's practice:
the pin tests assert the enabled lists, the authority and publisher suites
commit through the real gate, and a test that needs a gate shut closes it
per test. The H3 design's status and the §13 hold text record the lift and
the runtime_session_held coding.

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

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

* fix(core): close over a result manifest's page envelopes at commit (#13856)

The staged HTTP session store persists a staged resource only when a
commit's resource closure names it, and the walk that builds the closure
never descended into a result manifest — so the page envelopes the
manifest names stayed staged in memory and never persisted, and the
Artifact read chain 404'd at the page hop. The #13856 rig pass's B2 arm
proved it physically: the producer's bytes landed (the content rows held
them, digest-true), the manifest sealed complete, and the page envelope
it named was in no table.

The walker now collects the page references a manifest names. Pages
themselves name only segment digests (never refs), and segments persist
through the tool-result publish route directly, so the chain closes:
record → manifest → pages → segments.

Witness: a manifest naming a page ref closes over exactly it; removing
the branch turns the test red. The B2 rig arm re-runs green over it.

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

* docs(managed-agent): record the H3 enablement's physical acceptance (#13856)

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

* test(core): drop a stray enablement flag from the shell acceptance case (#13856)

The case commits a `child_run` of the shell kind, which the production
gate now admits, so the switch it flipped was never this suite's to
flip — and `npm run typecheck` refused the file over the unknown
property.

* test(cli): run the H3 gates at their production default (#13856)

Both suites mocked the monitor_run domain and the shell kind shut and
asked each case to open them, so a case that never thought about the
gate exercised a refusal production no longer produces. The flags now
start open and a case that needs a gate shut closes it per test, and
the kind mock sends every kind the production list carries through the
real gate; only a kind production has not enabled — a workflow planting
— stays a fixture, admitted so its case runs what it is about.

* fix(cli): drain the Hook-owned Runtime at Session close (#13856)

A Hook-configured Shell turn shares the Hook broker, and its `finish()`
skips the release, so a background run that outlives the turn holds that
Runtime and never reaches the Session's own held-Runtime drain: the close
died on the Hook session's routine release refusing `runtime_session_held`,
answering 503 forever and leaving the run alive. The close is the drain arm
— an acquire still hands an earlier owner back without stopping anything,
because the Session lives on.

Co-Authored-By: Qwen Code <[email protected]>

* fix(cli): park a Monitor auto-stop that cannot prove its stop (#13856)

The quota and the idle window run in the Harness while the watch runs in
the worker, and the client control route admits MCP and Hook operations
only — no Harness-side `monitor-stop`. A remote handle's terminate
therefore stopped nothing, yet the loop settled the record at
`max_events`/`idle_timeout`: the task read as ended while the watch kept
running and kept its Runtime held. A handle now reports whether its stop
is proven, the supervised watcher answers from the supervisor's evidence,
and an unproven auto-stop parks the record on the runtime_lost line the
`stop()` call already uses. Giving an auto-stop its own remote exit
evidence is recorded as future work in both languages.

Co-Authored-By: Qwen Code <[email protected]>

* fix(cli): let a parked Monitor consume the exit evidence that follows (#13856)

The park ended the loop's observing and its record in one step, so the
watch's own later end had nowhere to land: `onExit` and `stop` both returned
early over `ended`, and the record stayed `recovery_blocked` with a null
stopReason even after the worker exited, its hold released and its capture
published complete. Observing and the terminal settle are separate flags
now, so a park keeps the record open for the evidence that follows — a
natural exit, a platform stop, or the close's proven drain.

A terminal settle also clears the recovery reason a park or a rebuild left
on the run: the record contract admits no reason for `settled` or
`cancelled` and only a quota reason for `failed`, so carrying `runtime_lost`
into them refused the very commit that proved the end.

Co-Authored-By: Qwen Code <[email protected]>

* fix(cli): keep a failed close's Monitor park open for the evidence (#13856)

`stop()` marked the record terminal before it knew which arm it would take,
so the `runtime_lost` park closed the very record it left unsettled. That arm
is what a held-Runtime drain failure asks for: the close answers 503 and
keeps the Session and its publisher alive for a retry, and neither the
retry's proven drain nor the watch's own later end could reach the record —
it stayed `recovery_blocked` with a null stopReason over a worker that had
exited, a hold that had released and a capture that had published complete.
Only the arm that actually settles now marks it terminal.

Co-Authored-By: Qwen Code <[email protected]>

* feat(managed-agent): give a Monitor auto-stop its remote exit evidence (#13896)

A Monitor's quota and idle window run in the Harness while its watch runs
in the worker, and the client control route admitted MCP and Hook
operations only, so the remote handle's terminate stopped nothing and an
auto-stop could only park its record (the #13889 interim).

The Broker now admits Monitor operations on the client control route as
recovery kinds, and only for one of the Runtime Session's own watches: the
operation names the execution that started the watch and the Runtime call
the worker derived its unit from, both checked against the watch's hold
row in the ledger; anything else is refused before the worker sees it. A
worker answer of `exited` settles that hold row there, as the release
sweep would. These controls run outside the control fence, as the
re-acquire scan's status ask does: the hold row already keeps the Runtime
Session from releasing while the watch may run, and a fenced auto-stop
still waiting on the worker would turn its own turn's routine release
into runtime_session_busy and block the Session.

The CLI gains `monitorControl()` with the watch vocabulary
(running/exited/unknown, `exited` only with evidence), and the remote
handle's terminate issues `monitor-stop` and proves the stop only on
`exited`; any other answer, a refusal or a failure keeps the parked line.
The worker answers only after the capture's finalize has run, and that
finalize hands the exit to the loop while the auto-stop still waits: the
exit is the stop's own evidence, so the loop settles `max_events` or
`idle_timeout` there, after the delivered final output, and the worker's
answer finds the record settled. A quota reached by the final window of an
exit settles from that exit with no stop: the finalize is the proof, and a
stop issued from inside it would wait on that same finalize.

A stop now waits on a round trip, so the loop runs one at a time: only a
loop that still observes starts an auto-stop, the close's stop waits out
an auto-stop in flight and acts only on its park, and a stop keeps the
settlement its own exit made before its answer returned. Since #13889
leaves a parked record open to the evidence that follows, the finalize's
stop marker would otherwise settle the record ahead of the auto-stop, and
the auto-stop's own settle would then conflict.

Worker-side quota and idle bounds are not added: the record's quota counts
the Harness's debounced windows, which the worker cannot reproduce, and a
hold whose Harness never returns is a family-agnostic problem recorded as
follow-up work.

* fix(managed-agent): pin the Monitor control review gaps (#13896)

A Monitor operationId that no hold row can carry — one with a NUL, or one
too long once the row suffix is added — reached the ledger lookup, whose id
bound threw and answered `runtime_broker_invalid_request` instead of the
protocol-shaped `runtime_control_operation_invalid` every other refused
Monitor operation gets. The Broker now screens the row id the same way the
ledger admitted every row and refuses such an id as naming no watch (R1-4).

Tests pin the rest of the review: an `unknown` stop answer keeps the hold
row (R1-5); `monitorControl()` admits `running` and `unknown` as watch
answers (R1-2); an end without evidence while a stop waits settles
`watch_failed`, never the stop's reason (R1-1); and the resume case calls
`resumeMonitorWatch` with its Runtime call, as production does (R1-3).

* fix(managed-agent): keep a proven Monitor stop open until its final output lands (#13896)

A proven auto-stop whose final capture the publisher refused still
answers `exited`: the worker's completion records the delivery failure
and keeps the evidence. The loop then settled the record terminal ahead
of the sealed manifest, and the publisher's redrive of that finalize was
refused by the successor rule, stranding the output at `pending`.

The loop now keeps such a record open at the stop's reason, with no
park and no terminal settle. The redrive delivers the exit, which settles
at that reason after the sealed manifest lands. If it never lands, the
Session close settles at the stop's reason with no second stop.

The loop's fake executor now answers a proven stop only after delivering
the exit, as the worker does; the two close-park cases model their failed
drain as unproven. New cases: a proven stop with no exit stays open and
unparked, and the real-publisher auto-stop E2E runs with a final forward
the store refuses twice.

Broker: pin the completion half of the control fence. A completed Monitor
control must not end a fence it never began, or a fenced control still in
flight no longer keeps the release busy.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen Code <[email protected]>
2026-10-13 06:55:17 +00:00
ChiGaoandchiga0 bc682d1754 fix(cli): give bare ↑/↓ back to input history in Virtualized History (#14016)
#12518 routed a bare ↑/↓ on an empty composer to transcript scrolling in VP
mode, on the theory that terminals without SGR mouse reporting deliver the
wheel as arrow-key sequences. The composer is empty at rest, so the fallback
fired on every deliberate ↑/↓: input history became unreachable from the
arrow keys and only Ctrl+P / Ctrl+N still recalled it.

That also contradicted the contract the project had already settled on —
#4942 ("scroll with wheel or Shift+↑/↓ anytime, type messages anytime"),
#5159 (closed with a docs workaround rather than by reassigning the keys),
ui.useTerminalBuffer's own description, and both keyboard-shortcuts.md and
troubleshooting.md, none of which #12518 updated.

Gate the fallback behind a new opt-in ui.arrowKeysScrollTranscript (default
false). Bare ↑/↓ walk input history again everywhere by default; a terminal
that genuinely cannot report the wheel turns the setting on and gets
#12518's behaviour verbatim, with Ctrl+P / Ctrl+N for history. The opt-in
keeps every guard #12518 added — empty composer, transcript actually
overflowing, ↓ still yielding to descendFromComposer — and non-VP mode is
untouched.

Docs now name the setting as the alternative for the tmux trackpad case.

Fixes #14015

Co-authored-by: chiga0 <[email protected]>
2026-10-13 06:27:55 +00:00
易良andQwen-Coder 0df41f4464 test(managed-agent): clean up recovery fixtures after probe failures (#14018)
* fix(ci): recover reused runner checkouts and Java lock waits

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

* test(managed-agent): clean up recovery fixtures after probe failures

* fix(ci): refresh Git metadata across Linux validation lanes

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

* fix(ci): refresh checkout metadata for triage jobs

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-13 06:13:42 +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
ef38ad7753 feat(web-shell): add read-only Excel artifact previews (#13664)
* feat(web-shell): add read-only Excel artifact previews

* fix(vscode): refresh notices for Excel preview dependencies

* fix(web-shell): bound Excel merge loading and preserve preview content

* fix(web-shell): bound Excel preview work and preserve recovery

* fix(web-shell): harden Excel preview values and recovery

* fix(web-shell): include bundled ExcelJS dependency licenses

* fix(web-shell): guard column expansion and complete ExcelJS notices

* fix(web-shell): complete Excel color fallback and review guards

* test(web-shell): follow artifact filename labels in Excel previews

---------

Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-10-13 02:43:10 +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
jinye 7e5131d97b feat(runtime): add private CSI file worker with local-process isolation (#13899)
* feat(runtime): add private CSI file worker and retained history

* feat(runtime): pair CSI file worker with local-process isolation

* codex: address PR review feedback (#13899)

* codex: address PR review feedback (#13899)

* codex: address PR review feedback (#13899)

* codex: address PR review feedback (#13899)
2026-10-12 13:06:01 +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 9f14e7d9bb feat(managed-agent): verify tool publications asynchronously (#13654)
* feat(managed-agent): verify tool publications asynchronously

* codex: fix CI failure on PR #13654

* codex: address PR review feedback (#13654)

* codex: address PR review feedback (#13654)

* codex: address PR review feedback (#13654)

* codex: address PR review feedback (#13654)

Preserve the current main actor-key migration and append async verification at V57.
Cover both the legacy-to-main and main-to-async upgrade histories.

* codex: address PR review feedback (#13654)

* fix(managed-agent): recover lapsed activations and failed finishes

A renewable activation horizon was recorded as a permanent failure, and a
permanently failed finish left the publication stuck in FINISHING. Give the
MariaDB job enough time for the new async integration tests.

* fix(managed-agent): verify accepted CSI claims after retirement

Async verification used PRODUCE, so a settled CSI execution with a
retirement row permanently failed work that the claim path would finish.

* fix(managed-agent): defer expired grants and drop failed terminal bytes

A real activation lapse expires the publication grant on the same clock, so
the grant check failed the operation before the activation deferral. A failed
finish also refunded its charge while leaving a non-VERIFIED object that
blocked collection.

* fix(managed-agent): keep object deletes and scan errors off the failure transaction

A failed finish deleted its object inside the database transaction, so a
rollback could drop the bytes without recording FAILED. A handler exception
also aborted the rest of the verification page and retried the same row.

* fix(managed-agent): preserve transient retries and writer fences

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

* fix(managed-agent): distinguish verification corruption from authority failures

Keep accepted verification pending during session recovery and classify corruption only from verified byte mismatches. Preserve permanent writer fences and retry unclassified reads through complete capture validation.

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

* fix(managed-agent): fail verification on recovery blocks

Restore permanent failure for accepted asynchronous work when Session recovery is blocked. Exercise the production recovery-block entry point without resetting the Session to READY, and document the fencing policy in both languages.

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

---------

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-12 13:01:51 +00:00
qqqysandqqqys e9789d0911 feat(mods): Add tool interception and permission checks (PR3a) (#13950)
* feat(mods): Add tool interception and permission checks

* fix(mods): Preserve execution facts across result references

---------

Co-authored-by: qqqys <[email protected]>
2026-10-12 11:16:36 +00:00
ytahdnand钉萁 364593cb25 feat(web-shell): add action reminders and in-panel notification access (#13832)
* feat(web-shell): notify for approvals and user questions

* fix(web-shell): isolate notification help and authorization attempts

* fix(web-shell): preserve notification authorization and keyboard focus

---------

Co-authored-by: 钉萁 <[email protected]>
2026-10-12 10:51:11 +00:00
ytahdnand钉萁 f3f5e005a3 feat(web-shell): support copying selected reply content (#13833)
* feat(web-shell): support copying selected reply content

* fix(web-shell): preserve selected reply data and menu lifecycle

* fix(web-shell): keep selection copying faithful to visible content

---------

Co-authored-by: 钉萁 <[email protected]>
2026-10-12 09:43:56 +00:00
ytahdnand钉萁 e9b87acd73 fix(web-shell): localize and dismiss voice feedback (#13938)
* fix(web-shell): localize and dismiss voice feedback

* fix(web-shell): exclude dictation messages from transcript bundle

---------

Co-authored-by: 钉萁 <[email protected]>
2026-10-12 07:50:44 +00:00
b1b2b2c6df fix(managed-agent): config and API-surface hygiene from the #12692 R2 review (#13335)
* fix(managed-agent): config and API-surface hygiene from the #12692 R2 review

Nine R2 review follow-ups on #12692, re-verified against current main
(2c591ecc08) before patching:

- Enforce a 4M-character aggregate input budget in create/submit; the
  per-block 1M cap times the per-list 100 cap admitted ~100M
  characters in a single command.
- Drop the nine kubernetes* config fields no code could ever consume
  (the kubernetes provisioner fails fast with a named error until it
  ships) and the required-but-unread cliEntry knob, across the
  properties class, application.yml and the README.
- scan-delay keeps exactly one default: the annotation placeholder.
  The dead typed accessor is gone instead of drifting from it.
- materialize-interval gains its typed field, pinned against the
  annotation fallback so the two cannot diverge; all nineteen
  Duration fields now declare @DurationUnit so a unit-less operator
  override can no longer bind as milliseconds (a bare 120 would have
  been a 120 ms lease).
- The 100 ms materialize pass moves onto its own single-thread
  scheduler instead of sharing Boot's default with turn recovery.
- WebShellTurn.usage is removed from the record, the OpenAPI spec and
  the regenerated TypeScript contract: it was serialized but never
  populated by any caller.
- WebShell metadata is now accepted and ignored (Phase 1 persists
  metadata.title only) instead of passing a clientId-only validation
  that was then silently discarded.

Verified on the managed-agent-server module with mvn verify: 557
tests, checkstyle and spotbugs green; the web-shell managed component
suite (263 tests) and typecheck pass against the regenerated
contract.

* fix(managed-agent): close the contract gaps from the #13335 review

Eight findings on the config/API cleanup, each verified against the
cited code before patching:

- The metadata acceptance widening stopped at the controller: the
  published WebShellMetadata schema still enforced
  additionalProperties:false with a clientId-only shape, and the
  regenerated client could not even express what the server now
  accepts. The schema is relaxed to any-object with an
  accepted-and-ignored description; managed-agent-api.ts regenerated
  ({ [key: string]: unknown } | null).
- The justification comment was itself wrong (the handlers forward
  null, so nothing from metadata persists at all) and the widening
  was pinned on create only. Both comments corrected; the test now
  reads the session back (metadata.title does not surface) and posts
  a non-clientId body to turns/submit (fails later, at the unknown
  session, not at the deleted validator).
- Events.materializeInterval was a typed field with no production
  reader -- the same dead-surface defect the diff removed for
  scanDelay, plus a comment that crowned the shadowed annotation
  fallback. The pass is now scheduled programmatically on the
  dedicated single-thread scheduler at the typed interval (yml-bound,
  100 ms default); the @Scheduled placeholder is gone along with the
  three-source divergence. A discriminating test parks Boot's shared
  one-thread pool and asserts the materializer keeps passing (the
  existing parking tests could not see this arm).
- The scan-delay comment now names the real deployed truth
  (application.yml '1s') and all three @Scheduled fallback sites,
  and notes @Scheduled reads a bare number as milliseconds.
- The Duration sweep walks the class graph transitively from the
  outer class, so a depth-2 component (RuntimeBroker.WorkspaceMount)
  can no longer slip it.
- The seven string-form cliEntry consumers the compiler could not
  flag are deleted (3 e2e env assignments, 4 hosted fixtures);
  relaxed binding was swallowing them silently.
- The 4M aggregate bound is published on all four input arrays in
  the OpenAPI document and interpolated into the 400 message (a
  retune no longer lies to clients).

Module verify green: 558 tests, 0 failures (was 557 + the new
discriminating case); web-shell typecheck + 263 managed tests green
against the regenerated contract.

* test(managed-agent): pin the contract and cadence witnesses from the #13335 review

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

* fix(managed-agent): close the R3 review follow-ups on the v3-window unit docs, input-budget counting, and the two-arm 413 fake (#13335)

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

* fix(managed-agent): bind auth.allowed-drift in milliseconds (#13335)

The @DurationUnit sweep left Auth.allowedDrift unannotated, reddening
everyDurationFieldDeclaresABindingUnit on both Java CI lanes. Annotate
it MILLIS rather than the sweep's SECONDS: a stale milliseconds-style
override (300000) must keep binding the documented 5m — as seconds it
would silently widen the signature-replay window to ~83h and still pass
BrokerSecurity's 1s floor, while 300 fails closed via that floor either
way.

Closes the round's remaining review findings:
- R5-1: the annotation above plus a binding arm proving 300000 -> PT5M
- R6-1: warn at startup when a seconds-convention duration binds at
  >=1000x its field default, the stale-milliseconds-override signature
- R3-5: behaviour-pin the 4,000,000-character aggregate input budget on
  the public events route, not just the WebShell one
- R4-1: pin the schema's machine-readable InputBlock maxLength to the
  enforced per-block cap
- R4-2: the fixture harness records which 413 arm fired so the record
  arm cannot be shadowed by the prompt arm
- R6-2: pin @Scheduled timeUnit for the three dispatch coordinators and
  document the remaining millisecond-bound settings

* test(managed-agent): pin every scan-delay @Scheduled match per coordinator (#13335)

The pin sampled one annotation per coordinator via findFirst(), so a
second dispatch.scan-delay sweep could pass silently depending on
getDeclaredMethods() order. Match exhaustively and assert hasSize(1)
plus allSatisfy on the fallback and timeUnit, so an added or retuned
site fails loudly.

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

* fix(managed-agent): warn on the stale-override bands the startup sweep missed

The seconds-convention startup warning only fired at 1000x above a field
default and ran after the approval-timeout range check, so three real
misconfiguration shapes booted or failed silently:

- a stale 600000-style approval timeout always threw the 1s-24h range
  error before the sweep could name the unit flip; the sweep now runs
  first (the range check still fails the boot).
- the three required tool-publication deadlines ship no default, so the
  1000x comparison never bound for them; they now warn from a one-hour
  floor.
- a bare value meant in a larger unit (v3-result-window=30 read as 30
  seconds) sat far below the default and lost slow v3 tool results to
  UNKNOWN; a seconds-convention field now also warns at 10x or more
  below its default.

Also pin what review found unpinned: the declared unit value of all 24
@DurationUnit fields against an expected-unit table, the two MILLIS
exceptions through the real binder, the projection-interval @Scheduled
placeholder/scheduler/timeUnit triple, and the scan-delay helper now
flattens the @Repeatable container and matches every schedule attribute
so a second sweep cannot hide. Narrow the WebShellMetadata description
to the actual Map binding and regenerate the client.

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

* fix(managed-agent): key the startup unit sweep on the written shape (#13335)

The magnitude bands missed the most damaging stale override
(HARNESS_TURN_DEADLINE=600000 binds 6.9 days at ~333x the default,
inside no band) and warned on correctly suffixed values (2s, 500ms,
1s). With an Environment the sweep now reads each seconds-convention
field's raw property text: a bare integer warns with both readings, a
suffixed value stays quiet. The magnitude bands remain as the fallback
for a plain new with no Environment.

Also address the round's remaining findings: the approval-timeout range
refusal names the seconds convention, the PT0S zero guard and the
MILLIS binding arms gain real witnesses, the materializer annotation
control and the projection-interval pin see every schedule shape, and
the runtime-broker javadoc stops claiming guarantees that live in
managed-agent-server.

* fix(managed-agent): unit-rule doc, turn-deadline refusal and cadence pins (#13335)

The README's duration unit rule named two @Scheduled placeholder
cadences, but child-relay.scan-delay is a third one with no typed field
and no application.yml entry, so its annotation fallback is the deployed
default and the startup sweep can never see an override; state the rule
structurally and name all three. The Hosted Harness turn-deadline
refusal still stated the pre-sweep millisecond range, telling operators
a stale 3600000 was legal; it now names the seconds convention like the
approval-timeout sibling. Pin the shipped application.yml scan-delay and
materialize-interval values next to the annotation fallbacks, pin the
deleted Dispatch.scanDelay surface alongside kubernetes*/cliEntry, pin
the ChildResultRelay placeholder/scheduler/timeUnit, and make the
Duration-unit sweep prove its walk is non-vacuous.

* fix(managed-agent): unit-annotate merged Channels durations and pin the cadence list (#13335)

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

* fix(managed-agent): drop the dead Channels.scanDelay duration field (#13335)

The @DurationUnit(SECONDS) annotation certified a unit for a cadence the
field never drives: the only consumer of channels.scan-delay is the
@Scheduled fixedDelayString placeholder on ManagedChannelService.reconcile,
which binds a bare number as milliseconds, and getScanDelay() had no
production readers. Delete the field per the Dispatch.scanDelay precedent so
the key becomes placeholder-only: the README cadence sentence now names it,
the duration-field inventory drops to 31, and droppedConfigSurfacesStayDropped
pins the deletion. The shipped cadence is unchanged (application.yml and the
annotation fallback both stay 30s).

* fix(managed-agent): repair the turn-deadline refusal test and finish the cli-entry removal (#13335)

- turnDeadlineRefusalNamesTheSecondsConvention failed at head: with
  harness.enabled set and session-store.enabled unset, the properties
  @PostConstruct gate fails the context with 'Hosted Harness requires
  the Managed Session Store' before the connector's turn-deadline range
  check is ever reached. Assert the refusal on the narrower context that
  constructs QwenHostedHarnessConnector directly, matching the file's
  existing constructor-refusal tests, and drop the now-unused
  HarnessRefusalConfiguration. The suffix-less-seconds binding side
  stays pinned in ManagedAgentPropertiesTest.
- Pin the messageMaterializerTask pool identity: capture the firing
  thread's name in the parked-pool test and assert the
  'message-materialize-' prefix, so a retargeted @Qualifier to another
  poolSize(1) dedicated scheduler goes red.
- Delete the stale --qwen.managed-agent.runtime-broker.cli-entry=
  argument from the three fixtures that entered via the origin/main
  merge, matching the seven sibling sites this branch already cleaned.

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

---------

Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-autofix <[email protected]>
2026-10-12 07:16:48 +00:00
c528b37e83 fix(managed-agent): settle provably never-started dispatches at session open (#13801) (#13917)
* fix(managed-agent): settle provably never-started dispatches at session open (#13801)

* fix(managed-agent): probe never-started dispatches without reacquiring a released runtime session (#13801)

* fix(managed-agent): own the :process ledger key in the Broker client and pin review witness gaps (#13801)

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

* fix(managed-agent): attribute never-started skip causes in diagnostics and pin Gap A scope (#13801)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-12 06:37:54 +00:00
1bbaf93e8d feat(managed-agent): enable the H3 Monitor and background Shell gates (#13856) (#13889)
* feat(runtime-broker): split Runtime Session release into routine and drain arms (#13856)

The H3 enablement's first design call: a Runtime Session release refuses
409 runtime_session_held while a background run is provably live, and the
run holds its Runtime until its exit is proven — the turn-settle drain
that SIGTERM'd live producers is gone from the routine arm. The drain arm
(the Session close, and the control plane's close cascade through
releaseSavedSession) keeps the status → terminate → status sweep, and the
public :release route gains the optional "drain" flag that names it.

The Monitor watch joins the hold: is_monitor payloads admit their own
ledger row (background_v3_monitor), the sweep and the re-acquire scan ride
the worker's pre-built monitor-status/monitor-stop maintenance route
through a new ManagedMonitorProtocol (the mirror of ManagedShellProtocol),
and the cancel guard covers both hold families.

Design: docs/design/2026-10-11-managed-h3-enablement.md (+ zh-CN).

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

* feat(cli): hold the Runtime across turns and project platform stops (#13856)

The harness side of the H3 hold: a turn whose routine Runtime Session
release is refused 409 runtime_session_held settles clean — the designed
hold, not a wedge — and the Session records the owed drain. The Session
close drain-releases every held Runtime ahead of the loop stops and the
publisher close, and a drain that cannot prove a stop fails the close
honestly (503, reopenable) with monitors parked runtime_lost, the same
unproven shape as a blocked settlement. The recovered-lease handback at
close drains too; the cancellation path's fire-and-forget handback keeps
routine semantics (a cancel route never stops a live run).

A8: a platform-driven stop now projects as cancelled/stop_requested
instead of a bare exited. The worker registry marks any stop it was asked
to make (terminate/stopSession/stopAll), the marker rides the capture
finalize's process evidence (present only when set, so the foreground
lane's closed key set never sees it), and the publisher settles the record
through requestStop + settleStopRequested; the monitor lane answers the
same marker through the observation loop's onExit. An unproven end still
settles failed, marked or not — the marker never mints a stop nobody
proved.

Witnesses: the publisher background suite settles stop_requested with the
marker and exited without; the monitor loop suite covers the platform-stop
arm; the hosted close-drain test watches the turn hold and the close's
drain release; the turn suite pins the held/busy classification split.

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

* feat(core): enable the H3 monitor_run domain and the shell child_run kind (#13856)

The gate lift itself, after the Runtime hold landed and the physical
acceptance pass ran: monitor_run joins MANAGED_SESSION_ENABLED_DOMAINS and
shell joins MANAGED_SESSION_ENABLED_CHILD_RUN_KINDS beside child_agent.
The turn's background admission reads the kind gate now — the child_run
domain never joins the plain list, so the old plain-domain read was a
latent refusal the lift would otherwise have shipped.

The pinned-disabled suites flip deliberately, following #13846's practice:
the pin tests assert the enabled lists, the authority and publisher suites
commit through the real gate, and a test that needs a gate shut closes it
per test. The H3 design's status and the §13 hold text record the lift and
the runtime_session_held coding.

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

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

* fix(core): close over a result manifest's page envelopes at commit (#13856)

The staged HTTP session store persists a staged resource only when a
commit's resource closure names it, and the walk that builds the closure
never descended into a result manifest — so the page envelopes the
manifest names stayed staged in memory and never persisted, and the
Artifact read chain 404'd at the page hop. The #13856 rig pass's B2 arm
proved it physically: the producer's bytes landed (the content rows held
them, digest-true), the manifest sealed complete, and the page envelope
it named was in no table.

The walker now collects the page references a manifest names. Pages
themselves name only segment digests (never refs), and segments persist
through the tool-result publish route directly, so the chain closes:
record → manifest → pages → segments.

Witness: a manifest naming a page ref closes over exactly it; removing
the branch turns the test red. The B2 rig arm re-runs green over it.

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

* docs(managed-agent): record the H3 enablement's physical acceptance (#13856)

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

* test(core): drop a stray enablement flag from the shell acceptance case (#13856)

The case commits a `child_run` of the shell kind, which the production
gate now admits, so the switch it flipped was never this suite's to
flip — and `npm run typecheck` refused the file over the unknown
property.

* test(cli): run the H3 gates at their production default (#13856)

Both suites mocked the monitor_run domain and the shell kind shut and
asked each case to open them, so a case that never thought about the
gate exercised a refusal production no longer produces. The flags now
start open and a case that needs a gate shut closes it per test, and
the kind mock sends every kind the production list carries through the
real gate; only a kind production has not enabled — a workflow planting
— stays a fixture, admitted so its case runs what it is about.

* fix(cli): drain the Hook-owned Runtime at Session close (#13856)

A Hook-configured Shell turn shares the Hook broker, and its `finish()`
skips the release, so a background run that outlives the turn holds that
Runtime and never reaches the Session's own held-Runtime drain: the close
died on the Hook session's routine release refusing `runtime_session_held`,
answering 503 forever and leaving the run alive. The close is the drain arm
— an acquire still hands an earlier owner back without stopping anything,
because the Session lives on.

Co-Authored-By: Qwen Code <[email protected]>

* fix(cli): park a Monitor auto-stop that cannot prove its stop (#13856)

The quota and the idle window run in the Harness while the watch runs in
the worker, and the client control route admits MCP and Hook operations
only — no Harness-side `monitor-stop`. A remote handle's terminate
therefore stopped nothing, yet the loop settled the record at
`max_events`/`idle_timeout`: the task read as ended while the watch kept
running and kept its Runtime held. A handle now reports whether its stop
is proven, the supervised watcher answers from the supervisor's evidence,
and an unproven auto-stop parks the record on the runtime_lost line the
`stop()` call already uses. Giving an auto-stop its own remote exit
evidence is recorded as future work in both languages.

Co-Authored-By: Qwen Code <[email protected]>

* fix(cli): let a parked Monitor consume the exit evidence that follows (#13856)

The park ended the loop's observing and its record in one step, so the
watch's own later end had nowhere to land: `onExit` and `stop` both returned
early over `ended`, and the record stayed `recovery_blocked` with a null
stopReason even after the worker exited, its hold released and its capture
published complete. Observing and the terminal settle are separate flags
now, so a park keeps the record open for the evidence that follows — a
natural exit, a platform stop, or the close's proven drain.

A terminal settle also clears the recovery reason a park or a rebuild left
on the run: the record contract admits no reason for `settled` or
`cancelled` and only a quota reason for `failed`, so carrying `runtime_lost`
into them refused the very commit that proved the end.

Co-Authored-By: Qwen Code <[email protected]>

* fix(cli): keep a failed close's Monitor park open for the evidence (#13856)

`stop()` marked the record terminal before it knew which arm it would take,
so the `runtime_lost` park closed the very record it left unsettled. That arm
is what a held-Runtime drain failure asks for: the close answers 503 and
keeps the Session and its publisher alive for a retry, and neither the
retry's proven drain nor the watch's own later end could reach the record —
it stayed `recovery_blocked` with a null stopReason over a worker that had
exited, a hold that had released and a capture that had published complete.
Only the arm that actually settles now marks it terminal.

Co-Authored-By: Qwen Code <[email protected]>

* fix(cli): keep a parked Monitor resumable only when it is live (#13856)

Two faults in the parked-monitor path the review round surfaced. A parked
record has stopReason null exactly like a live watch, so the resume gate
re-adopted it after a restart and its first post-quota observation was
refused and swallowed inside the loop's queue; the gate now also rules out
the recovery_blocked state (R1-55). And `onExit`'s final flush could cross
maxEvents and park the record with a false runtime_lost revision ahead of
the exit's own settle — a crash between the two commits would stick it — so
the exit path's flush no longer fires the quota settle: the physical end
supersedes the quota (R1-30).

Co-Authored-By: Qwen Code <[email protected]>

* fix(cli): tolerate an already-gone Runtime in the close drain (#13856)

The close's held-Runtime drain treated every broker refusal as close-fatal,
so a Session whose held Runtime was already gone (404
runtime_session_not_found) answered 503 on every retry and stayed half torn
down — hooks and the wake pump closed, the publisher and the session entry
never. A 404 names nothing left to stop, so the close now discharges it and
proceeds; every other refusal is unproven and still parks honestly (R1-1).

Co-Authored-By: Qwen Code <[email protected]>

* fix(cli): settle a parked Monitor from its real exit evidence (#13856)

The no-observer finalize branch re-committed a parked record's capture tail
as an observation before settling it: the quota park refused it as an
over-quota sequence and the idle park as an illegal successor revision, so a
parked watch whose real end later arrived stayed `recovery_blocked` with a
null stopReason even after the worker provably exited and released its hold.
A parked record's observation period already ended, so the tail is not a new
observation — the exit evidence settles the record directly (R1-55).

Co-Authored-By: Qwen Code <[email protected]>

* fix(cli): settle a crashed wake turn over a held Runtime (#13856)

A wake turn that started a background run and then crashed left its Runtime
held by design, so the crash aftermath's routine release refused
`runtime_session_held` and the whole aftermath reported pending: the wake
input never settled and the Session's close never learned it owed the drain.
The aftermath now reads the refusal as the designed hold, registers the
Runtime for the close's drain, and keeps settling the crashed input — without
turning the routine recovery into a drain, which would stop a background run
the user meant to keep (R1-28).

Co-Authored-By: Qwen Code <[email protected]>

* test(cli): hold the held-Runtime set in a local in the wake-crash case (#13856)

The R1-28 case read `session.heldRuntimeTurns` off a session cast `as never`,
which the package build's `tsc` refuses (TS2339) while `tsc --noEmit`'s
typecheck tsconfig does not — so the failure only ever showed up as a red CI
build lane. Hold the set in a local and assert on that.

Co-Authored-By: Qwen Code <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen Code <[email protected]>
2026-10-12 06:29:49 +00:00
Shaojin Wen 9da6c6f7c9 docs(managed-agent): Plan the EventTransport MQ2 producer/consumer wiring in four sub-slices (#13939)
* docs(managed-agent): Plan the EventTransport MQ2 producer/consumer wiring in four sub-slices

Phase plan for MQ2 (the parent design's producer/consumer wiring phase),
decomposed into four independently reviewable sub-slices: MQ2a pins a Java
mirror of the frozen envelope contract with a fixture consumer; MQ2b adds
the authoritative-journal publication point and the LiteTopic producer
behind a default-off flag; MQ2c wires the public-event lane and the
wake/materialization consumers; MQ2d runs the fault-matrix E2E in CI. All
MQ2 entry gates are closed (MQ1 #13909, §5.1 #13927 + staging, §11 #13933).
Both languages in lockstep.

Part of #12380

* docs(managed-agent): Fix the MQ2 plan's dependency gate and add the handoff acceptance

Review follow-up on #13939: the rocketmq-client-java:5.1.0 client is a
shaded artifact (io.grpc/com.google relocated to private namespaces), so
the Boot BOM never re-aligns them and the 'Boot-aligned vs pinned gRPC'
gate was built on a wrong premise — the gate is now empirical (clean
dependency tree and packaging, plus a server start and a broker
round-trip). And the owner-handoff recovery acceptance is added to
MQ2c/MQ2d/Validation: during a handoff the stale subscriber's fenced
no-op ACK advances the group offset, so the new owner recovers the
pending item by SQL reconciliation, never by assuming MQ delivery. Both
languages in lockstep.
2026-10-12 05:59:29 +00:00
jinye e17ad4caef fix(managed-agent): order publication placement before retention (#13894)
* fix(managed-agent): order publication placement before retention

* codex: address PR review feedback (#13894)

* codex: address PR review feedback (#13894)

* codex: address PR review feedback (#13894)
2026-10-12 04:19:41 +00:00
qqqysandqqqys 049c56d82a feat(mods): Add middleware dispatch and recovery (PR2b) (#13915)
* feat(mods): Add middleware dispatch and recovery (PR2b)

* fix(mods): Correct log budgets and worker regression checks

Keep charging caller execution while void log notifications are pending.
Update the relocated worker test for the grouped protocol and cancellation buffer.
Clarify and protect the Claude-compatible catch stopping and replay rules.

* fix(mods): Bound idle work and preserve dispatch capacity

---------

Co-authored-by: qqqys <[email protected]>
2026-10-12 03:48:44 +00:00
Shaojin Wen 23add5ef9d docs(managed-agent): Resolve the EventTransport §11 routing and wake-carrier questions (#13933)
* docs(managed-agent): Resolve the EventTransport §11 routing and wake-carrier questions

Decided 2026-10-12 with the maintainer: the wake envelope keeps the
committed wake.requested record as its carrier (already in the envelope
v1 vocabulary; a distinct WakeIntent would store the same fact twice),
and Session-envelope consumption uses a shared bootstrap lane plus
owner-side dynamic subscription — the activation fence, not the broker,
decides which node may act, so neither consumer-group-per-Session nor
group-per-node fan-out is needed. Both languages in lockstep.

Part of #12380

* docs(managed-agent): Sync §8/§9 with the §11 decisions

Review follow-up on #13933: the §9 carrier bullet still said the
wake-intent carrier was unpicked until #13300 lands — it now records the
resolved decision (wake.requested, superseding #12827 question 3). And
the routing decision's sole-subscriber claim is qualified as the steady
state: during an owner handoff (lease expiry without a clean
unsubscribe) a stale subscription can coexist, its fenced no-op ACK
still advances the group offset, and the new owner can miss the
notification — SQL reconciliation covers the miss, and the handoff case
joins MQ2's verification list. Both languages in lockstep.
2026-10-12 01:58:37 +00:00
Shaojin Wen 4cd30565b2 docs(managed-agent): Resolve the EventTransport §5.1 LiteTopic verification ledger (#13927)
* docs(managed-agent): Resolve the EventTransport §5.1 LiteTopic verification ledger

All five UNVERIFIED rows resolve in LiteTopic's favor from public
documentation, dated 2026-10-12: at-least-once with retry + DLQ,
per-LiteTopic FIFO, per-ClientID subscription, broker-persisted RocksDB
offsets, gRPC SDK >= 5.1.0 / server >= 5.5.0; classic consume-side
semantics do not carry over. §5.2 table cells updated, §11 question 3
closed (doc-level verification pinned in §5.1; the MQ2-start staging
re-check remains). Both languages in lockstep.

Part of #12380 · Refs #13200

* docs(managed-agent): Correct the dispatch semantics and pin sources in the §5.1 resolution

Review follow-up on #13927: LiteTopic dispatch is shared within a
consumer group — exactly one client per group receives each message
(source-verified at the 5.5.0 tag: LiteEventDispatcher, and the
(group, LiteTopic, 0)-scoped lock/FIFO/offset in PopLiteMessageProcessor),
so cross-node fan-out needs one consumer group per node; the capability
table, the resolution item and the §11 routing question now say so.
Also: correct the Aliyun quota figures (23.2M on rmq.u2.1000xlarge, 1M on
rmq.u2.30xlarge; 200 = consumption TPS; TTL frees the LiteTopic count, not
the subscription count), mark the fixed-interval retry as
commercial-doc-only evidence in the table, and make every resolution
source clickable with version-pinned links. Both languages in lockstep.

* docs(managed-agent): Scope the shared-dispatch claim to one dispatch attempt

R2 review follow-up on #13927: a lost ACK or client inactivity
re-dispatches the in-doubt message, possibly to a different group member,
so 'exactly one client receives each message' overstated the source
guarantee. The resolution block, capability table and §11 now say one
dispatch attempt selects exactly one client, redelivery may move nodes,
and there are no per-node copies — consumer idempotence and the
activation fence absorb that. PR description synced to match. Both
languages in lockstep.

* docs(managed-agent): Sync §8/§9 with the §5.1 resolution

yiliang114's review on #13927: two places still described the
pre-resolution state. §9's risk bullet now records that the doc-level
verification resolved the delivery/ordering items on 2026-10-12 and that
the residual exposure is the MQ2-start staging re-check; the §8
consumer-crash row drops the 'must be verified in §5.1 first' note to
'verified at doc level, re-checked against staging at MQ2-start'. Both
languages in lockstep.
2026-10-12 01:04:47 +00:00
e977bdac71 fix(cli): bound managed function-hook module evaluation and keep hold-fenced owners recoverable (#13243)
* fix(cli): bound managed function-hook module evaluation and fix its test import

PR #13129 merged with two unresolved real findings from its round-4
review:

- A managed function handler module is evaluated with an unbounded
  await import(): it ignores both the manifest-mandated function
  timeout and the operation abort signal. A module whose top-level
  await never settles wedges the operation permanently, pin one of the
  16 admission slots and makes runtime close() never resolve. Race the
  import against the manifest function timeout and the operation abort
  signal: timeout settles as managed_hook_handler_unavailable (same as
  a failed import), abort settles the operation as cancelled, matching
  the existing pre-dispatch abort outcome.
- The 'ignore-abort' interruption test imported its handler module via
  a raw absolute filesystem path; on Windows import() throws
  ERR_UNSUPPORTED_ESM_URL_SCHEME there, failing the schedule-only
  test_windows lane on main, which never runs on pull requests. Import
  through pathToFileURL like the production sibling does.

Add regression tests for both newly bounded paths, including one that
fails close() hanging was verified by mutation: with the bounding
removed, both new tests fail (8s status poll timeout; afterEach close
hang), with it, the full file passes.

* fix(cli): fence abandoned function-hook module evaluation as unknown and bound its budget

Address PR #13243 review round 1:

- R1-1: a module-evaluation timeout settled with the same
  managed_hook_handler_unavailable code a genuinely failed import
  produces, which the host records as not_started_proven — but ESM
  evaluation cannot be cancelled, so the abandoned module may already
  have run top-level effects. Report the timeout with a dedicated
  managed_hook_module_evaluation_timeout code instead, letting the
  host fence the execution honestly as outcome_unknown; keep the
  unavailable code for imports that actually rejected.
- R1-2: the import budget reused the manifest's callback timeout
  verbatim, legally as small as 1 ms, turning cold-but-healthy
  evaluation into false handler-unavailable receipts. Give evaluation
  its own floor (500 ms) while keeping the callback budget unchanged.
- R1-1/abort: hasHolds() now accounts for an abandoned evaluation
  besides view.state, so workspace release stays fenced while handler
  top-level code may still be running; a never-settling evaluation
  keeps the hold — the deliberate honest-hold trade-off, documented at
  the flag.
- R1-3: pin the import rejection arm with a broken-module test whose
  receipt must arrive well before the 8s status poll.
- R1-4: pin the manifest timeout as the evaluation bound with an
  elapsed-time assertion that reds on a 5s constant mutation.

Hosted-side regression: a module-evaluation-timeout receipt fed
through the broker records execution outcome_unknown, keeps one
hook-execute request, and refuses retry; genuine
handler_unavailable receipts still record not_started_proven.
Each production change was mutation-verified: removing the floor,
dropping the rejection arm, decoupling hasHolds from the flag, or
replacing the budget with a 5s constant each reds a named test.

* fix(cli): teach earlier-owner release to skip a hold-fenced owner and retry later

Address PR #13243 review round 2:

- R1-1 (critical): the abort arm publishes a terminal receipt while
  hasHolds() keeps reporting unfinished work (per round 1), so
  releaseEarlierOwners computed the earlier owner as settled, called
  release(), got a 409 from closeSessionAdmission, and rethrew —
  blocking every replacement activation's acquire on the same daemon
  until it dies. Tolerate the refusal like the already-tolerated 404:
  accept 409 with managed_runtime_identity_conflict or
  managed_runtime_provider_operation_failed, skip the owner without
  recording it released, and let a later acquire retry the release.
  The failed behavior is pinned by two new rows in the existing
  earlier-owner reconciliation matrix (the runtime_session_busy row
  keeps refusing, so only the hold-fenced pair is tolerated).
- R1-4: add a discriminating case that proves the manifest timeout
  governs evaluation above the 500 ms floor (1500 ms evaluation under
  timeout 3000); reword the elapsed-latency comment which the floor
  had made an over-claim.
- R2-1: pin the hold's self-healing half — an evaluation that
  overruns its budget and then completes must clear the hold (700 ms
  evaluation, timeout 10), so a `finished`-guard tidy-up cannot wedge
  workspace release for finished work.
- R2-2: update the managed-hooks design doc in both languages: name
  the module-evaluation phase, its max(timeout, 500 ms) budget, the
  settled-at-Runtime admission release with retained hold, the
  outcome_unknown fencing code, and the hold-fenced earlier-owner
  release retry; correct the 16-slot sentence which previously
  claimed the pairing without exception.

Each fix arm mutation-verified: removing the 409 tolerance reds
exactly the two new matrix rows; substituting the bare floor for the
Math.max reds the manifest-governs case; guarding the flag clear with
`!finished` reds the over-budget hold-release case.

* test(cli): widen evaluation-timing margins and sharpen the release-refusal docs

Audit follow-ups on this branch, plus merging origin/main (which changed
the same release path in #13195) so the branch is verified on the base it
will land on:

- The two timing-sensitive cases had thin margins against CI contention:
  the manifest-governs case declared timeout 3000 for a 1500 ms evaluation
  (2x) and the over-budget case a 700 ms evaluation against the 500 ms
  floor (1.4x). Widen to 5000 and 900 ms; both mutation witnesses still
  red exactly as before (bare floor at the budget site; `!finished` guard
  on the flag clear).
- The design doc described the skipped release as hold-fencing only, while
  the code tolerates any 409 conflict on that path — which is also how a
  genuine identity conflict answers. State both refusals (absent 404,
  conflicting 409) and that the owner is left unreleased for a later
  acquire, in both language versions.

Re-verified on the merged base: 964 passed / 4 Linux-only skipped across
the seven related files with no unhandled-error output, build, typecheck,
ESLint and Prettier all exit 0, and the hold-fenced tolerance mutation
still reds exactly its two matrix rows while the refusal rows keep
throwing.

* fix(cli): name a hold-fenced owner release and log the refusals this PR absorbs

Address PR #13243 review round 3 (all seven findings are Suggestions):

- R3-3: close() releases the current owner without recognising a hold
  fence, so an abandoned module evaluation that outlived the cancelled
  receipt let a bare Broker 409 escape to the DELETE/detach route, which
  answered a generic managed_session_close_failed while the durable
  record said the execution settled cleanly. Classify that refusal as
  the Hook recovery-required condition and give the route catch an
  instanceof branch, so the 503 names the retained hold. The pairing
  follows the hosted_mcp_recovery_required 503 already in that route,
  and no consumer outside this file keys on the code. A refusal that is
  not a hold fence still propagates unchanged.
- R3-4: a tolerated release refusal was dropped without a trace in a
  file that had no logger. Log both tolerated branches — the absent one
  books the owner released permanently, and one of the two fenced codes
  is the provider worker's catch-all, so an absorbed internal failure
  was indistinguishable from a hold fence.
- R3-5: the `continue` that keeps a hold-fenced owner out of
  releasedOwners was the whole retry-later guarantee and nothing pinned
  it. Extend the reconciliation matrix with a second release pass on the
  same instance: the two 409 rows must retry, the 404 row must not.
- R2-1 follow-up: the over-budget case now re-queries its receipt after
  the hold releases, like its five siblings, and its fixture callback
  writes a marker so the test also pins that a late-finishing evaluation
  never dispatches into a settled occurrence.
- R3-1, R3-2, R2-2 follow-up: correct the design doc in both languages.
  The release sentence now names the exact codes, splits the 404
  (booked released, never retried) from the 409 (skipped, retried)
  outcome, and states that any other refusal propagates; the
  handler-unavailable sentence no longer claims a proof for a module
  rejected after it evaluated in full; and the recovery section scopes
  the lifetime-hold claim to operations that never settle, matching the
  paragraph and test that pin the release.

Mutations re-verified: removing the `continue` reds exactly the two 409
matrix rows; dropping the close() classification reds the new
current-owner case while the non-fence case still leaks the raw
refusal; returning a different receipt once the evaluation flag clears
reds only the over-budget re-query; a probe fixture confirms the marker
assertion observes a real dispatch, so its absence claim is not
vacuous.

* fix(cli): retry a hold-fenced Hook owner each turn and stop reporting DELETE as done

Address PR #13243 review round 4 (one Critical, six Suggestions):

- R4-1 (critical): absorbing the hold-fenced release left the promise of a
  later retry unimplemented. acquire() short-circuits once acquired, so no
  in-life pass ever re-attempted it, and close() absorbed the same fence and
  resolved — letting DELETE answer 204 and delete the Session together with
  the only state that could retry. The earlier owner then stayed attached at
  the Runtime for the worker's lifetime, which is what blocks tools across
  the Workspace, and the merge base's self-healing (refusal propagates, 503,
  Session retained, released once the hold clears) was lost. Track fenced
  owners, retry them on each turn's acquire, and make close() report the Hook
  recovery-required condition while any earlier owner is still fenced, so
  DELETE keeps the Session attached and retryable. The retry shares one pass
  across parallel Hooks and still consults releasedOwners, so an owner is
  never released twice, and acquire() keeps proceeding on a fence.
- R3-4 follow-up: the absorbed refusals were logged through the debug logger,
  which writes nothing on a daemon started without --debug. Report them on
  the serve convention (stderr) instead, naming the owner and the refusal,
  and drop the logger this file did not previously have.
- R4-2: the close-route translation to 503 hosted_hook_recovery_required was
  asserted nowhere, so removing it survived the suite. Pin body.code on a
  DELETE whose Hook owner release is fenced, and keep the existing case that
  pins the generic code for a non-Hook close failure.
- R4-4: the retried assertion cleared the release spy before close(), and
  close() itself re-attempts the release, so it observed the close pass
  rather than the acquire-path retry it claimed to pin. Clear after close().
- R2-1 remainder: nothing pinned that an abandoned evaluation releases its
  admission slot. Fill the 16-slot budget with abandoned function
  evaluations, assert the holds are all retained, and assert the next
  operation is dispatched rather than answered with the quota refusal.
- R4-3, R3-2 follow-up: scope the design doc in both languages — the
  close-time naming applies to a Hook Session that owns its Runtime owner,
  while a Session sharing the MCP owner is released by that session and still
  reports the generic code; and handler_unavailable covers a module rejected
  before, partway through, or after top-level execution, so the proof does
  not cover effects that already ran. Also record that the evaluation budget
  bounds only yielding evaluations, and fix the Chinese text that said a
  settle releases all 16 admission slots where the English says one of 16.

Mutations verified: widening the admission filter to count held evaluations
reds the new admission case; booking a fenced owner as released reds both 409
matrix rows and the in-life retry case; removing the route branch reds the
new body.code assertion while the generic-code case stays green.

* fix(cli): stop a stale owner's release refusal from failing a healthy Hook turn

Address PR #13243 review round 5 (Critical only, per the post-round-five
cutoff; the six Suggestions are recorded in #13133).

R4-1 (critical, fix-induced): the per-turn retry added in the previous
round made acquire() fallible on an already-acquired Session.
retryFencedOwners() had no catch and releaseOwner() rethrows any refusal
outside the tolerated set, so a stale earlier owner answering 409
runtime_session_busy, a 5xx, an unconfirmed release or a client timeout
escaped acquire() before any hookControl was sent. The hook never
dispatched although the Session's own owner was healthy, the child record
stayed at execution 'intent' with a null resultRef, hasPendingOperations
stayed true, and every later turn threw recovery-required; DELETE failed
the same way through close(). The id also stayed in fencedOwners, so the
failure repeated every turn, and the loop abandoned the remaining fenced
owners on the first throw.

Absorb and report each owner's retry failure inside the shared
single-flight pass, leaving the owner fenced for the next turn. The catch
sits in retryFencedOwners() rather than releaseOwner(), so the acquisition
path still propagates an untolerated refusal (pinned by the matrix's
allowed:false rows) and close() still leaks a non-fence refusal raw
(pinned by its own case). releasedOwners remains the sole once-only
guarantee and the retry pass still consults it.

Also scope the design doc in both languages: the acquisition path
propagates any other refusal and blocks the replacement activation, while
a later turn's retry pass absorbs and reports it, because that pass runs
on an already-acquired Session and must not fail a healthy live turn for a
stale owner's sake.

Pin: a sibling of the in-life retry case fences the earlier owner on the
first acquire(), then answers a non-tolerated 409 runtime_session_busy on
the retry, and asserts the turn still dispatches one hook-execute, that
attempts reached two, that hasPendingOperations is false, that the refusal
was reported naming the owner, and that a later turn retries again.
Mutation-checked: removing the absorbing catch reds this case with the
raw "Runtime Broker returned HTTP 409 (runtime_session_busy)" escaping
into the live turn.

* fix(cli): keep a hold-fenced close() from releasing the Session's own Hook owner

* fix(cli): fence a Hook cancel landing mid module evaluation as outcome_unknown

An abort that lands while a function handler's module is still evaluating
published a definite cancelled receipt, so the Harness committed a known
outcome while the module's top-level code was still running: the status
route answered recoveryBlocked=false and drain() passed while hasHolds()
kept refusing DELETE and detach with 503 for the worker's lifetime. Report
the abandonment with a dedicated code instead, the way the evaluation
timeout does, so the Harness fences the execution as outcome_unknown.
Keep the settled cancelled receipt for a cancel that lands after the
evaluation genuinely finished, and re-anchor the hold-fenced release
comments: with the fence, only a Runtime that certified an abandoned
evaluation as cancelled still triggers that machinery.

* fix(cli): give the abandoned-evaluation fence a terminal transition

Address PR #13243 review round 8 (its only finding, Critical R7-1).

The abandonment fence published when a cancel lands mid module evaluation
had no way out. Nothing revisited the receipt: entry.view is written only
inside execute(), which has already returned; hook-status returns a clone
of the stored view; and the late import settle handler cleared
moduleEvaluationPending and then called an already-finished finish(). On
the Harness side the only exits from recovery_blocked are settleExecution,
which needs a result, and executionUnavailable(), which lists neither
evaluation code.

So a cancel that merely raced a healthy cold import — a window of roughly
dispatch+200ms to evaluation end, because the Harness delivers a cancel
through its 100ms poll loop — tombstoned the Session for the worker's
lifetime: the import resolved, the callback provably never ran, hasHolds()
went false and the slot and Broker hold were released, yet the record
stayed recovery_blocked / outcome_unknown. Every later fire() rejected,
drain() threw, DELETE and detach answered 503, no broker release was ever
attempted, and the Workspace owner stayed pinned. At the merge base and at
the previous commit the same input settled as cancelled and the Session
stayed usable and deletable.

Republish the receipt once the evaluation definitively ends, with what is
then provable rather than a single code for both arms: a cancelled result
when the module loaded, since dispatched is still false and the callback
demonstrably never ran, and managed_hook_handler_unavailable when the
module itself failed. The republish happens only after
moduleEvaluationPending clears, so no receipt is certified while top-level
code is still running, and it is keyed on the abandonment code so it can
never overwrite another receipt. The record contract already allows
recovery_blocked to step to cancelled, and status() does not short-circuit
a recovery_blocked / outcome_unknown record, so the next status or drain
reconciles it. A never-settling evaluation keeps the fence and its
recovery barrier, which is the case the previous commit was accepted for.

Both design doc versions now state the clearing transition beside the
sentences that bound the hold.

Pin: a sibling of the abandonment case whose module finishes its top-level
code shortly after the abort asserts both halves — fenced with
managed_hook_module_evaluation_abandoned and hasHolds true immediately,
then cleared to a cancelled receipt with hasHolds false — and that the
callback's marker was never written. Mutation-checked: deleting the
republish reds this case on its 3s poll while the never-settling sibling
stays green.

* fix(cli): republish the evaluation-timeout fence and total the import settle path

Address PR #13243 review round 9 (Criticals R9-1 and R9-2).

R9-1: settleAbandoned() republished only the abandonment fence, so an
over-budget cold module load — one that overruns max(manifest timeout,
500ms) and then finishes — kept its recovery_blocked / outcome_unknown
record forever: hasHolds() went false when the import resolved, but
nothing rewrote the view, so every later status re-committed
outcome_unknown, fire() and drain() threw, and close() threw before
releasing the Session's own Runtime owner, tombstoning the Session. At
the merge base the same module simply loaded late and ran. Widen the
republish guard to both evaluation codes, and publish the truthful
outcome for the timeout arm: 'timeout' rather than 'cancelled', since
nothing was cancelled there and HookExecutionOutcome keeps the two
distinct; both reconcile identically at the host. Both design doc
versions already promised the transition for both fences; their
cancelled-only phrasing now names the timeout outcome.

R9-2: importHookModule dropped the promise derived from
import().then(...), and the rejection handler's new
Error(String(error)) is not total, so a handler module whose top level
rejects with a value String() cannot convert (Object.create(null), a
revoked Proxy, a throwing toString) threw inside the handler, escaped
as an unhandled rejection, and killed the session's Runtime worker —
Node's default, with no unhandledRejection handler on this path — mid
tool-call. At the merge base the same input settled as
managed_hook_handler_unavailable. Convert under a guard so the primary
settle path cannot throw, and give the derived promise a terminal
.catch that clears moduleEvaluationPending, republishes
managed_hook_handler_unavailable, and finishes with the same code, so
a throw inside either settle handler settles the operation instead of
escaping. finish's own guard keeps the terminal handler from
re-settling an operation the timer or abort already settled, and the
publish stays ordered after moduleEvaluationPending clears.

Pins: the over-budget case now asserts the republished timeout receipt
after the hold releases (it previously asserted the receipt stayed
identical), a new case settles a module that rejects with
Object.create(null) as managed_hook_handler_unavailable while a
process-level unhandledRejection listener records nothing, and a
host-side companion reconciles a timeout-fenced record through
status(), drain() and close() once the view carries a result.
Mutation checks: restoring the single-code guard reds the over-budget
case on its 3s poll; restoring the bare String(error) with no terminal
handler reds the rejection case at the 500ms floor with the wrong
fence code and an escaped rejection. Each layer alone also carries the
rejection case — removing only the .catch or only the guarded
conversion stays green, as the finding's own flip measured — and the
pair is shipped because the finding prescribes both a total primary
path and a terminal structural backstop.

* fix(cli): name the Hook owner-hold refusal and reconcile fences on the turn path

The hold-fence predicate matched the two catch-all 409 codes, so any
non-hold refusal (an identity mismatch or an unexpected provider error)
was absorbed as a fence and wedged close() into a permanent
recovery-required error for a Session holding no Hook work. The two
genuine pending-work producers now answer with a dedicated
managed_runtime_owner_hold_pending code and only that code fences;
every other refusal propagates as before.

A module evaluation fenced as outcome_unknown was reconciled only on
the DELETE-path drain, so a live Session refused every later turn even
after the Runtime republished the receipt. The turn path now polls each
unsettled record once before refusing a new occurrence — status only,
never a re-dispatch — so the fence clears on the next turn.

* test(cli): deflake the fixed container port bind in container entry tests

The deterministic gate rejected the previous round: both container-mode
tests in managed-runtime-container.test.ts failed with EADDRINUSE on
0.0.0.0:43190. The fixed container port lies inside the Linux ephemeral
source-port range, so under --maxWorkers=25% another worker's outbound
fetch can hold 43190 while these tests try to bind it; both pass solo.

Bind a real loopback-ephemeral socket through a Server.listen mock — the
pattern the ACK-ownership suite in managed-runtime-attestation-worker.test.ts
already uses for the same container-mode path — and keep the fixed-port pin
as a call-argument assertion (mutation-probed: port 43191 turns it red).
All route and attestation assertions still run against the real server.

* fix(cli): reconcile fenced Hook records at the prompt gate and forward the owner-hold code

- forward managed_runtime_owner_hold_pending through the Java runtime
  broker's 409 switch so the dedicated refusal reaches the Harness
- poll exactly the pending Hook records once (non-cancelling, children
  via their occurrence marker) before the prompt admission gate, and
  settle the turn an evaluation fence parked, so a Session whose cancel
  landed mid-evaluation recovers instead of staying blocked for life
- report the routine owner-absent release refusal on the debug logger
- drop the never-read asError guard; cover the reject-after-fence
  republish with a Runtime test

* fix(cli): keep a refused close's wake pump alive and bound the prompt-gate reconcile

- Defer the monitor wake scheduler teardown until the Hook close is
  known terminal, so a DELETE/detach refused by an owner hold leaves the
  kept Session delivering monitor wake turns (wakeBusy still blocks
  mid-drain turns through mcpClosing).
- Move the prompt-gate fence reconcile behind body validation and the
  idempotent replay, and give the pass a 30s budget plus a client-disconnect
  abort, so a stalled Runtime cannot hold the route open and a refused
  prompt never pays Runtime RPCs or durable writes.
- Share the reconciled-fence predicate between the pre-model and pre-tool
  settle arms, so a republished PreToolUse evaluation fence settles its
  parked turn the way the pre-model arm already did.
- Pin the absent-owner release refusal on the debug logger, settle the
  leaked async-child poll loop in its test, assert the abort's rejection
  identity, and size the admission-quota fixture from
  MANAGED_HOOK_MAX_RUNNING.

* fix(cli): bound the fence-settle scan by occurrence and abort signal

- Filter settled PreToolUse records by their in-memory occurrence id
  ahead of any durable read in the pre-tool settle arm, so a recovery
  pass no longer pays one resources.read per historical record.
- Treat an unreadable or unparsable hook result as no fence, so one
  corrupt historical record cannot fail session open with
  managed_session_open_failed.
- Thread the prompt-gate reconcile's abort signal through
  settleCancelledHookTurn so the 30s budget and a client disconnect
  bound the whole pass, not just the Runtime poll.
- Pin the witnesses: no result read for an unrelated occurrence,
  totality on a missing result resource, abort identity on both settle
  arms, the disconnect handler ending the reconcile and releasing
  hooksBusy, and the fenced-owner refusal never logging the
  absent-owner wording.

* fix(cli): re-arm the prompt-gate hook settle while a Session stays blocked

- Widen the prompt-gate reconcile to fire while session.blocked, not only
  while Hook records are pending, and skip the poll when nothing is
  pending: a pass could drain the last record and still drop the settle
  (the shared abort fires between poll and settle, or a concurrent
  cancel's settle declines while hooksBusy is held), and blocked is
  cleared only inside settleCancelledHookTurn, so the Session refused
  every later prompt for its attached life (R4-2).
- Admit a republished InstructionsLoaded evaluation fence by its creation
  inside the parked turn — a creation commit carries the record id as its
  operationId, revisions commit as <id>:<revision> — instead of
  cancelRequested, which a republished receipt can never carry, and keep
  historical InstructionsLoaded receipts behind the arithmetic pre-filter
  instead of paying a durable read per record per pass (R4-1).
- Read Hook inputs tolerantly in both settle scans so one unreadable or
  corrupt input resource skips the record instead of rejecting the whole
  recovery pass (R4-5).
- Correct the design doc's exclusivity claim for 409
  managed_runtime_owner_hold_pending in both languages: the code also
  answers for unfinished non-Hook work the record-based release gate does
  not inspect (R4-4).

* fix(cli): keep Hook recovery fences typed on the fire and close paths

An unguarded reconcile ahead of the pending-gate throw let a generic
durable-store or broker error escape fire() while the fence stood, so the
turn was committed as a definite error and the recovery barrier never
latched; absorb the poll failure, report it, and let the fence re-check
answer with the typed recovery error.

In the shared-broker shape the recovered-owner release loop bypassed the
absorbing release helper and the fence check sat above it, so a
hold-fenced recovered owner escaped close() as a raw Broker refusal and
stranded every owner behind it; route the loop through releaseOwner and
name the fence after the loop.

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

* fix(cli): retry fenced Hook owners on the close path before naming them

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

* test(cli): pin an empty QWEN_HOME for the cli test suite

The autofix verification gate failed 389 tests across 13 files when the
shared runner's real ~/.qwen/settings.json held invalid JSON: unpinned
settings reads (loadSettings, createMinimalSettings, ...) parse the
operator's real files. test-setup now pins QWEN_HOME to an empty
per-worker temp dir, matching how it already isolates other operator
env state. settings.test.ts mocks the filesystem outright and keys its
fixtures on the os.homedir() mock, so it drops the pin before its
module-load path constants are captured.

* fix(cli): keep genuine callback timeouts out of the Hook fence classifier

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

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

* fix(cli): keep genuine callback-phase cancellations out of the Hook fence classifier

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

* test(cli): drain the crashed wake park before teardown in hosted-harness tests

The 'refuses a prompt retriably when the wake park probe read nothing'
test ended with its crashed automation wake park deliberately unsettled,
so the session's wake pump kept retrying the settle past teardown; under
parallel-file load the retried failures logged into later tests' stderr
spies and failed their blanket no-log assertions (reproduced pre- and
post-merge in ~2/3 of 8-file batch runs; the owning test was confirmed
with temporary owner-stamp instrumentation).

- Drain the parked settle (journal-witnessed turn.settled) before the
  delete, matching the file's sibling pattern; the delete then answers
  204 and closes the scheduler.
- Scope the two blanket writeStderrLineSafe never-called assertions to
  lines naming the scenario's own turn/operation id.
- Retry the session-root rm in afterEach against in-flight pump writes
  (ENOTEMPTY race observed once pre-merge).

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

* fix(cli): unstrand the runtime hold when pathToFileURL throws (#13243)

A synchronous pathToFileURL throw escaped the importHookModule executor
before the derived promise existed, so none of the three
moduleEvaluationPending clearing handlers was ever attached and
hasHolds() stayed true for the worker's lifetime (Windows UNC paths
pass manifest validation and then throw). Convert the href inside a try
ahead of arming the flag, and pin it with a test that mocks node:url to
throw.

Also restore the two strict writeStderrLineSafe assertions in the
Hosted Harness no-tool suite: quiesce each test's wake pump in
afterEach via DELETE /session/:id (bounded, so a Session fenced behind
a never-answering Runtime poll cannot wedge the hook) instead of
narrowing the assertions around a leaked pump, and drop the rm
maxRetries workaround the leak had forced.

---------

Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-10-12 00:49:42 +00:00
Shaojin Wen c81e0b4a9b feat(managed-agent): Introduce the EventTransport seam behind the after-commit publisher (#13909)
* feat(managed-agent): Introduce the EventTransport seam behind the after-commit publisher

Phase MQ1 of the cross-node EventTransport design: name the adapter point
behind the after-commit hook as EventTransport and route publication
through a single composite bean — local SessionEventHub fan-out first
(unchanged failure semantics), then default-empty EventTransport adapters,
each isolated so a failing adapter never blocks local post-commit SSE.
No broker, no configuration, no behavior change; MQ2 plugs in additively.

Part of #12380 · Refs #13200

* feat(managed-agent): Pin the MQ1 seam contract points from review

Review follow-ups: pin batch pass-through, the drop log's
tenant/session/sequence identity, and container collection of
EventTransport beans; record the EventTransport implementer contract
(synchronous on the committing thread, RuntimeException-only isolation,
borrowed list); sync the slice doc and narrow the parent design's pre-MQ1
present-tense claims, both languages.
2026-10-11 16:55:39 +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
jinye 7cbb993ccd fix(managed-agent): recover original Hook sessions during Workspace retirement (#13673)
* fix(runtime-broker): recover original Hook sessions during drain

* fix(runtime-broker): drain absent original workers before Hook detach

* fix(workspace): release stopped Hook holders under the drain claim

* fix(runtime-broker): initialize the stopped Session holder schema

* test(workspace): preserve the unknown execution reference identity

* test(workspace): persist unknown execution through its dispatch claim

* codex: address PR review feedback (#13673)

* fix(runtime-broker): match the lease snapshot to the maintenance column

V60 adds maintenance_id to the execution lease, and the broker schema snapshot has to carry the same column or the Flyway parity check fails after merging main.
2026-10-11 15:41:27 +00:00
Shaojin Wen 7cc2054a7c docs(managed-agent): H4e-b3 member commands, plan approval and shutdown design (#13912)
Designs the last H4e runtime slice: a member-to-lead command channel
(a new session_command domain on H4d-b's relay, executed on the lead's
team funnel), member board tools under Legacy's rules, member-to-member
messages relayed by the lead to running members, asynchronous plan
approval decided by the lead's model with team_plan_approval, and
shutdown as a lead-side stop. team_message stays unproduced and legacy
import is a non-goal. Implementation lands in four batches, b3a to b3d.

The H4e-a, H4e-b1, H4e-b2 and H4d-b designs and the H4a and H4b
delivery maps gain pointers; H4e-a's team_plan.requestId and
team_message texts follow the new decisions.
2026-10-11 14:54:18 +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
Long Jin f8c99414b2 fix(core): report monitor startup failures as tool errors (#13460)
* fix(core): report monitor startup failures as tool errors

* fix(core): classify monitor startup failures consistently
2026-10-11 14:21:47 +00:00
Shaojin Wen 7f496aca15 test(managed-agent): Add FG7 channel fault gates for the channel domains (#13802) (#13839)
* test(managed-agent): Add FG7 channel fault gates for the channel domains (#13802)

The channel_route and channel_delivery domains are enabled since #13572,
but nothing yet proves the channel side effects stay honest across lost
answers, mid-send process deaths, and ambiguous provider replies. FG7
pins them in FG6's shape: real packaged binaries, a fault-injecting
proxy, a real MySQL lane, and one named mutation per gate.

- FG7a/b/d/e/f in HostedChannelFaultGatesIT (15 cases, in-JVM Spring,
  SQL trigger store faults, per-letter -Dqwen.fg7X.case selectors);
  FG7c in HostedChannelProcessCrashIT + HostedChannelCrashFixtureMain
  (4 cases, killable child-JVM control plane and adapter).
- One parametric TS driver (hosted-channel-fault-driver) with a
  content-aware lose-once proxy, the real ManagedEmailAdapter against
  scripted FakeImap/FakeSmtp, and a recording wrapper over the
  production HttpManagedChannelControlPlane so the wire shape cannot
  drift from production.
- FG7e gates read-ledger continuity instead of an SSE surface that
  does not exist (cursor pagination stays newest-first, dedup-free);
  FG7f gates the unknown family; partial waits for H5d's multi-segment
  plan.
- CI budget raised fork 900s to 1200s, Verify step 25 to 35 min, job
  148 to 160 min, pinned by hosted-process-ci.test.js, plus a local
  hosted-channel-gates profile.

15 mutation red signatures verified by execution (the 10 designed ones
plus M2/M5 collateral cases), all reverted to green.

* docs(managed-agent): FG7 header and validation wording audit follow-ups

Open-ended audit of the FG7 diff found two wording drifts left by the
review rounds: the driver file header still described the hand-rolled
thin-HTTP client the wave-2 fix replaced with a recording wrapper over
the production control-plane client, and the surefire count in the
validation bullets read as an at-HEAD number although the rebase moved
the baseline (main added unit tests since). Wording only; no behaviour.

* test(managed-agent): fail the FG7e reads when the parked driver fails

The two FG7e parked-pull branches asserted `driver.waitFor(...)` completes
and went straight to the JDBC/read assertions without checking the driver's
exit value. A driver exiting 1 at its settle-oracle (for instance the
exact send/receipt counts introduced in this PR) could therefore leave both
`delivery-page-resume` and `delivery-state-progress` green — the other
parked-pull branches propagate either through `exitValue()` or through the
success-only results file. Both branches now assert
`driver.exitValue() == 0` with the driver log attached; a sabotaged driver
(exit 1 after settle) redness-witnesses the propagation, reverted to green.

Also corrects mutation-table entry 8 in both design languages: the
production claim discovery is the two Java layers
(`listSessionsWithPendingDeliveries` + `findPendingDeliveries`), not
`pendingDeliveries()` (unit-test callers only); the executed mutation was
the compound of both discovery filters plus the funnel `claim` terminal
refusal weakened together, and only that compound reaches the wire.

* test(managed-agent): FG7 review wave fixes and the local suite blockers

Answer the second review wave (R1-3 to R1-47 suggestions) and two
reproduction blockers the rerun forensics found locally:

- Positive controls on silent paths: cancel-planned/replay assert a
  claim pull reached the control plane; the out-of-plan-ordinal gate
  asserts the 409 envelope code; receipt-reply reads the ledger state
  and provider receipt; the resend case asserts segment identity; all
  ten vacant-sends sites go through assertSendsEmpty.
- Evidence hygiene: per-letter and per-class case roots, per-launch
  driver tags, awaitLog writer-liveness, drive() reaping in finally,
  caseLogs inclusion of fixture logs, fg7-cases artifact upload.
- Launch reliability: --server.port=0 for the in-JVM Spring and the
  crash fixture public listener; the harness bound port must match
  the reservation; -Dnode.executable asserted before boot.
- spring-kill-dispatch: holdUntilFile keeps the deceived adapter
  ticking across the bounce and the driver ticks after release, so
  the warm claim relay reaching the restarted control plane is real
  (postBounce) instead of vacuous.
- The workflow's path filters now cover packages/channels/email and
  packages/cli/src/commands/channel, mirrored in the pinned test; the
  job headroom moves with its 13-minute estimate; the hosted fork
  budget is pinned literally at 1200.

* test(managed-agent): FG7 review follow-ups on positive controls

Second round of answers to the suggestion wave, all verified green
twice on the full FG7 suite:

- relayCountOk counts only relays whose answer is < 400 for the
  positive controls (adapter liveness, outbox pulls, re-drives); a
  4xx/5xx answer at the proxy is not evidence the adapter kept
  working. The drop counters stay on relayCount.
- assertSendsTo pins the recipient of every physical send on the
  exactly-once cases, so a misdirected re-drive cannot pass on
  counts and messageId alone.
- spring-kill-dispatch re-reads both unknown projections after the
  cold resume, matching its sibling re-checks.
- The crash fixture's /control/reconcile and /control/expire report
  a failing call as 500 with the exception text instead of an
  unconditional 204, so a reconcile-side failure fails at the
  control() call with the cause attached.

* test(managed-agent): answer the R2 review wave for the FG7 channel gates

Second suggestion wave (R2-1 … R2-27) answered across the driver, both
IT classes, the workflow and the design docs, verified green twice on the
full FG7 suite before commit:

- Positive-control hygiene: the postBounce witness now only counts a
  healthy answer (status < 400) past the bounce mark; both crash-class
  cold-start zero-send legs assert the cold adapter's claim pull provably
  reached the control plane beside the no-send check; spring-kill-dispatch
  re-reads ledger and input counts after the warm window so the widened
  window has its own duplicate-effect witness.
- Evidence hygiene: drive() launches keep the DriverLaunch tag so
  consumer names stop being re-derived as -1 literals at all 25 sites;
  the two pull-terminate waits went through the writer-liveness-aware
  awaitLog on FG7_AWAIT_KILL, preserving the ordering the Files.exists
  poll relied on; caseLogs is total (safeRead + a bounded fallback) in
  both classes; await surfaces the last probe failure in its timeout
  report instead of a bare deadline; the driver persists its results
  file before propagating a failing phase; the exact-once and zero-send
  witnesses are one helper each, called from both exit paths;
  FG7_RELAY_FAILED prints the actual fetch cause, not just "fetch failed".
- Docs truthfulness (both languages): the guard actually gated is the
  missing-segment refusal surfaced as 409 channel_operation_conflict —
  the dense-ordinal store rule stays reachable only under H5d's
  multi-segment plans; the fault-fired mechanism list covers all 19
  cases; the FG7b trigger lifecycle names the per-class teardown that
  isolates it; the adapter-kill row names the resend assertions that
  actually belong elsewhere; FG7e's ledger rows state the frozen-state
  setup, the separate post-release re-read, the per-delivery route and
  the planned-minted-at-claim visibility rule; the driver inventories
  carry holdUntilFile and FG7_RELAY_FAILED, and the marker list splits
  sequencing markers from diagnostic log lines; evidence filenames get
  their per-launch tags; the acceptance bullet narrows to what actually
  ran (compilation only for the Java-side files, not Checkstyle /
  SpotBugs on test sources).
- CI: packages/channels/base/** and packages/cli/src/commands/channel.ts
  added to both workflow trigger lists and to the pinned test's list;
  the fg7-cases artifact path is pinned in hosted-process-ci.test.js
  with its module-qualified string next to the budgets it moved.

A note on diagnosis recorded for honesty: the first replay of the full
suite ran red across all five letters on an unmodified batch, with the
same \"managed email admission ... was refused (400)\" seen on earlier
runs; instrumented ApiExceptionHandler.invalid() captured the cause as
IllegalArgumentException(\"Unsupported JSON value type: ObjectNode\")
from JsonSupport.writeCanonical in the shared m2 alpha jar, which had
been rebuilt by a parallel worktree whose alpha build clobbers it. The
tests never saw that before because the failure window is the jar's
rebuild window; this tree's own qwencode module predates that
canonicalizer — its JsonSupport encodes the same body without that
rejection. After a straight reinstall from this branch, fg7a alone
passed in 8.2 s and both full-suite runs are green — this red was
never in the R2 diff.

* test(managed-agent): FG7 post-review exit-witness and post-bounce hardening

Answer the three R3 review threads on the channel fault gates:

- The parked-pull waits shared by cancel-sending and out-of-plan-ordinal
  asserted waitFor completion without the exit code, but since the
  driver's catch now persists its results file before propagating a
  failure, a driver-side assertion failure leaves a readable
  results-<tag>.json and then the case proceeds to passes at its coarser
  checks anyway. The sibling exit-value zero assertion closes that:
  a driver that dies with its own quota/identity check still makes the
  case fail because results were just written but nothing asserts what
  it contained.
- spring-kill-receipt now mirrors spring-kill-dispatch's positive
  control for the receipt verb: a warm receipt relay with a healthy
  status (status < 400) must land after the bounce, measured by
  bounceDone instead of any relay that happened with the server gone.
  bounceDone is declared outside the try block so the witness compiled
  beside the witness reads (same shape as the existing twin).
2026-10-11 13:39:34 +00:00
qqqysandqqqys e6ed740eb1 feat(mods): add TypeScript authoring and configuration (PR2a) (#13890)
* feat(mods): add TypeScript authoring and configuration

* fix(mods): preserve marketplace dependencies and update notices

---------

Co-authored-by: qqqys <[email protected]>
2026-10-11 12:59:02 +00:00
Shaojin Wenandgithub-actions[bot] 186193e250 fix(managed-agent): settle worktree child runs after their merge (#13753 I2 follow-up) (#13872)
* fix(managed-agent): settle worktree child runs after their merge (#13753 I2 follow-up)

Follows up the review deferrals of #13841.

A stopped worktree run kept its child Workspace. The stop arm (H4f)
settled the run cancelled without a finish request, so the row stayed
ready with the worktree on disk. It now asks for the discard before
its close_scope, or for the merge when the child's task Turn had
completed before the stop.

A failure was told before the merge landed. The over-bound, give-up
and later-failed-Turn arms settled the run failed while the merge they
asked for ran afterwards. A merge the relay asked for now holds every
settlement of the run (a failure, a stop or a give-up, not only the
result) until the row records its outcome. A child whose task Turn
completed keeps its work when a later message turn fails, its message
wait runs out or a stop cancels it, and the relay records that merge
as soon as it sees the task Turn of a bound child completed, so a
parent's close meanwhile pins the work. The foreground answer and the
recovery gap fill of a started worktree run that failed or was
cancelled end with a line telling the parent's model to inspect its
Workspace and where unlanded work is kept: the record has no receipt
to carry the outcome.

A merge waited on every heartbeat on a host that runs no child
Workspace steps. It now waits at the close-debt cadence there, as on a
host without the close capability, and the merge is asked even where
the child cannot be closed yet.

A parent's close discarded a completed child's work unpinned when its
discard replaced a merge that had not started, or one that ended
blocked before its pin. Such a discard is recorded as discard_pinned
and pins the child's result before removing the worktree. The pin
never holds the discard back: when no result can be taken (a
non-retryable refusal, or a failure on the last retry) the discard
goes on as it did before.

No contract change and no migration: discard_pinned is a new value of
the existing finish_request column.

* fix(managed-agent): settle lost-creation worktree runs without an unbound finish (#13872)

Review follow-ups for the child workspace settlement round:

- stopChild and the give-up chain never record a child Workspace finish
  on a row whose child was never bound (a lost creation answer): the stop
  binds the lineage-named child first, and the give-up discards instead
  of asking for a merge it cannot wait for without re-entering itself.
- The ended-run workspace line claims the merge's ordering, never its
  landing (a conflicted, blocked or pinned-away merge means the work did
  not land); both render sites now share childRunEndedText.
- The owed-merge wait logs what it holds for and no longer re-reads the
  child result text or re-asks the merge on every pass.
- One isDiscard predicate for the two discard spellings, one pinResult
  for the two take-and-pin sites, a witness for capable(), and the
  discard-pin fallback now logs the full key set and records last_error.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-11 11:58:52 +00:00
Shaojin Wen edec849aa9 feat(managed-agent): H4e-b2 the lead's mailbox over Session messages (#13892)
* feat(managed-agent): H4e-b2 the lead's mailbox over Session messages

Implement the H4e-b2 design (#13879) on top of H4d-b's Session messages
and H4e-b1's team, with no record, rule, route, migration or Java change.

- A member is its continuation chain. The roster names the chain's first
  run; task_list, team_delete and the owner rule read the chain head.
  sendToChild continues a finished member of an active team, which
  #13846 refused until this slice; a closing team still refuses, and a
  former member continues as a plain child.
- The lead's send_message beside the team tools takes to (a member name,
  or "*" for every member) or task_id. A name routes as its first run's
  task id does under the same ids; a broadcast commits one message or
  continuation per member under bcast_/msgb_ ids that no single send or
  launch can mint, and a batch that broadcasts cannot change the roster.
- Every run of a member's chain opens its instruction with a fixed
  preamble naming the member and its team and saying that only the
  answer to the last instruction reaches the leader. It counts toward
  the envelope bound, and a replay accepts the committed instruction with
  or without a well-formed preamble block.
- A member's message to the lead and a continuation's result carry the
  <teammate> label while the chain root is on the open team's roster;
  the reply hint stays H4d-b's task id.
- task_update delivers nothing. The owner rule accepts a member given
  work earlier in the same Turn even if it has completed since, keeps
  stopping and stop-requested heads off open work, and its refusals
  never claim which task a past send carried or offer to skip a send for
  work that never arrived.
- Recovery and the PostToolUse dispatch evidence probe a send_message's
  base ids and every broadcast position.

The H4e-b2 design now records the implementation; the E2E plan lists the
physical pass that runs before merge.

* fix(managed-agent): settle the round-1 audit findings on the H4e-b2 mailbox

- The owner rule checks a never-started newest continuation before work
  given in this Turn: an earlier send of the Turn cannot vouch for work
  the latest continuation never delivered.
- A task id of a continuation that never started routes from its
  predecessor, so an answer that named it still reaches the member.
- A replayed send by name answers with the member's name and label only
  while its team is open; a broadcast replay whose team was deleted or
  replaced answers for the recipients it reached and sends nothing new.
- One chain index serves the roster view, a broadcast and sendToChild,
  which now shares the member views' head definition; the PostToolUse
  dispatch evidence probes broadcast ids without indexing the lead's
  runs.
- A member's byte_limit refusal names the preamble; dead team helpers go;
  the board and roster descriptions match the owner rule.
- Tests cover the edges the round-1 mutants survived: stopped members
  given work this Turn, a kept owner given work, a cancelled message,
  never-started notes in answers, the active cap in a broadcast, the
  input order behind the approval preview, and broadcast dispatch
  evidence after a restart.

* fix(managed-agent): settle the round-2 audit findings on the H4e-b2 mailbox

- sendToChild returns, on its writes chain, what an answer must tell about
  the run a send reached: the open-team member, a continuation that never
  started before the one it launched, and, on a replay, its own run proven
  never started since. Answers by name, by task id, on replay and in
  recovery all read it, so a member addressed by task id is named, a
  replayed or recovered send that never arrived says so, and the note no
  longer misattributes a later continuation to a replayed broadcast.
- A never-started continuation's note, roster line and owner refusal count
  the messages it lost, and tell a continuation a user cancelled before its
  start apart: its work is asked for again only if still wanted.
- Refusals to a member name the member; an ended one points to a new member
  under a new name.
- Recovery of an interrupted broadcast counts a never-started recipient as
  not reached and lists ended members apart.
- task_update's notice acknowledges a send earlier in the turn.
- chainHead throws instead of falling back to its input.
- Tests for a rejected message, a launch not counting as work given, the
  launch-side broadcast batch rule, the encoded input order, a broadcast
  reaching no member, broadcast replay after delete or replace, admit
  evidence, and the recovered never-started send.
- The design text follows.

* fix(managed-agent): settle the round-3 audit findings on the H4e-b2 mailbox

- The owner rule records an ended member on the task a call completes when
  any run of its chain completed, as #13846 records who did the work; a
  member that never completed a run is pointed to the member that did it,
  not to a new member.
- task_update's answer for a restated owner, which is not checked again,
  reads the member: a newest continuation that never started is asked for
  again, and an ended member is never pointed to a send.
- A never-started continuation's work and the messages it lost are asked
  for again together; a replayed or recovered send asks again only for its
  own message, says "your resend" when it did not arrive either, and a
  message to a launch that never started says the member has ended.
- A member stopped before its continuation attached is refused with a text
  that waits for it to settle instead of pointing to a new member.
- A broadcast sends to no further member once its turn is cancelled, and a
  partial answer asks for sends by name; recovery of a broadcast whose team
  changed since names no new roster.
- Tests for the guards the audits found unpinned, and the design text,
  including the never-started task id route and the index note.

* docs(managed-agent): quote the H4e-b2 partial broadcast answer as implemented

* test(managed-agent): expect H4e-b2's owner refusal in the Hosted team IT

* docs(managed-agent): record the H4e-b2 physical acceptance results

* fix(managed-agent): read a message's delivery before answering it as reached

A committed message whose delivery failed did not arrive, though its run
did not prove it never started: cancelled before its handover, rejected by
its target, or still waiting for a handover the relay cancels because its
run ended or is being stopped. The route details now read the message's
delivery, so an interrupted broadcast's recovery no longer counts such a
recipient as reached (an ended member is listed as unable to take it, and a
live replay notes it on the member's line), and a replayed or recovered
single send says the message did not arrive. The answer points to a resend
while the chain takes messages, to waiting while a continuation stopped
before its start settles, or to a new member (a new agent for a plain
child) once the chain has ended. The H4e-b2 and H4d-b designs say so.
2026-10-11 11:11:29 +00:00
fce50611b6 fix(managed-agent): connector and broker robustness from the #12692 R2 review (#13330)
* fix(managed-agent): connector and broker robustness from the #12692 R2 review

Nine follow-ups from the post-merge R2 review round on #12692,
re-verified against current main (5ddfacc9d4) before patching:

- The durable session row is now the archive/delete fence: the broker
  resolver rejects ARCHIVED/DELETED rows for unbound Sessions, so the
  in-process retired set stops growing on the delete path and the
  fence survives restarts. Close keeps its in-process retirement.
- Attach fetch moved out of ConcurrentHashMap.computeIfAbsent: a slow
  create no longer stalls every attachment hashing to the same bin.
- The connector close protocol no longer leaks a client built after or
  during close(): a closed flag under the same monitor fails late
  calls before any executor or heartbeat is constructed.
- Hosted Harness base URL is validated as an absolute http(s) URI at
  startup; "localhost:4170"-style mistakes no longer die opaquely at
  the first RPC.
- Tool-call item identities tag both branches, so a harness callId of
  the literal form "source:<n>" cannot collide with the no-id
  fallback.
- The dispatch constructor rejects lease/renew misconfiguration
  (non-positive values, renew >= duration) instead of letting it
  surface later as hosted_harness_unavailable.
- Dependency failures at the rename site keep their root cause via a
  cause-carrying ApiException instead of an unchained 503.
- A poisoned materialization target no longer heads every 100 ms pass:
  consecutive failures retry on 1, 2, 4, ..., 64 streaks and the
  target rotates behind fresher rows (covered_sequence untouched), so
  healthy sessions are never starved and identical warnings stop
  repeating every pass.
- The kubernetes provisioner rejection now says it is unsupported and
  names the valid values instead of referencing an internal review
  slice.

Verified on the managed-agent-server module with mvn verify: 562
tests (new cases pin each change), checkstyle and spotbugs green.

* test(managed-agent): close the method boundary between the two merged tests

The tail splice merged sourcePrefixedCallIdDoesNotCollideWithTheNoIdFallback
into projectsDeadlineExpiryAsAClassifiedFailure without the closing brace
and @Test annotation (main-scope compile passed; test-compile is the gate
that sees it).

* test(managed-agent): correct the stream-close comment in HostedHarnessMySqlIT

HarnessEventStream moved from a monitor to a ReentrantLock in #13388, so
close() no longer contends on a synchronized read; it queues on the
stream lock while next() holds it across the blocking socket read.

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

* fix(managed-agent): fence closing sessions durably and keep tool item identity on the v1 rule (#13330)

* fix(managed-agent): address the #13330 R2 review findings

- MessageMaterializer: best-effort deferrals via deferQuietly, warning
  before rotating, so a store fault while deferring degrades one target
  instead of aborting the pass and swallowing the diagnostic
- QwenHostedHarnessConnector.close(): capture and null the client under
  clientLock, close it outside the lock so the executor shutdown no longer
  stalls client() callers behind it
- HarnessCoordinator: validate lease durations as the millis the scheduler
  receives, rejecting sub-millisecond renew intervals at startup
- EventIdentity: share the callId tool-item rule with the Harness event
  projector via the public toolCallItemId; the projector test now asserts
  against the authority instead of a re-derived literal
- Tests: rotation witness for deferMaterializationTarget against the
  selection ORDER BY, per-clause base-URL guard cases, negative and
  sub-millisecond renew cases, close-outside-the-lock witness; rename a
  sequential test that was named for concurrency
- Docs: sync gate 3 of the review-corrections design (both languages) with
  the durable lifecycle fence

* fix(managed-agent): fence closed Sessions on the warm path only

The durable lifecycle fence sat in the shared HarnessSessionResolver
lambda, so the release and unknown-outcome reconcile routes — which run
after a Session closes — hard-refused a CLOSED Session with a
non-retryable 409 and leaked the binding the teardown exists to release.
Scope the fence to a new admission resolve that only warm() consults;
teardown routes resolve the closed Session's scope again. Also defer a
poisoned materialization target only when its retry is not due, so a due
pass stops writing the progress row twice, and sync the README/design
wording with the shipped fence.

* chore(managed-agent): re-present the round-3 fixes with the tree cleaned

The deterministic gate rejected 6a671b32cb on npm run lint alone: the
round's own Maven verification had generated JaCoCo/Javadoc JavaScript
under packages/sdk-java/*/target (gitignored build output, never part of
the committed tree), and ESLint scans those files when they exist on
disk. The artifacts are removed and lint, build, and typecheck were
re-run green on the final tree. No tracked content changes; the round-3
fixes are preserved as-is.

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

* fix(managed-agent): converge teardown behind the drain fence

The persisted ARCHIVED/DELETED fence in the embedded broker's session
resolver guarded every scope resolution, so the durable release and the
unknown-outcome reconciliation of a closed Session died on a
non-retryable runtime_broker_session_closed after closing. Split the
resolver into a bootstrap channel (fenced: warm, acquire) and a teardown
channel (permissive: persistedSession's release and reconciliation) via
HarnessSessionResolver.resolveForTeardown.

MessageMaterializer's pre-attempt defer ran outside the due gate, so
every 100 ms pass updated every poisoned row, and the capped streak made
attempts and their stack warns fire at 10 Hz forever. Skip passes are
now the only ones that rotate the row and the streak is uncapped, so
attempts and warns ride the power-of-two ladder.

The README still described the drain fence as an in-process retirement
flag; it is persisted Session status.

* test(runtime-broker): pin the admission fence to warm in the teardown release witness

Renames the fake resolver's teardown outlet to the admission channel the
interface now exposes, flips the witness to fence warm while release keeps
resolving, and keeps the durable release assertion.

* fix(managed-agent): gate the skip-path rotation on window saturation (#13330)

A skipped pass below TARGET_LIMIT rotated a row the next pass selects
anyway, so the throttle fires only when the selection window is full; the
rotation warn is stack-less so a persistently faulting deferral cannot bury
the one-line signal at scheduler rate. Pins the streak reset, the skip-path
guard under a fault, and the snapshot_stale_since marker the deferral must
not clobber, narrows the README fence clause to the warm admission resolve,
and corrects the release witness comment to what the test executes.

* fix(managed-agent): address the #13330 R4 review findings

Probe one row past the selection window for the skip-path rotation gate
so an exactly-full window does not rotate rows that are selected again
anyway, pin the post-cap modulo retry at streak 192, and mirror all five
of the client builder's base-URL rules in the boot-time guard.

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

* chore(managed-agent): re-verify the #13330 R4 round on a target-free workspace

The deterministic gate rejected e5940ae490 on 'npm run lint': the previous
round's Maven verification runs left generated JavaScript under
packages/sdk-java/*/target (jacoco-resources/prettify.js + sort.js,
reports/apidocs/*.js), and the repo-wide eslint scan fails on those
gitignored artifacts (728 errors, all confined to target/). No tracked
change can carry this repair: the root eslint config and the lint script
are verification machinery outside this round's footprint. The fix is
environmental — the target/ trees are removed so the gate's lint re-run
scans only tracked sources — and this empty commit re-presents the
preserved R4 round for re-verification.

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

* fix(managed-agent): address the #13330 R5 review findings

Fence the materialization tick onto its own scheduler pool so a faulting
skip-path rotation cannot occupy the dispatch thread, require a lease
renew interval of at most half the lease duration so a late renewal keeps
its margin, attribute the rename WARN to the operation rather than a
subsystem the fault may not be in, and pin the saturation probe row's
exclusion from the work loop.

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

* chore(sdk-java): clean generated Maven target dirs to unblock lint gate

Same-run verification repair: the previous round's local mvn runs left
generated JaCoCo report JS under packages/sdk-java/*/target. Those dirs
are git-ignored but are not in the root ESLint ignore list, so the
gate's npm run lint (eslint .) failed with 139 errors on generated
code. The artifacts are untracked, regenerable build output; removing
them from the working tree restores lint. No source change.

* fix(runtime-broker): fence a fresh acquire behind the admission resolve (#13330)

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

* test(managed-agent): align the rename fault witness with the renamed store call

The aFailedRenameLogsAndChainsTheRootCause store mock still stubbed the
pre-rename completion entry point; production now persists rename results
through completeSessionRename, so the mock fell through to the real store
and the rename succeeded, failing the assertion that it must raise. The
stub is pinned to completeSessionRename with its six-argument shape.

* test(managed-agent): drop the racy live-title read in the rename retry witness

The retried rename is completed either by the synchronous third call's
inline claim or by the delivery coordinator's asynchronous re-drive, and
either order is a legitimate completion order under the admission-fence
semantics established by #13682. When the coordinator wins, the replay
correctly answers the session's current state, whose title is the
sibling rename's value instead of the retried one. The replay flag and
the no-duplicate-events inventory carry the witness deterministically;
the live-title read depends on which lane finished first.

---------

Co-authored-by: qwen-code-autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code <[email protected]>
Co-authored-by: qwen-code-autofix <qwen-code-autofix[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
2026-10-11 08:43:44 +00:00
Shaojin Wen 23274922f5 feat(web-shell): add Send now for queued mid-turn messages (#13681)
* feat(web-shell): add Send now for queued mid-turn messages

A message typed while a turn runs waits for the model response that is
streaming and for its tool batch, so a correction typed into a long answer
reached the model seconds later, and Stop was the only faster path.

Typing keeps that behavior: the message stays queued, editable and
deletable until the next tool boundary. A new Send now action on a queued
row asks the daemon to deliver the queued messages at once.
POST /session/:id/mid-turn-messages/send-now, advertised by
session_mid_turn_send_now, sends the agent craft/midTurnSendNow. While a
foreground turn streams a response that has not started a tool call, the
agent drains the user's messages, cuts the response short, keeps the part
already streamed, and answers in the same turn. Running tools are never
interrupted, a response that started a tool call is never cut, queue-only
steering is not taken early, and a request is scoped to the turn that
received it. Input taken by a turn that is then stopped or fails is kept
in that turn's conversation.

The TypeScript SDK gains sendMidTurnMessagesNow on both clients.

* fix(web-shell): address review findings on Send now

- Restart the Stop-hook state when a continuation answers input that
  cut its response short, so the next Stop check is not hook-forced and
  the consecutive-block count and reasons start over.
- Give input a finished turn keeps unsent a 2 s resolution deadline of
  its own; the deadline also cancels media bridges.
- Requeue a failed send-now drain in its original order, and log a
  send-now request the agent rejects.
- In Web Shell, reconcile a row whose message already left the queue
  and ignore a repeated click while a request is out.
- State the tool-call carve-out wherever Send now is described, and
  document goal turns, the narrow no-answer window and token accounting.
- Add tests for the review's gaps, drive the drain deadline with fake
  timers, and add the route to the multi-workspace failure tables.

* fix(web-shell): keep send-now input a stopped tool boundary leaves unbuilt

When a turn is stopped while a tool-boundary drain resolves a message,
the drain now hands back the messages it has not built that an early
send-now drain took, so the end-of-turn keep records them. Input the
boundary drain took for itself is still dropped there, as before.

- Tests: the stopped boundary keeps that input once and carries nothing
  into the next turn; a Goal continuation is cut; a background
  notification turn is not; a failed send-now drain does not requeue a
  message that left the queue meanwhile.
- Bridge and SDK contracts: the cut applies to a foreground turn's
  streaming response, and `requested: true` does not mean a cut.
- Protocol and design docs: the timed-out send-now drain, resending a
  settled id, the 10-message drain limit, and the tool-boundary drop.

* test(web-shell): pin send-now behavior questioned in review

- Assert that cron and background-notification turns never open their
  response to interruption and that a send-now request arms nothing
  while they stream.
- A response cut short by queued input still answers that input when it
  ends with an error, while an error from a response its drain did not
  cut remains the turn's error. Explain in `watch` and the design doc why
  the cut response's error is absorbed: the cut does not always arrive as
  an AbortError, and the response was given up.
- A message whose deletion missed while a failing drain held it is still
  delivered, which matches the `{ removed: false }` answer the client
  got. Document this in the protocol reference.
- Rename a requeue test to say it covers a queue-only entry.

* docs(web-shell): scope the abort invariant and drain-held removals

- The main prompt loop's catch states that other AbortErrors surface.
  Note that a response cut short by queued input reaches that catch only
  when the turn itself is stopped meanwhile, because `watch` absorbs
  whatever a cut response throws.
- In the force-drain paragraph, add the drain-held case of
  `{ removed: false }`. Such an id must not be re-posted, and its listing
  in `settledMessageIds` is not final, because a failed drain returns it
  to the queue without an event.
2026-10-11 06:24:48 +00:00
Shaojin Wen bb7de6aeed test(managed-agent): run the H4e-b1 team flow end to end in the Hosted MySQL lane (#13867)
* test(managed-agent): run the H4e-b1 team flow end to end in the Hosted MySQL lane

HostedTeamLeadMySqlIT drives a real Spring server on MySQL, the packaged
Hosted Harness, the embedded Runtime Broker and the child result relay,
with a scripted model, through three cases the physical pass covered:

- a lead creates a team, launches two named members, keeps a board, is
  refused team_delete while they run (the refusal writes nothing), takes
  each member's report under its own label, completes the tasks (one
  owner kept, one set only once its member finished, the refused open
  ownership writing nothing) and deletes the team;
- closing the lead cancels its running member: through four refused
  cascade attempts the close stays owed, the member's Session stays
  active and its Turn keeps running; the Turn then completes on its own,
  the team records stay as they were, and once the relay is done the
  lead holds no acceptance of the result and has made no new request;
- in default approval mode each team write and the named launch asks on
  its own card, previewing its own input, and task_list asks nothing.

The lead is created through the store and moved to the Shell profile in
the same transaction, because no public route creates a Shell-laned
Session; everything after that goes through the public routes. Each case
closes its lead and waits until the tenant owes no operation, child
result or Turn, because the operation recovery and the relay scan every
tenant and the next Spring context in the fork would pick that work up;
a failed case ends that work in its teardown. Each Session's Broker
worker is durable and outlives Spring, so the cleanup HostedPublicWorkspaceIT
used moves into HostedDurableWorkers and both ITs call it.

The Hosted ITs share one 900 s fork. A fixture failure, a failed lead
Turn or member, a report without its label or a lead without the team
tools ends a case at once. Awaitility ignores the interrupt a @Timeout
sends, so every wait goes through a helper that fails on it, which makes
each case's ceiling (80 s, about 20-26 s healthy) a real one. The CI row
in hosted-process-ci.test.js pins that every case has a ceiling, that
all of them together fit the fork beside its healthy runtime even if
every case stalls, and that no wait bypasses the helper; the healthy
runtime the Hosted rows assume grows to 650 s with this class. A failed
case prints the Harness log tail, where a failed wake Turn shows.

* test(managed-agent): close the Hosted IT Spring test contexts after their class

The Hosted*IT classes share one failsafe fork and one MySQL schema, and
Spring caches a @SpringBootTest context for the rest of the fork.
HostedConcurrentTurnBurstMySqlIT's context has a fixture Harness but no
Runtime Broker, and its operation and Turn recovery scan every tenant, on
a 50 ms dispatch scan. When HostedTeamLeadMySqlIT ran after it in CI, that
context claimed the team cases' close operations and could not close their
Workspace Sessions ("This instance cannot verify the original worker
stop", "close-incapable host"): in the close case the member's close was
blocked, so the lead's close never completed, and the approval case's
lead close ended RECOVERY_BLOCKED.

Close that context after its class, and HostedWorkspaceRecoveryWorkerIT's
too (H2, but its schedulers otherwise run to the end of the fork). A
hosted-process-ci row now requires @DirtiesContext, AFTER_CLASS or the
default, on every Hosted*IT whose class declares a Spring test context.

* test(managed-agent): end a stalled burst round's work and harden the context guard

Now that HostedConcurrentTurnBurstMySqlIT's context closes after the class,
a round that stalled would leave its Turns and operations for the next
class's context to claim. End them in @AfterEach, before the context
closes, as HostedTeamLeadMySqlIT does for its tenant.

The hosted-process-ci context row now strips literals and comments in one
pass (a "/*" inside a line comment no longer swallows the class header),
reads the header from where a type declared before the class ends, accepts
@SpringJUnitWebConfig, an "@ " spacing, empty parentheses, hierarchyMode
and AFTER_EACH_TEST_METHOD, and still refuses BEFORE_* modes.
2026-10-11 06:22:19 +00:00
jinyeandQwen-Coder 6936c77e60 feat(managed-agent): support WebShell session cwd changes (#13760)
* feat(web-shell): support managed session cwd changes

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

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

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

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

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

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

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

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

* refactor(managed-agent): address triage nits

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

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

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

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

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

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

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

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

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

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

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

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

R1-12.

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

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

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

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

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

* codex: address PR review feedback (#13760)

* codex: address PR review feedback (#13760)

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-11 04:55:03 +00:00
易良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
Shaojin Wen 90f066af56 docs(managed-agent): H4e-b2 team mailbox design (#13879)
* docs(managed-agent): H4e-b2 team mailbox design

Pin the decisions for the second runtime slice of H4e (#13745) before
implementation. Messages between the lead and its members travel as
H4d-b Session messages: a member is a child run of the lead, so the
lineage edge, the receipt with its input, the settlement hold, the stop
over message turns and the close all apply unchanged, and team_message
stays disabled. On top of that the lead addresses members by name and
reaches every member with "*"; a member is its continuation chain, so
the refusal #13846 added until this slice is lifted and a finished
member takes more work and stays a member; every run of a member's
chain opens with a fixed preamble naming the member, its team and how
its result reaches the lead; and the lead sees a member's messages and
continued results labeled with its name. task_update stays a board
record: delivering from it would rerun work a finished member did, or
cost a running member the report of its current work, and a replay
could not tell whether it owed a delivery. Member-side commands (board
tools, member-to-member messages) move to H4e-b3 with plan approval and
shutdown, on one member-to-lead command channel. No record, rule,
route, migration or Java change.

The H4e-a design records the answers to its open questions 1, 2 and 4
and notes that task_assignment is not produced; the H4e-b1 design points
to the re-scope, the lifted refusal and the answered open question 3;
the H4d-b design's routing exception and H4e row point here.

* docs(managed-agent): give broadcast ids their own namespace and cover a member that completes before its owner update

Address the review on #13879.

- A digest put in the call-id slot could equal a plain call's provider
  id, which would mint the same message and run ids. Broadcast run ids
  are now bcast_ plus a digest with no colon, while every single-send or
  launch run id is promptKey:callId; broadcast message ids start with
  msgb_, while single sends start with msg_. The two id sets cannot
  meet whatever call ids the provider hands out. A recipient's
  continuation or message names the broadcast call's key as its
  executionCallId.
- One batch orders the send before task_update but cannot keep the
  member running: the relay settles a child while the lead's Turn runs.
  The owner rule now accepts, on an open task, a member given work in
  this Turn (one prompt id, all its model rounds and its resumption): an
  earlier call of the Turn committed a continuation of its chain that
  did not prove it never started, or a message to its head that was not
  cancelled or rejected. A head that failed, was cancelled after it
  started, is stopping or completed after a stop request is still
  refused. Refusals never claim which task a past send carried and never
  offer to skip a send for work that never arrived: a completed member
  gets one conditional text, and a never-started continuation its own.
2026-10-11 04:23:19 +00:00
jinyeandQwen-Coder d534f90d32 feat(core): Keep complete file reads on one opened source (#13776)
* feat(core): keep complete file reads on one opened source

Extract the ordinary file-source and PDF cancellation lifetime from #13526.

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

* fix(core): admit special and zero-length files before opening them

Opening every inline read before classification could touch a tty, fifo, or device, and a reported size of 0 made procfs reads come back empty. Classify first and keep those paths on the pathname source.

* fix(core): annotate admission-test predicates for tsc

The const case rows made isFile's return type circular, so the package build failed before tests ran.

* fix(core): keep successful PDF output and prior read rights

A timer that fires before a child's close was reported as a timeout, descriptor cleanup could replace a finished render, and a failed re-read erased an earlier full read from the cache.

* test(core): skip the descriptor render cleanup case off Linux and Darwin

pdfDescriptorPath throws before the test's try on every other platform, so the Windows nightly suite would fail a case that only the descriptor path can exercise.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-11 04:17:26 +00:00
jinyeandQwen-Coder d35277c2f3 fix(runtime): retain CSI mount identity through directory resolution (#13772)
* fix(runtime): retain CSI mount identity through directory resolution

Retain the original root descriptor across CSI observation and normalized
directory traversal, join admitted work before close, and clean up the mount
on worker startup failure and shutdown. Preserve failed acquisition cleanup
as an observable error.

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

* codex: address PR review feedback (#13772)

* test(runtime): pin the in-walk descriptor check against replace-and-restore

Cover the interleaving wenshao's round-2 verification left unpinned (F3,
MU4): a child replaced between the walk's lstat and open, then restored
before the post-walk lstat. The descriptor-vs-lstat comparison right
after open(2) is the only guard that refuses it; this test goes red when
that guard is deleted.

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

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

* fix(runtime): re-validate the mount root before excusing an unlocatable sibling

Round-2 review follow-up. The private-estate exemption now re-reads
rootDirectory() at the moment an unlocatable sibling would be excused:
a mount that fenced between the first root read and the sibling's
resolution no longer flips the cross-session veto from deny to allow,
for CSI fences and for ordinary-mount root replacement alike (R2-1).

Tests: the round-1 witness now pins the refusal reason and adds a
positive control so a wholesale execute-path failure cannot satisfy it
(R2-2); a new in-loop case fences the mount inside the sibling resolve
and goes red if the freshness conjunct is removed; a new cross-device
case keeps the named stats real so only the descriptor-device arm can
decide, going red when that arm is deleted (R2-3).

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-11 04:03:10 +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
ca08a3d152 feat(managed-agent): Collect retired stream-capture tool outputs (#13534) (#13554)
* feat(managed-agent): Collect retired stream-capture tool outputs (#13534)

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

* fix(managed-agent): Address stream-capture collection review findings (#13534)

- Drop the page-exit exhaustion probe: eligibility is fetched with
  LIMIT PAGE_ROWS + 1 and the overflow row answers "more pages" directly,
  halving ELIGIBLE executions per page.
- Log a blocked ledger once per gc_blocker transition (previously only
  persisted, never surfaced); the design doc names the per-claim log line
  as the P1 observation surface.
- WorkspaceRecoveryReader answers resource_collected for inline rows whose
  bytes the O4-2 publication collector freed while leaving state REFERENCED,
  matching the README and design-doc contract; those rows misreported
  resource_corrupt before.
- Pin the claim scan's gc_next_at >= 0 predicate with a comment (measured
  0.013 ms vs 1,779 ms at 1M completed ledgers).
- Correct the design and lifecycle docs (EN/zh): the ledger-creation scan
  runs at a 60-second cadence with a 32-candidate oldest-first admission
  bound, the shared single-thread output scheduler now hosts two collectors
  plus the observer, and gc-enabled gates both collectors.
- README and operations docs name the tool-publication.enabled precondition
  and note that freed bytes stay inside the InnoDB tablespace until an
  explicit OPTIMIZE TABLE or rebuild.

* fix(managed-agent): Skip already-freed rows in stream-capture accounting (#13534)

The ELIGIBLE predicate never required the bytes to still be present,
while the ledger adds byte_length, which is metadata that outlives the
bytes. A row already freed by another writer without a state change
would be selected again and counted a second time on the permanent
audit ledger (the sandbox verifier measured 1500 counted vs 500 freed).
Add the one-predicate guard, pin it with
alreadyFreedRowsAreNotCountedAgain, and sync the design doc's
eligibility enumeration in both languages.

* fix(managed-agent): renumber stream capture migration to V51 and tighten collection verdicts (#13554)

- Rename V48__managed_stream_capture_collection.sql to V51: main took
  V48-V50 via W1c, and the merged tree booted Flyway with two V48
  migrations. All prose references (both doc languages, operations docs,
  README) follow the new number.
- Gate the ledger-scan cadence on an injectable monotonic clock
  (System::nanoTime) so a wall-clock step back cannot silently stall
  ledger creation, and pin the re-scan with a one-instance cadence test.
- Answer resource_collected from positive evidence of collection (row
  COLLECTED, or a cataloged publication object COLLECTED in scope) and
  leave unexplained byte loss on the fail-closed resource_corrupt path.
- Correct the design doc baselines in both languages: O4-2 already frees
  cataloged REFERENCED bytes; the sealed manifest is journal-referenced
  and pinned permanently; the never-cleared guarantee covers layout
  invariants only; collected_bytes sums byte_length metadata.

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

* fix(managed-agent): renumber stream-capture migration to V53 and pin the collected-state probe (#13554)

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

* chore(sdk-java): clear generated javadoc residue failing the lint gate (#13554)

The deterministic gate's npm run lint scanned untracked, gitignored Maven javadoc output under packages/sdk-java/qwencode/target/reports/apidocs (generated by a package-phase build in the shared checkout) and failed with 589 no-undef errors. The repair is the working-tree cleanup of that regenerable residue; this commit carries no tracked content change and preserves the rejected V53/probe commit unchanged.

* docs(managed-agent): name both pre-V53 verdicts for collected recovery reads (#13554)

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

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

* fix(managed-agent): block stream-capture collection during in-flight recovery (#13554)

- Add the recovery_active blocker: a CAPTURING or VERIFYING workspace
  recovery reads a retired Session's bytes through WorkspaceRecoveryReader
  without a read lease, and no lease can exist for a tombstoned Session,
  so the collector now joins managed_workspace_recovery_session to live
  operation state before collecting; it reschedules on the 60-second
  class like reader_active.
- Move the phantom-row guard off the page query: referencing inline_bytes
  in WHERE makes InnoDB materialize every candidate's BLOB (up to 101
  rows per page) just to test it for NULL. The page UPDATE keeps
  AND inline_bytes IS NOT NULL and collected_bytes now sums only the rows
  the UPDATE actually moved (maintainer-verified candidate patch).
- Pin the resource_collected positive-evidence probe's scope_key and
  resource_id predicates with a test (maintainer's candidate test; kills
  the surviving N08/N14 mutants).
- Docs (EN/zh): eligibility, the blocker list and the acceptance criteria
  name recovery_active; the operations doc notes that under MySQL's
  default binlog_row_image=FULL each collection UPDATE also copies every
  freed BLOB into the binlog.

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

* fix(managed-agent): close stream-capture claim→page recovery race, renumber ledger migration to V56 (#13554)

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

* fix(managed-agent): terminate captures on collected publication bytes, renumber ledger migration to V57 (#13554)

A collected publication object surfaced as publication_object_unverified,
which is in neither invalidating set, so the capture wedged in CAPTURING
and its recovery_active blocker pinned the retired Session's collection
forever; map the COLLECTED marker to the terminal resource_collected at
the read. Rename the ledger migration V56 -> V57 after main landed
V56__managed_operation_actor_key, and pin the collector's transactions to
REPEATABLE READ so the page-time recovery fence keeps its gap locks on
servers defaulting to READ COMMITTED, with InnoDB witnesses on the
mysql-integration lane plus an H2-runnable assertion of the pin.

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

* test(managed-agent): drive a genuinely ACTIVE head in liveHeadBlocksCollection (#13554)

The fixture's UPDATE rewrote SEALED over the insert's own SEALED, so the
test never exercised the ACTIVE head its name promises, and a future
narrowing of the session_head_live predicate to only ACTIVE would have
shipped green. Maintainer verification round 1, suggestion 2.

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

* test(managed-agent): serve the stream-capture MySQL suite from one schema per class (#13554)

The MariaDB integration job (timeout-minutes: 15) was killed at 15m17s on
the merged head. The suite this IT inherits runs ~52 tests, and each test
created its own database and re-ran all 57 Flyway migrations, so the
bootstrap — not the assertions — dominated the job's budget once main's
child-workspace tests joined the same run.

Create one schema per class, keep every test and assertion on real InnoDB,
and isolate tests by truncating every table but flyway_schema_history after
each test instead. The FK toggle and the truncates share one connection
because FOREIGN_KEY_CHECKS is session-scoped and the data source hands out
a fresh connection per call.

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

* fix(managed-agent): renumber stream-capture migration to V65 (#13534)

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code <[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-autofix <qwen-code-autofix@localhost>
2026-10-11 03:11:35 +00:00
Shaojin Wen 66ddb151cb feat(managed-agent): enable the H4e-b1 team domains after the physical pass (#13846)
* feat(managed-agent): enable team_state and team_task after the H4e-b1 physical pass

* fix(managed-agent): let a finished member own the task the lead completes

* docs(managed-agent): record the H4e-b1 physical pass and the enabled team tools

* docs(managed-agent): note the close wait, the Shell-profile reach and the pure control arm

* test(managed-agent): drop the team gate lifts that the enablement makes moot

* fix(managed-agent): keep a finished owner off open tasks and settle the round-one review notes

* fix(managed-agent): name the kept owner when a reopen is refused and settle the round-two review notes

* docs(managed-agent): point open question 5 at its tracking issue

* docs(managed-agent): note isolated team members and the landed message runtime

#13841 lets a named member run with isolation "worktree" and #13822
landed H4d-b. Record what that changes for the lead's team: the
isolation argument on the team's Agent tool, the prepare and merge
steps of an isolated member's relay, its run counting as running until
the merge records an outcome (so team_delete stays refused for that
unbounded wait), its completed state meaning settled rather than
merged, and that the physical pass did not cover isolated members. Fix
the step team_delete waits for (the settle, step 5).

* fix(managed-agent): refuse to continue a finished team member and show member task ids

#13822 lets send_message continue a finished child as a new run. For a
team member that run would sit off the roster: task_list would keep
showing the member completed, team_delete would pass while it runs, and
its result would arrive without the teammate label. Until H4e-b2 lets
the roster follow a continuation, sendToChild refuses to continue a
member's run by name; a running member still takes messages.

The task_list roster now ends each member line with the task id that
send_message takes, and a failed or cancelled run with its stop reason,
so a member the host never started (for example an isolated member
whose Workspace was not prepared) reads apart from one that failed its
work. With a team, send_message's refusal of a name points at those task
ids instead of the legacy tool, and the board answers mention
send_message for a running member.

Add the isolated-member test #13841 left out (both labels and the
settle team_delete waits for), and record in the design and the E2E
plan that the physical pass predates #13841 and #13822, that H4f's
public cancel has landed, and the other statements the two merges made
stale.

* fix(managed-agent): let a former member continue once its team is deleted

The continuation refusal read every roster, deleted teams included, so a
former member stayed refused forever, and its advice to launch a new
member pointed at a team that no longer existed. Once a team is deleted
nothing reads its roster, so a former member now continues as a plain
child with its result unlabeled; the refusal applies only to members of
an open team and says so.

Pin that a plain child beside a team still continues, and that a former
member continues after team_delete. Note the exception in H4d-b's
routing decision, say in task_list's description that a member's task
id addresses it while it runs, and tidy two test texts.

* fix(managed-agent): share the open-team check and show task ids only for running members

Review follow-ups on the team enablement:

- One open-team predicate (isOpenTeam: active, or closing until deleted)
  for openTeam() and for send_message's refusal to continue a finished
  member, through a new hostedOpenTeamMembership. hostedTeamMembership
  keeps its unfiltered answer for the label, replay and recovery readers,
  so a result accepted after its team was deleted keeps its label.
- managedChildTaskId in core: every Hosted CLI derivation of a child task
  id (launch answers, taskIdOf, the notification, the roster) calls it,
  and the cross-language fixture pins it.
- task_list's roster shows the task id only for a running member, the one
  send_message can reach. The continuation refusal says when a new member
  can be launched and what deleting the team costs. The board notices and
  the by-name refusal point to the id task_list shows beside the member's
  name, and send_message beside the team tools states the member rule.
- Docs: the H4e rows of the H4a, H4b and H4d designs; the open-team
  exception wherever H4d-b states its routing rule; decision 3 no longer
  says nothing reads a deleted team's roster.
2026-10-11 02:59:34 +00:00
qqqysandqqqys 9763580b84 feat(mods): run experimental headless Mod commands (#13843)
* feat(mods): run experimental headless Mod commands

* fix(mods): preserve marketplace hooks and packaging fixtures

* fix(mods): skip ignored Claude hook paths

---------

Co-authored-by: qqqys <[email protected]>
2026-10-10 16:52:23 +00:00