457 Commits
Author SHA1 Message Date
Bob.qwencodeandgithub-actions[bot] 208beb2b3b chore(release): v0.26.9 [skip ci]
* chore(release): v0.26.9

* docs(changelog): sync for v0.26.9

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 18:03:10 +00:00
Bob.qwencodeandgithub-actions[bot] 3718b57fa8 chore(release): v0.26.8 (#16409)
* chore(release): v0.26.8

* docs(changelog): sync for v0.26.8

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 01:02:08 +08:00
qqqys 08f1843c22 feat(mods): run the prompts a Mod submits in the Ink terminal (#16373)
* feat(mods): run the prompts a Mod submits in the Ink terminal

* test(mods): run a Mod's prompt in a real Ink terminal

* fix(mods): stop a Mod's prompts when the person stops a turn in Ink

- Esc or declining every tool a turn asks to run drops the Mods' queued
  prompts and refuses new ones until the person sends a prompt again. The
  stopped turn's turn.complete hooks run after the stop, and a call a hook
  does not await reaches the host after the hook returns, so dropping the
  queue alone let such a hook start the next turn. $.turn.abort still keeps
  the queue.
- A Monitor update waiting out its minimum interval no longer holds a Mod's
  prompt, which a Monitor printing within every interval held for good.
- A prompt put back after losing the idle moment runs once the winning
  submission settles, since that one may start no turn.
- The dual-output sidecar gets the Mod's text and a mod_prompt system
  message, as a stream-json client does, instead of the frame.
- The guide's capability table lists Ink for a Mod's turn events.

* fix(mods): drain Mod prompts after any settle and strip a stopped one

A Mod prompt queued while a submission that starts no turn held the
session (a cron's Mod command, a notification dropped before sending)
stayed queued with the session idle: the drain re-ran on settle only for
a prompt it had put back. Re-run it whenever a Mod prompt waits.

When the person stops a Mod prompt's turn before it answers, the framed
prompt stayed at the end of the model history, so the next prompt reached
the model behind it. The cancel info now says the turn ran a Mod's
prompt, and the handler strips it as it does a Goal continuation's.

* fix(mods): run a waiting Mod prompt once a Goal lets go of the turns

A Mod prompt queued while a Goal owned the session's turns waited for the
next submission to settle or another prompt to be queued. The Goal often
releases its turns after the session is already idle (its verifier accepts,
its last turn is released), so the prompt could sit until some unrelated
event. The drain now also re-runs on the Goal runtime's broadcasts while a
Mod prompt waits.

The test no longer queues a second prompt to wake the drain; it changes the
Goal snapshot and broadcasts, and fails without the fix.
2026-11-05 20:39:00 +08:00
Jiankai XiaoandJiankai Xiao de6ccb4e3c feat(serve): allow configuring the Conversations working-directory root (#16105)
* feat(serve): allow configuring the Conversations working-directory root

* test(serve): register Conversations root E2E in no-AK CI

* fix(serve): address Conversations root review feedback

* fix(serve): clarify Conversations root configuration errors

---------

Co-authored-by: Jiankai Xiao <[email protected]>
2026-11-05 16:53:05 +08:00
qqqys a818643533 feat(mods): let a Mod submit a prompt of its own in headless runs (#16302)
* feat(mods): let a Mod submit a prompt of its own in headless runs

$.prompt.submit({ text, asUser? }) raises prompt.submit through every hook
but the caller's, with the Mod as its origin, and queues what arrives
beneath them. The headless run takes it once idle and runs it as a turn of
its own: UserPromptSubmit hears it, turn.start and turn.complete fire, and
the transcript records it with the provenance mod_prompt and the Mod's
name. Unless the Mod passes asUser, the model reads the text inside a block
that names the Mod and says the person did not type it.

The call resolves once its prompt is queued, as next(e) does for a typed
prompt. The Mods' prompts start at most 8 turns before the person sends a
prompt. The terminals do not run these prompts yet: there the method is not
listed and a call resolves to a drop.

* fix(mods): queue a Mod's prompt once, outside its caller's lifetime

- Run the hooks a `$.prompt.submit` raises outside the calling hook, so a
  call the hook does not await still queues after the hook returns.
- Queue what last arrived beneath the hooks once they have all answered,
  so a hook that calls `next(e)` twice queues one prompt.
- Drop an empty or whitespace-only text, from the call or from a hook.
- Discard Mod prompts that have not started when a headless run ends, so a
  stream-json session does not run them on the client's next message.
- Escape every `<` and `>` in the framed text.
- Recall memory for the Mod's text rather than the frame, keep a Mod's
  prompt out of the session title (in its turn's place) and out of the
  commit attribution's prompt count.

* fix(mods): keep the title, SDK output and turn count right for Mod prompts

- Mark a Mod's prompt as `null` in the title projection, not `''`, so it
  no longer switches a session whose prompts recorded no display text to
  an empty projection, which stopped auto-title and `/rename --auto`.
- Tell a stream-json client of a Mod's prompt: its text as a user message,
  then a `mod_prompt` system message naming the Mod, before the turn runs.
- Reset the Mods' turn count for the person's slash commands too.
- End a Mod turn that a duplicate provider call stops as an error on the
  shared loop, as on the legacy loop and for the main turn.
- Refuse a prompt submitted while /clear or /resume ends the session, and
  start the new session with nothing queued and the count at 0.

* fix(mods): keep charging a hook while the other Mods hear its submit

A $.prompt.submit call outlives its caller, yet it lent the caller's
budget for the whole submit chain, so a hook that did not await it ran
unbudgeted until the other Mods' prompt.submit hooks answered. It now
runs as the void notices do, charging the caller as it goes.

The frame also no longer says the Mod sent its prompt while the session
was idle: most are sent mid-turn and wait for the session to be idle.

* fix(mods): run a Mod command's submitted prompt and keep it out of the title

A Mod command in a headless run always answers with a message, and that
branch returned before the drain, so a prompt the command submitted with
$.prompt.submit was discarded while the call had resolved as queued. The
reply is now shown without a turn of its own and the run goes on to the
drain, ending with one result.

A session whose prompts recorded no display text drew its title from the
conversation, which held the Mod's framed prompt. The recorder now keeps
the text the model read for each Mod prompt, and the conversation-based
title leaves those messages out.

* fix(mods): keep a Mod's prompt within its payload and its command's reply in view

- Drop a submitted prompt past half a Mod payload, from the call or a
  hook's rewrite, before it is queued, so a queued prompt is never
  reported as failed; an input too large for the bridge now rejects.
- Print a Mod command's reply in text output, and give the worktree notice
  the command would have carried to the first request the run sends.
- Leave an `asUser` Mod prompt out of the title keys, which matched a
  prompt the person typed alike, and keep the keys least recently used.
- Make Goal evidence leave a Mod's prompt out explicitly.
- Pin the documented turn limit and the tool-result guard in tests, and
  add a built-CLI test of `$.prompt.submit` against the fake server.
- Docs: six steps, the non-goal narrowed, the hook docs and the budget
  paragraph naming Mod prompts.

* test(mods): run the $.prompt.submit integration test in the no-AK job
2026-11-05 14:42:41 +08:00
qqqys a85370b38f feat(cli): run the Ink TUI's model rounds on the shared loop's round (#16206)
* feat(cli): run the Ink TUI's model rounds on the shared loop's round

With experimental.unifiedAgentLoop on, every model round of the Ink TUI
goes through the shared loop's runModelRound, which collects the tool
calls, drops those of an attempt a retry or fallback replaced, and turns
the client's terminal events into a stop reason. The TUI keeps its
stream processor and its tool step. Adds the Ink parity suite.

Part of #15327.

* test(cli): tighten the Ink parity suite and fix the no-AK script pin

- add the suite to the no-AK script pin
- check each run's request count, and through the debug log that only the
  run with the setting on sends its rounds on the shared loop
- compare the system prompt, the tools offered and recorded usage
- run each scenario once, without the root config's retries, with a
  source pin
- describe the suite as it is in the design doc
- say in the setting's description that the Ink TUI sends its model
  requests through the shared loop
2026-11-04 07:36:48 +00:00
Bob.qwencodeandgithub-actions[bot] 7d5a82443a chore(release): v0.26.7 [skip ci]
* chore(release): v0.26.7

* docs(changelog): sync for v0.26.7

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-04 04:49:30 +00:00
jinye b1cd4ac0c6 refactor(managed-agent): align shared MySQL schema conventions (#16143)
* docs(managed-agent): document database tables and columns

* refactor(managed-agent): align shared MySQL schema conventions
2026-11-03 12:00:29 +00:00
Bob.qwencodeandgithub-actions[bot] 5e267805c3 chore(release): v0.26.6 (#16163)
* chore(release): v0.26.6

* docs(changelog): sync for v0.26.6

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 07:00:07 +08:00
e26f377a28 chore(release): v0.26.5 (#16035)
* chore(release): v0.26.5

* docs(changelog): sync for v0.26.5

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-01 12:40:25 +08:00
Shaojin Wen bc8ad53f95 test(managed-agent): gate the B3 round-and-cost-budget crash window (#15981)
* test(managed-agent): gate the B3 round-and-cost-budget crash window

Matrix row B3 ("after earlier tools") had focused/integration evidence
only: nothing crashed a replacement after several tool rounds and proved
the numeric round counter and the turn cost ledger survive it. A recovery
that restarts the round at 0 or zeroes the token ledger would pass CI.

Package the W2 window in hosted-failover-e2e: a multi-round fake fixture
runs two earlier tools (each its own approval, execution and file effect),
the replacement is crashed while the later post-tool request is held on
the fake-model side (no new serve hook), and the next generation must,
from MySQL alone after scrashing the crashed disk, resume the saved
logical round, restore the TurnBudget snapshot and per-model token ledger
(asserted as per-model output-token totals with non-vacuity guards, since
an unrestored ledger is a truthy {}), reissue the saved request deep-equal,
retract the published prefix, and project one terminal. "Cost budget" here
is the snapshot plus the ledger, not a cumulative spend gate.

Real-stack (.75) evidence: head green twice (round 2->2, budget 500000
deep-equal, ledger 600->700 = restored 600 + final round 100, parity equal,
both earlier Actions decided under their original IDs); three named
mutation kills (round reset, ledger wipe, snapshot re-baselining). The
execution-count assertion is defense-in-depth in this window: three
mutation cells survived because exactly-once rides three independent
guards (replay routing, continuation settled-batch adoption, Runtime
runtimeCallId idempotency), documented in the design.

No production behavior change; failover ceiling 26 -> 27 arms (285 min).

* test(managed-agent): harden the B3 arm per review (R1-1..R1-4)

R1-1: the W2 rationale claimed the saved request carries the budget; it
carries only the round (budget lives on the attempt's route/usage
resources). Corrected in both design languages.

R1-2: the behavioural fixture could not distinguish a newest-first from
an oldest-first budget/models scan, and the ledger assertion had only
text pins. Add a discriminating fixture (older attempt with a different
budget and smaller ledger) and lift the cost block into a top-level
assertB3CostContinuity helper with injected-state behavioural cases:
healthy growth passes; null-budget snapshot, missing snapshot,
re-baselined snapshot, empty before-ledger, zeroed after-ledger and
shrunk per-model totals all throw.

R1-3: the budget non-vacuity guard tested the snapshot object, but a
non-null snapshot with budget: null is a legal production state (lost
+Nk directive), so null == null passed vacuously. Guard the numeric
field instead.

R1-4: site-anchor the B3 post-tool request count (? 3) like the
neighbouring R1-7/F1 pins so a conflict resolution back to the pre-PR
ternary reds at unit speed, not only in the real-stack lane.

Each fix has a demonstrated witness: oldest-first scan, deleted ? 3
clause, object-truthy guard and candidates->prompt repath each red the
new cases/pins; all restored. Contract suites 132/132, typecheck green,
and the .75 head arm re-run with the hardened runner is green (round
2->2, budget 500000 deep-equal, ledger 600->700).
2026-10-31 05:36:12 +00:00
Shaojin Wen b3e9ce0cd1 test(managed-agent): gate the A3 mixed-sibling approval crash window (#15945)
* test(managed-agent): gate the A3 mixed-sibling approval crash window

Package the native half of recovery-matrix row A3 (#14481) as a
hosted-failover-e2e arm: crash a replacement inside a two-call approval
batch (sibling[0] decided, sibling[1] still requested) and prove the
surviving siblings recover from MySQL alone — earlier decisions persist,
the pending sibling keeps its original Action/deadline and earns its own
Allow, and both calls settle once each in their original order.

The rewritten-input / Hook-no-rerun half stays a commandStep follow-up
because a native continuation refuses Hook/MCP sessions.

* chore: re-trigger CI after the main merge

The web 'Update branch' merge incorporated main's .prettierignore (lint gate
freshness) but did not trigger a CI run on the updated head; this empty commit
re-triggers the lanes on the merged tree.

* test(managed-agent): harden the A3 arm per review (R1-1..R1-7)

Address the seven inline review suggestions on the mixed-sibling approval arm:
read the decided sibling and require it still decided with its original deadline
before answering the pending one (R1-4); gate execution settle order on settled_at
along the plan (R1-5); add an ascending-link behavioural case so a first-match
plan-link selection reds (R1-6); anchor the execution-count pin and pin the
per-call identity loop (R1-7). Correct the design docs in both languages: the
fixture is two write_file calls, the validation plan lists the reds actually
produced, and the arm proves matrix clauses 1-2 plus a narrower pending-sibling
observation, not the rewritten-input clause 3 (R1-1/R1-2/R1-3).
2026-10-31 10:25:56 +08:00
Shaojin Wen 0c1b42fa31 test(managed-agent): gate the B3a finished-before-commit crash window (#15881)
* test(managed-agent): gate the B3a finished-before-commit crash window

Package the recovery-matrix B3a row: crash the replacement Harness after its post-tool main stream records the durable Finished marker but before the assistant output commits. A test-gated hook (QWEN_E2E_CRASH_BEFORE_ASSISTANT_COMMIT, set only on the crashing generation, never in production) holds the turn in that window; the runner asserts the barrier landed, kills the generation, and requires the next one to reissue the saved request deep-equal and project one terminal with the recovered answer rather than compensate an already-committed output. Adds the failover arm, CI step and ceiling (25 arms), contract pins, the serve env-guard allowance, and bilingual design/README updates.

* test(managed-agent): harden the B3a arm barrier, hold and contract pins

Address review R1-1..R1-4: assert the durable crash precondition (the approved execution SETTLED) from MySQL before the SIGKILL like the sibling crash arms, instead of trusting the hook log alone; wire the test-gated hold to the Turn abort signal so a leaked flag cannot park a real Session uncancellably; pin the crashing/recovery generation call sites and the parameter default rather than bare identifiers; and pin the combined deep-equal parity gate so reverting it to after-result-only reds the fast-tier contract suite.

* test(managed-agent): assert the B3a crash window from the durable journal

The finished-before-commit barrier proved only that the approved execution
settled (executionAtCrash=SETTLED) and that the hook printed its log line. It
did not read the post-tool main model attempt's durable Finished marker, so a
hold moved above the model layer's usage-only output_committed commit could
crash with no durable Finished marker and still pass every downstream check
(the recovery reader also accepts a saved request whose attempt is started).

Before the SIGKILL, read the same journal the recovery reads
(ma_session_journal_tx) and assert findHostedCommittedModelOutput's own
discriminator: the latest <promptId>:main: model.attempt is output_committed
and no assistant message.committed follows it. Retain the attempt identity and
sequence in approval-crash-barrier.json. Add a window-shaped fast-tier contract
pin for the new assertion, keeping the existing pins.

Verified on real MySQL/Spring/Broker: head green (attempt retained, parity
equal); moving the hold above output_committed reds at the new pre-crash
assertion; removing the hook still reds at the barrier timeout; moving the hook
after the assistant commit now reds at the new post-commit assertion.

* test(cli): cover the finished-before-commit hold's abort release

The QWEN_E2E_CRASH_BEFORE_ASSISTANT_COMMIT hold's abort wiring had no test that reds without it: the env-guard only counts AST reads, the meta-tests grep source text, and the packaged arm SIGKILLs the pending generation, so no lane exercised the release path. Arm the module-scope switch via a vi.hoisted env stub set before the static import under a dedicated Session id, then drive a real turn into the hold and assert the abort settles it cancelled with no assistant message.committed; a non-matching-id case pins that the hold never fires. Removing the release/addEventListener wiring makes the matching case time out. Test-only; the single env read and the hold's park-until-killed behaviour are unchanged.

* test(managed-agent): pin the B3a hold id-guard and window arming

R4-1: run the non-matching finished-before-commit hold case before the
abort case so the module-scope one-way latch is still unset, letting the
session-id comparison (not a spent latch) suppress the hold. Deleting the
id conjunct now reds the non-matching case.

R4-2: pin the finishedBeforeCommitWindow arming ternary condition, so
narrowing its guard can no longer silently delete the arm's only
durable-window check while the scripts suite stays green.

* test(managed-agent): exercise the B3a window helper on injected journal rows

Address the round-5 deferred suggestion: finishedBeforeCommitWindow had only source-spelling pins. Add a behavioural test that injects runMysql through the existing load() machinery and asserts the latest-attempt selection and both throw branches on journal rows (the output_committed window holds; a started or newer-started latest attempt reds before the Finished marker; a following assistant commit reds after it; role/prompt/sequence filters hold). Witness: mutating the latest-attempt index to mainAttempts[0] — a change the source pins survive — reds the behavioural test.
2026-10-30 18:15:45 +00:00
Shaojin Wen d4e8d9ffde test(managed-agent): gate the A4 takeover-load reply-loss handoff (#15857)
* test(managed-agent): gate the A4 takeover-load reply-loss handoff

The A4 load half was recorded as a recovery gap (a lost takeover-load reply
leaving the original Turn stuck RUNNING). On the current tree that variant does
not reproduce: when the replacement loses the reply to its first drive load and
the control plane redrives the same load into the now-active driver, the redrive
restates the active attachment and the original Turn projects exactly one
terminal with the recovered answer. Two clean real-stack runs (Linux aarch64,
MySQL 8.0.46, JDK 21) recover correctly with the fault injected.

Rather than leave the row focused, package it as a regression gate so the
handoff cannot silently regress:

- A test-gated Harness hook (QWEN_E2E_DROP_LOAD_REPLY=<sessionId>, set only on
  the replacement by the arm, never in production) destroys the first load
  attach reply once, after the driver started.
- The arm asserts the reply was actually lost (via the replacement's captured
  log) before trusting the recovery, then requires one terminal, the recovered
  answer, one settled execution and one file effect.
- Refusing the redrive's active-branch restatement makes the arm fail at the
  adoption barrier, proving the gate is load-bearing.

The failover job grows to 24 arms (ceiling 255). The bilingual Step 3 design and
the server README flip the A4 load row from a focused gap to packaged and note
that the earlier stuck-RUNNING variant did not reproduce on the current tree.

* test(cli): document the A4 load-reply fault flag in the serve env guard

The packaged load-reply-lost arm reads QWEN_E2E_DROP_LOAD_REPLY once at session-bridge construction in hosted-harness-session.ts. The serve process.env guard requires every access to be a documented process-scoped exception, so register the test-gated flag with its reason. Fixes the Qwen Code CI env-guard red on the merged head.
2026-10-29 15:26:20 +00:00
Bob.qwencodeandgithub-actions[bot] ae51b081ce chore(release): v0.26.4 [skip ci]
* chore(release): v0.26.4

* docs(changelog): sync for v0.26.4

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-29 15:14:52 +00:00
Shaojin Wen 702c1773a1 fix(managed-agent): retry owed Hosted cleanup after its generation dies (#15824)
* test(managed-agent): gate G3 cleanup and terminal recovery faults

Add three packaged fault arms to hosted-failover-e2e: a refused Runtime cleanup release followed by another crash, a committed terminal whose reply is lost, and a refused terminal write over a payable checkpoint. Each keeps the original Action, execution, file effect and terminal identity, and the runner now witnesses the fault on the store or Broker boundary rather than on a client-side retry.

Part of #14481.

* test(managed-agent): harden the G3 cleanup and terminal fault arms

Drop every bounded terminal retry so the terminal reply-loss crash lands on a committed-but-undelivered terminal, and require the surviving public transcript to carry the recovered answer. Document both observations. Part of #14481.

* test(managed-agent): address the G3 terminal arm review findings

Arm C1's crash barrier only on a confirmed store 200, record that pre-crash count, and state that the replacement projects the committed terminal without a second store commit. Snapshot fault counts before the relief Turn, keep the result-commit diagnostic for after-result, and fail loudly on an unhandled crash barrier. Remove the cleanup arm: a delayed replacement reproduces a control-plane cleanup/adoption gap, so C3 stays open as a product follow-up.

Part of #14481.

* fix(managed-agent): retry owed Hosted cleanup after its generation dies

A Turn that completed with cleanup still owed had no driver once its Harness
generation died: no Turn was left to dispatch, and the operator recheck route
was the only path that could pay the durable `HOSTED_CLEANUP` projection.
A delayed replacement therefore never received a load, kept the original
Runtime binding family retained, and the packaged C3 arm passed or failed
depending on whether the terminal projection beat the crash.

The control plane now scans that projection. For each Session with no active
Turn it resolves the owed Turn from the cleanup descriptor and issues the
existing prompt-scoped cleanup-only load, which pays the original Runtime
identity and commits `confirmed`. Attempts back off in memory from 10 seconds
to 5 minutes, one debt per Session per scan tick, and a failed attempt drops
the connector's cached attachment so the retry performs its own cold load.
Unreadable or foreign descriptors are logged once and stay owed.

The C3 arm is packaged again: it refuses every release until the crash, waits
for the control plane to project the terminal before killing the generation,
and requires the replacement's own release, one `confirmed` event and no
remaining debt. Disabling the scan makes it time out at the retry barrier.

Design: docs/design/2026-10-28-hosted-cleanup-debt-retry.md

* fix(managed-agent): qualify the cleanup retry executor and clock

Main now publishes the shared executor and clock as named beans, and every
coordinator injects them by qualifier. Without the qualifiers the new
coordinator fails context startup, which breaks every Hosted Spring test and
the packaged server.

* fix(managed-agent): keep a live attachment when a cleanup retry is refused

The debt scan dropped the connector's cached attachment on every failure. A
named refusal (409/503: a Turn still owns the Session, or recovery is required)
is not evidence that the cached generation is dead, and evicting it churns a
Session whose Turn is still running — the scan can race a Turn admitted between
its query and its attempt.

Only transport loss, a moved generation, a vanished reference (404) and the
connector's own generation-moved signals now evict; a named refusal just backs
off. Two tests pin both sides, and the design and README state the rule.

* fix(managed-agent): address the cleanup debt scan review findings

Two P2 findings from the review of the cleanup debt retry:

The store injected an unqualified JdbcTemplate. In an embedded deployment the
managed template is defaultCandidate=false, so the scan bound the host's
business database and failed every tick with "ma_resource_ref not found",
never retrying the dedicated database's debts. Qualify the injection with
managedJdbcTemplate; a new embedding regression test fails with the exact
table-not-found error when the qualifier is removed.

The scan always read the oldest fixed page and validated descriptors after the
LIMIT, so a prefix of unclearable debts (unsupported profile, unreadable
descriptor) starved every recoverable debt behind it across all Sessions and
tenants. Make the scan keyset-paginated: the cursor advances over every raw row
it reads, including unreadable ones, and wraps when a page comes up short.
Pacing entries now expire by idle time rather than by page membership, so
rotation never resets a still-owed debt's backoff. New tests pin the cursor
advancing past an unreadable prefix and wrapping on a short page; reverting the
cursor to advance only over valid rows reproduces the starvation.

The packaged C3 arm and the terminal reply-loss regression stay green on the
real MySQL/Spring/Broker stack after both changes.

* fix(managed-agent): address the second cleanup debt review round

- Name the scheduler. retryOwedCleanup was the module's only @Scheduled tick
  without scheduler = "managedAgentScheduler"; managedAgentScheduler is
  defaultCandidate=false, so the bare tick bound the shared one-thread
  sessionWorktreeScheduler that a blocking Git scan already owns. Align it with
  its siblings and pin its 1s fallback in the (now four-site) scan-delay test.
- Register HostedCleanupDebtStore in the row-lock-order audit. Its read-only
  NOT EXISTS guard on ma_turn made it a turn reader, so the exact-set sibling
  boundary test (SessionRowLockOrderTest) failed on SDK Java (fast). It takes no
  row lock and never writes, so it is a turn reader but not a session locker;
  list it explicitly rather than relax the audit.
- Classify attachment staleness by an explicit whitelist mirroring
  HarnessPoolConnector's reachability set (transport loss, moved generation,
  unknown mutation outcome) plus 404/5xx. Named refusals that are not
  DaemonHttpException — HostedHarnessRecoveryDeclinedException,
  harness_pool_unavailable — and 4xx now only back off instead of evicting a
  live Session's attachment. A table-driven test pins every branch; reverting
  the fallback to "evict" reds the declined and pool-unavailable cases.
- Pin the backoff ladder (10/20/40/80/160/300) and prove the idle-time prune
  keeps a still-owed debt's backoff when it leaves the scan page; clearing the
  pacing map each tick reds that test.

The packaged C3 arm and the terminal reply-loss regression stay green on the
real MySQL/Spring/Broker stack.
2026-10-29 16:35:42 +08:00
Bob.qwencodeandgithub-actions[bot] 08d97f383a chore(release): sdk-typescript v0.1.25 [skip ci]
* chore(release): v0.26.3

* chore(release): sdk-typescript v0.1.25

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-28 17:02:53 +00:00
Shaojin Wen c3e1c9bcd6 test(managed-agent): gate G3 terminal recovery faults (#15756)
* test(managed-agent): gate G3 cleanup and terminal recovery faults

Add three packaged fault arms to hosted-failover-e2e: a refused Runtime cleanup release followed by another crash, a committed terminal whose reply is lost, and a refused terminal write over a payable checkpoint. Each keeps the original Action, execution, file effect and terminal identity, and the runner now witnesses the fault on the store or Broker boundary rather than on a client-side retry.

Part of #14481.

* test(managed-agent): harden the G3 cleanup and terminal fault arms

Drop every bounded terminal retry so the terminal reply-loss crash lands on a committed-but-undelivered terminal, and require the surviving public transcript to carry the recovered answer. Document both observations. Part of #14481.

* test(managed-agent): address the G3 terminal arm review findings

Arm C1's crash barrier only on a confirmed store 200, record that pre-crash count, and state that the replacement projects the committed terminal without a second store commit. Snapshot fault counts before the relief Turn, keep the result-commit diagnostic for after-result, and fail loudly on an unhandled crash barrier. Remove the cleanup arm: a delayed replacement reproduces a control-plane cleanup/adoption gap, so C3 stays open as a product follow-up.

Part of #14481.
2026-10-28 11:38:48 +00:00
Bob.qwencodeandgithub-actions[bot] 5180685d97 chore(release): sdk-typescript v0.1.24 [skip ci]
* chore(release): v0.26.2

* chore(release): sdk-typescript v0.1.24

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-27 18:01:25 +00:00
qqqysandqqqys 9a41afe0d4 feat: add persistent session Workflow mode (DW-04A) (#15422)
* feat(core): persist session Workflow mode and dispatch reminders

* feat(sdk): expose session Workflow mode through native and daemon controls

* feat(cli): add Workflow mode controls and end-to-end coverage

* fix(cli): preserve pasted text when recalling prompt history

* fix(sdk): complete Workflow mode routing and owner-safe replies

* test: verify Workflow mode recording and Mod permission boundaries

* test(sdk): select the secondary workspace ACP mount

* test(cli): assert headless Workflow mode eligibility

* fix(workflow): enforce owner trust for prompt mode writes

* fix(workflow): keep daemon write policy out of MCP metadata

* fix(workflow): preserve retry and restore state across lifecycle failures

* fix(workflow): align ACP retry types with shared loop

* test(workflow): isolate ACP cadence from followup suggestions

* fix(sdk): close query after initialization failure

* fix(cli): serialize workflow mode writes with session switches

* fix(sdk): keep mode errors async and reject pending reinitialization

* fix(cli): filter workflow mode from SSH raw session state

* fix(cli): preserve workflow mode startup failures

* fix(acp): refresh restored session config options

* fix(core): rearm workflow notices after cancellation

* fix(core): rearm workflow notices after failed switches

---------

Co-authored-by: qqqys <[email protected]>
2026-10-28 01:09:37 +08:00
3521066b4f feat(serve): allow secondary sessions to cd within their workspace (#15323)
* [to #DW-549] feat(serve): allow secondary session cd within its workspace

* [to #DW-549] fix(serve): preserve cd rejection diagnostics

* [to #DW-549] test(serve): cover session cd in no-AK CI

---------

Co-authored-by: 罗辉 <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-10-26 20:19:50 +08:00
qqqys 645d32eed3 feat(acp): run the model round of fresh prompts on the shared agent loop (#15509)
* feat(core): let a host run its own rounds on the shared model round

ACP keeps its tool step while its model round moves to LlmClient, so it
needs a few things the client did not expose:

- Turn gives Thought the chunk's raw text and ToolCallRequest the
  functionCall it was built from;
- with SendMessageOptions.toolCallPreparations, Turn yields a
  ToolCallPreparation event for a call the model started streaming,
  also in a blocking Stop hook's continuation; without it Turn yields
  the events it did, which matters to the Hosted Harness;
- SendMessageOptions.userTurnRecordedByHost skips the UserQuery's user
  message record and todo work chain start;
- admitUserPrompt takes the hook's submitted_prompt and a signal;
- the agent loop exports runModelRound.

Part of #15327.

* docs(design): plan PR 2 in three steps and describe PR 2a

* feat(acp): run the model round of fresh prompts on the shared agent loop

With experimental.unifiedAgentLoop on, a fresh user prompt outside a
Goal, channel or goal-proposal approval turn sends each model round
through LlmClient via runModelRound. The client then does what ACP
re-implements: UserPromptSubmit on the verdict admitUserPrompt took at
the same point, memory recall, the file-history snapshot, system
reminders, workflow intent, MessageDisplay, compaction, the session
token gate and the external Stop hook. ACP keeps its tool step, its
user message record, its Stop loop for the Todo Stop Guard and
background results, mid-turn input and its session/update output.

The Web Shell settings panel now lists the setting. A new no-AK suite
drives ACP twice per scenario and requires the same requests, updates
and stop reasons, apart from three asserted differences.

Part of #15327.

* docs(design): link the PR 2a pull request

* fix(acp): keep the Stop hook in ACP's Stop loop on the shared round

Review fixes for the shared model round of fresh ACP prompts:

- the client skips its Stop hook (new stopHookRunByHost send option) and
  ACP's Stop loop fires it as with the setting off, so ACP's own
  continuations stay interruptible and are judged by the hook, and the
  Todo Stop Guard is suspended at the cap
- the queued-input window closes when a response finishes
- a round that ends with a stop reason discards its resolved tool cards
- ACP throws the error the provider threw (getStructuredErrorCause)
- one failure helper for both rounds, one compression notice, and
  getThoughtSummary built on getThoughtText

* fix(acp): leave the interaction span and tool-call guards to ACP on the shared round

Second review round for the shared model round of fresh ACP prompts:

- interactionSpanByHost: the client neither starts nor ends an
  interaction span, so ACP's span (with its daemon parent) is the only one
- toolLoopGuardsByHost: the client skips its always-on tool-call checks,
  the exploration budget and reminder, the stateful-read result checks and
  the tool-call heuristics (new LoopCheckOptions.toolCallPatterns); ACP's
  daemon guards stay the only ones, so the reminder is no longer sent twice
- a stream Error is thrown after the round, so the client's error epilogue
  runs first
- ACP schedules the managed memory tasks itself when no UserQuery or
  ToolResult round ended the response, as after queued input cut it short
- parity: a scenario past the per-turn tool-call cap; fix the
  typecheck:integration errors in the parity suite

* docs(design): list host-owned send options and move each surface's default on its own

- Add a ledger of the send options that tell the client to skip part of
  its work for one host, with the step that removes each, and a
  Stop-point continuations subsection for ACP's own continuations.
- Swap ACP's next steps: PR 2b moves the tool batch of fresh prompt turns
  onto the scheduler, PR 2c moves the other turns onto the whole loop, so
  each ACP loop body changes once.
- Each surface changes its default on its own once it meets the listed
  conditions, starting with headless, and drops its current loop one
  release later.

* fix(acp): keep tool-result recall and run memory tasks after the Stop loop on the shared round

Third review round for the shared model round of fresh ACP prompts:

- toolLoopGuardsByHost now skips only the tool-call guards, the
  exploration reminder and the todo reminder ACP's tool-result message
  carries; a tool-result send again delivers the memory recall and the
  queued loop reminder and runs the size-only microcompaction
- under stopHookRunByHost the client no longer schedules the managed
  memory tasks when a round ends; ACP runs them through the now public
  runManagedAutoMemoryBackgroundTasks once its Stop loop ends the turn,
  with its existing end_turn and loop-protection conditions
- a UserPromptSubmit block of a tool-result round shows the block notice
  and suspends the Todo Stop Guard instead of ending the turn silently
- core values come from their defining modules, not the package root

* test(acp): leave the follow-up suggestion out of the ACP parity suite

The suggestion request runs after session/prompt returns and may or may
not land before the agent is stopped, so one arm could record an extra
request. The suite compares the prompt's model rounds only.

* fix(acp): snapshot the user turn and open send-now from the request on the shared round

ACP records the user turn before the shared round's first send, but the
round left the file-history snapshot to that send. A prompt cancelled
between admission and the first send kept its user turn without a
snapshot, and rewindToTurn indexes snapshots by user-turn position. ACP
takes the snapshot again, as on the current path, and
userTurnRecordedByHost also turns off the client's snapshot.

The response-interrupt window opened on the client's first event, while
the current path's watch() is open once the request is out, so input
sent before a slow first token waited for the first chunk. The window
now opens at the send; a cut is held while the round's request has not
reached history, so it cannot drop the message it carries, and the
first event pulls again.

* fix(acp): recall from the prompt text and close blocked tool results on the shared round

- Pass the raw prompt text as `memoryRecallQuery`, so managed-memory recall
  no longer searches resolved @-file contents and host notices.
- When UserPromptSubmit blocks a tool-result round, close its calls with an
  explicit block result instead of leaving them to the orphan repair, whose
  "retry if needed" text invites rerunning the tool into the same block.
- Take the memory task promises the client queues after each turn; ACP
  reports no memory updates, so they otherwise accumulate for the session.

* docs(design): drop the recall-text difference and note the blocked tool-result close

* fix(acp): keep unsent input and per-response facts on the shared round

- Start fresh response facts and report usage per request when the
  client's next-speaker check continues within one round, so a
  continuation's complete edit is not refused for the earlier response's
  max-tokens cut, and its usage is not dropped.
- When the round throws before its request pushed the message, such as a
  cancel during a tool-result round's UserPromptSubmit hook, keep the
  message in history as the non-throwing stops do, so tool results that
  already ran are not left to the "retry if needed" repair.
- When UserPromptSubmit blocks a tool-result round, keep the input taken
  with the results (already shown as delivered) beside the blocked
  results instead of dropping it.
- Cover the merged hook-context dedup's empty-request append in core.

* test(acp): pin the unsent-message keep on a tool-result hook timeout

A tool-result round's UserPromptSubmit request carries no abort signal,
so a cancel does not make it throw; the case that throws before the push
is the hook request failing, such as its 60 s message-bus timeout. Test
that case instead, and say so in the comment.
2026-10-26 04:49:42 +00:00
Shaojin Wen 1df5031e70 perf(ci): overlap web-shell and standalone tests with the CLI suite (#15438)
* perf(ci): overlap web-shell and standalone tests with the CLI suite

The Linux Test job ran every workspace suite, the standalone runtime
regression and scripts/tests one after another. The CLI suite keeps its
workers busy for 13-20 minutes, while web-shell's suite spends most of its
run on App.test.tsx alone and the standalone regression starts one
runtime process at a time, so those two added their mostly idle minutes
straight to the job.

Run them in a side lane beside the other workspaces instead.
.github/scripts/ci/run-unit-test-lanes.sh starts web-shell and the
standalone regression in the background, runs every other workspace's
test:ci (listed with npm pkg get, in npm's workspace order) and then
scripts/tests, and replays the side lane's log after the main lane so
the two never interleave. The step runs the script under the same
setpriv wrapper, so every test command still runs without the DAC
override capabilities.

test:scripts now reaches its Vitest run through a new test:scripts:unit
script with a trailing --, so the main lane can run it without the
standalone regression while test:ci, test:ci:workspaces and test:scripts
run what they ran before.

* fix(ci): name the cause when the lane script cannot start a lane

- Fail with an ::error:: line when mktemp cannot create the side lane's
  log file, instead of running the main lane alone and dying in tail.
- Check the exit status of `npm pkg get` and of the parser. A failing
  `npm pkg get --json` prints an error object, which used to parse as an
  empty listing and report "Found no workspaces".
- Note in the header that the replay puts side-lane failures after the
  main lane's in main-failure-signature.mjs's title and search markers.
- Lane test: clear the caller's NODE_OPTIONS, assert the main lane's
  output is present before checking its order, and cover the failed
  listing and the missing temp dir.
- Cross-check npm's workspace resolution against the selector in
  scripts/workspaces.js.

* fix(ci): halve web-shell's worker cap beside the main lane

Each Vitest run applies VITEST_MAX_FORKS / VITEST_MAX_THREADS on its own,
so running two lanes at once doubled the step's worker cap, and on the
hosted runners, where no cap is set, put two all-CPUs pools on 4 vCPU.
The lane script now runs web-shell at half the cap, at least 1, and at 2
where the cap is unset; the main lane keeps the full cap.

- Refuse a workspace name with whitespace in it: the main lane's
  arguments are read one per line, so a line break would split a name
  into other arguments, flags among them.
- Lane test: web-shell and the main lane's suites now wait for each
  other, so a side lane serialized before or after the main lane fails;
  record each call's worker caps; run the script against npm's real
  listing and compare its main lane with the selector the release pins
  use; cover a name with a line break in it.
- ci.yml: the standalone runtime cache comment names the lane script's
  side lane, not test:scripts, as what runs the regression in this job.
- ci-variables.md: say the cap applies per Vitest run and what the
  two-lane step runs at its peak.
- release-workflow.test.js: follow test:scripts through
  test:scripts:unit to vitest; #15102 pinned the shape before the split.
2026-10-25 10:05:56 +00:00
aa6b65a52a chore(release): v0.26.1 (#15468)
* chore(release): v0.26.1

* docs(changelog): sync for v0.26.1

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: 易良 <[email protected]>
2026-10-25 01:27:17 +08:00
qqqys bafe530a84 feat(core): add an experimental shared agent loop and run headless on it (#15369)
* feat(core): add an experimental shared agent loop and run headless on it

Every surface implements its own send, run tools, repeat loop. This adds
runAgentLoop to core: one model round through LlmClient, the returned
tool calls as one batch on a single CoreToolScheduler, the results
finalized and recorded once in request order, then the next round. The
client's terminal events become typed stop reasons.

CoreToolScheduler gains an optional beforeToolLaunch callback that runs
serially in request order before each launch; a concurrent batch asks
for every call that fits in flight before launching any of them.
Without the callback the scheduler is unchanged.

With experimental.unifiedAgentLoop (or
QWEN_CODE_ENABLE_UNIFIED_AGENT_LOOP=1), headless runs its rounds and
drain items on the shared loop. With it off, headless runs the current
code, which only moved into an else branch. A no-AK parity suite runs
each scenario through the built CLI with the setting off and on and
requires the same requests, stream-json frames and exit code.

Part of #15327

* docs(design): update the unified agent loop design for PR 1

Record what implementing PR 1 settled: the launch hook asks a whole
in-flight window and every call of a batch once, results reach the
surface at batch end, the intended headless differences, how the design
relates to #8775, and the status.

Part of #15327
2026-10-24 15:21:54 +00:00
jinyeandQwen-Coder c61044fae4 feat(managed-agent): Start daemon and Managed locally with one command (#14928)
* feat(managed-agent): add local serve and lightweight runtime packages

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

* feat(managed-agent): download local server runtime on first use

Require local Java 21, install the exact-version service JAR atomically from OSS or GitHub, and reuse verified runtime caches offline. Add protected signed Central publication and identical release distribution without bundling Java in the CLI.

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

* fix(ci): register managed runtime workflow size

Record the new release workflow in the repository size baseline so the static CI gate can track it.

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

* feat(managed-agent): Share the local daemon and Managed entry

Serve daemon APIs and the signed Managed proxy from one loopback listener. Keep workspace ownership explicit, enable authorized local directory import and preserve the daemon trust boundary.

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

* fix(cli): expose configured models in local Managed tasks

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

* test(web-shell): Cover Managed and local deep-link restoration

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

* test(managed-agent): Align local database expectation with main schema

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

* fix(managed): Keep local model and approval choices independent

Use native initial model selection for local model presets and persist it with the selected approval mode before admitting the first Turn. Preserve custom Agent definition policy pins and creation replay semantics.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-22 16:25:18 +00:00
易良 7091697f00 fix(ci): enroll the OpenTUI parity tests in the no-AK lane (#15142) 2026-10-22 11:16:42 +08:00
Bob.qwencodeandgithub-actions[bot] 8f259db527 chore(release): v0.26.0 [skip ci]
* chore(release): v0.26.0

* docs(changelog): sync for v0.26.0

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-22 02:08:04 +00:00
qqqysandqqqys b51ce2c305 feat(workflow): add per-turn human-origin workflow hints (#14876)
* feat(workflow): add per-turn human-origin workflow hints

* fix(workflow): preserve intent across recall and cached submissions

---------

Co-authored-by: qqqys <[email protected]>
2026-10-21 06:54:24 +00:00
顾盼 a791e47a54 feat(cua): streamline computer use and port native reliability fixes (#14846)
* feat(cua): preload the JavaScript computer use runtime

* refactor(cua): remove the native Wayland backend

* feat(cua): move app computer use into the driver

* feat(cua): preserve background delivery and port platform fixes

* feat(cua): add explicit desktop input and browser runtime facade

* feat(cua): package the unified 0.26.0 computer use runtime

* fix(cua): finalize cross-platform native reliability

* fix(cua): isolate runtime releases from CLI versioning

* fix(cua): keep Electron out of root workspace installs

* fix(cua): synchronize consumers and async cleanup tests

* fix(browser): migrate smoke to preloaded cua runtime

* docs(cua): document MCP setup for Qwen Code Claude Code and Codex
2026-10-21 14:03:31 +08:00
ChiGaoandgary.gq 8f3c7e15e4 fix(cli): preserve runtime boundaries during standalone updates (#14934)
* fix(cli): preserve runtime boundaries during standalone updates

Preserve Node/Bun archive flavor and validate actual CLI loading, package
identity, version and prepared installation identity before activation.
Keep Node npm transport and restrict Bun registry lookup to trusted config.

Integrates chiga0's update stack through ce67edaba5,
with isolated fixtures and real Node/Bun runtime regression coverage.

Co-authored-by: gary.gq <[email protected]>

* test(cli): bound cancellation test waits and cleanup

* fix(cli): validate standalone launchers and registry settings

---------

Co-authored-by: gary.gq <[email protected]>
2026-10-21 05:34:26 +00:00
ChiGao 6e6598e35c fix(cli): restore bounded OpenTUI dialogs and add migration gates (#14933)
* fix(cli): restore bounded OpenTUI dialogs and add migration gates

* fix(tui): harden migration cleanup and evidence after review

* test(tui): fail acceptance on unhandled errors
2026-10-21 10:05:14 +08:00
a1fb0306ae feat(mods): support tool middleware in interactive terminals (#14565)
* feat(mods): support tool middleware in interactive terminals

* fix(mods): preserve nested approvals and final terminal results

* style(mods): keep terminal imports below file headers

* test(mods): assert terminal middleware drains on quit

* test(mods): verify native attempt approval and validation isolation

* fix(mods): retain hook owner across worker tool callbacks

* fix(cli): Drain revoked Mod tool turns in OpenTUI

* fix(cli): Clear approvals when the owning turn ends

* fix(cli): Restore tool eligibility after failed session switches

---------

Co-authored-by: qqqys <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-10-20 16:14:50 +08:00
Shaojin Wen d2e12e404f test(managed-agent): crash the approval result-commit barrier (#14733)
* test(managed-agent): crash the approval result-commit barrier

A5's last packaged barrier: after the approved tool result commits, hold the
post-tool model round and crash the Harness. The next generation must reissue
the saved request (deep-equal), retract the published prefix, and finish with
one execution, one file effect and one terminal.

Part of #14481.

* test(managed-agent): gate the result crash on the published prefix

Review findings: wait for the public feed to carry the held prefix before
SIGKILL and record it in the crash evidence, so the transcript assertion
cannot pass on a no-op retraction. Also decouple the source pins from
prettier whitespace and the comparison operator.

* test(managed-agent): pin the after-result model count

Review follow-up: keep the two-request expectation tied to the after-result
arm via expectedModelsAfterResults, without reintroducing the indentation
coupling that the previous split removed.

* test(managed-agent): strengthen the result-commit assertions

Review findings: name the two pre-dispatch barriers and pin after-result's
exclusion from the held start, assert the stream.reconciled ordering beside
the reload text, retain both request bodies in model-request-parity.json,
and pin the retraction throw.

* ci: record the sdk-java workflow growth

The five approval fault arms add steps and comments to sdk-java.yml: 104042
bytes, 4100 over the previous 99942 baseline. Update the ratchet in the same
PR, as check-workflow-size.sh prescribes for real growth.
2026-10-20 05:44:55 +00:00
易良 eeec911c20 fix(ci): raise the build heap cap from 4096 to 6144 MB (#14822)
The packages/cli TypeScript build inside the Desktop Checks
'Prepare bundled runtime' step now dies with 'Ineffective
mark-compacts near heap limit' at exactly 4030 MB on both macOS
jobs, i.e. it hits the 4096 MB ceiling the root build script sets
via NODE_OPTIONS; the Linux and Windows legs of the same run finish
under the cap (#14793). The CLI project graph crossing the 4 GiB
type-checking threshold leaves neither macOS leg headroom.

Raise the build-time ceiling to 6144 MB, mirroring #11781's earlier
3072 -> 4096 bump. It is still a ceiling: V8 only consumes what a
build actually needs, so local builds and runners whose jobs fit
under 4 GiB are unaffected.
2026-10-20 02:55:35 +00:00
易良andQwen-Coder 5ac26809fe fix(ci): include updatedInput hook integration tests in PR checks (#14747)
* fix(ci): include updatedInput integration coverage in PR checks

* test(scripts): pin that every no-AK entry still resolves to a test file

Vitest positional args are filename filters, not paths: a renamed entry
matches nothing while the other filters keep vitest exiting 0, so the gate
would report pass with that suite's coverage silently gone. Assert every
pinned entry still exists under integration-tests/.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-20 10:39:41 +08:00
qqqysandqqqys de20ab6a30 feat(workflow): authorize daemon ancestor workflows (#14646)
* feat(workflow): authorize daemon ancestor workflows

* fix(workflows): fence ancestor trust across policy and cwd changes

Preserve the actual admission publication through reconciler rebuilds.
Reject in-flight and stale-directory replies throughout physical cwd changes, including partial failures.

Refs #14646

* fix(workflows): retain ancestor trust after context clear

* fix(workflows): normalize restored ancestor trust session ID

* test(workflows): use native cwd for restore trust fixture

---------

Co-authored-by: qqqys <[email protected]>
2026-10-19 15:00:52 +00:00
Shaojin Wenand易良 c076c699af test(managed-agent): add G3 approval fault-point E2E arms (#14687)
* test(managed-agent): add G3 approval fault-point E2E arms

Step 3's packaged gates crossed the approval path without a fault. Four new
arms layer one failure on the Allow arm: a crash after the decision commits,
a crash after the approved execution reaches PREPARED behind a held Runtime
Broker start, a lost decision reply retried under the same Idempotency-Key,
and a lost Turn admission reply retried the same way.

Part of #14481 (step 2).

* test(managed-agent): gate the approval crash on the committed decision

The response endpoint's 202 only admits the response operation; the Action
row stays requested until delivery commits. The after-decision arm now holds
the Runtime Broker start and waits for state=decided with a decision receipt
and digest before SIGKILL, and a predicate test rejects the admitted-only
answer.

Also add the four fault arms to the hosted-process-ci inventory and raise
its step/ceiling expectations to 190/205.

* fix(managed-agent): scope the approval decision barrier query

The after-decision arm's durable-decision wait referenced actionFilter, a const inside the earlier approval-setup block, so the new arm threw ReferenceError before it could crash the Harness. Build the same predicate from the in-scope sessionFilter and the record/action columns instead.

---------

Co-authored-by: 易良 <[email protected]>
2026-10-19 14:22:44 +00:00
qqqysandqqqys c4f0acdef8 feat(extensions): add bounded generated HTML and JavaScript views (#14338)
Co-authored-by: qqqys <[email protected]>
2026-10-19 13:09:33 +00:00
Bob.qwencodeandgithub-actions[bot] 530a22680d chore(release): v0.25.3 [skip ci]
* chore(release): v0.25.3

* docs(changelog): sync for v0.25.3

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-19 07:38:12 +00:00
qqqysandqqqys bd63f900c7 feat(workflow): discover trusted ancestor workflows in local sessions (#14369)
* feat(workflow): discover trusted ancestor workflows in local sessions

* fix(workflow): enforce ancestor trust authority and preserve scoped loading

* test(workflow): use discovery fixture agent type for initialization

* test(workflow): support partial settings fixtures in ACP trust mock

* test(acp): provide operator trust view in worktree fixtures

* fix(workflow): preserve safe discovery on root I/O errors

---------

Co-authored-by: qqqys <[email protected]>
2026-10-18 19:54:22 +08:00
Bob.qwencodeandgithub-actions[bot] 25ef506c03 chore(release): sdk-typescript v0.1.20 [skip ci]
* chore(release): v0.25.2

* chore(release): sdk-typescript v0.1.20

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-15 17:01:33 +00:00
Shaojin Wenand易良 95468e293f test(managed-agent): gate G3 Step 3 recovery, model-round takeover and control-plane fencing (#14181)
* test(managed-agent): gate G3 Step 3 recovery, model-round takeover and control-plane fencing

Run the five Step 3 packaged gates (approval Allow, Deny, Cancel and
expiry, and the continuation closeout) as hosted-harness-mysql steps
through a new --g3-verify flag; G3_VERIFY_MODE keeps working.

Add a model-round arm: the first model round of a Session with no
Workspace survives two Harness-only replacements, each reissuing the
same saved request and retracting the dead owner's prefix, with no tool
execution and a clean next-Turn history.

Add a frozen control-plane arm for the binding half of #12952 Q2: the
original Spring and Harness are SIGSTOPped mid-round, a second pair on
the same MySQL finishes the Turn, and after SIGCONT the woken Spring's
stale claim, witnessed in the MySQL general log, must leave the Session
binding, the Turn row, the public events and the journal unchanged.
The woken coordinator never reaches a bind call, so a store test pins
the bind guards directly: a former owner whose claim lapsed cannot
rebind, renew or admit, and a terminal Turn cannot be bound.

Fix the server README paragraph that still said a model-round park
wedges the Session, document every G3 arm and the first-release
recovery scope, and record decision D11 in both languages of the G3
design.

Refs #12952

* test(managed-agent): answer the first review of the G3 residual gates

The README no longer says a parked model round or approval in a Session
with Hooks or MCP ends with a typed decline: a Hooks Session never takes
over and an MCP approval plain-attaches. It now says Step 3 neither
reissues nor resumes those states and no gate covers how they end.

The model-round arm requires the writer generation, journal revision and
committed sequence to advance across every takeover, and its terminal
wait watches both the live control plane and the Harness finishing the
round; waitUntil and waitForTerminal accept several children for this.

The frozen control-plane arm gains a writer-side witness: the woken
Harness must meet the store's stale-writer refusal after the wake and
not before, counted on its output since the wake rather than the
rolling log tail. It also refuses to wake a Harness whose home is gone,
reports the retained home, and splits a poll budget across the
snapshot's synchronous probes while one-shot snapshots keep 10 s each.

The store test pins the owner check of recordRecoveryAdmission for a
former owner, and both admission refusals now check their message.
New pins cover the writer side of the Step 3 evidence path, the awaited
waits, the pre-wake baseline and the checks above. The design doc's
stale CI budget item now points at D11, which states the step sum.

Refs #12952

* test(managed-agent): answer the second review of the G3 residual gates

waitUntil's deadline message now takes its log tails from the normalised
child list, so a deadline on the model-round terminal wait reports both
tails instead of throwing a TypeError on the array. An early exit names
the child that died, which tells the control plane apart from the Harness
generation finishing the round.

The waitUntil tests cover a dead first child, a dead last child and a
deadline with both children alive, and the signal-death comment sits on
its own test again.

The frozen control-plane arm watches the replacement Spring and Harness
through the quiet window and every wait after the wake, and checks them
once more before the fenced verdict: a dead owner would otherwise hold
every snapshot still.

The README and both design docs no longer say the new gates run in CI.
They are steps of the Hosted MySQL job, which runs daily or on demand and
reaches them only after its verification step passes; that step was
failing on main when the gates were added. The D11 snapshot list now
says the journal probe is a per-generation, per-operation count and
highest revision.

Refs #12952

* test(managed-agent): let the frozen arm see an owner exit during its probes

The fenced snapshot's probes are synchronous, so a replacement that exits
while they run is not recorded until the event loop turns. The arm now
yields once before checking the owners, and the pin covers the check's
body, not only its position.

The docs say the new steps run only when every earlier step of the job
passes, not only the Verify step, and state in the past tense that no CI
run had executed them when they were added. The README's no-change
statement is bounded by what the D11 snapshot can see.

Refs #12952

---------

Co-authored-by: 易良 <[email protected]>
2026-10-15 19:29:48 +08: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
qqqysandqqqys 6775f24927 feat(extensions): add test failure report explorer (#13854)
* feat(extensions): add test failure report explorer

* fix(extensions): bound report snapshots before serialization

* fix(extensions): bound test report validation

---------

Co-authored-by: qqqys <[email protected]>
2026-10-11 12:58:55 +00:00
Bob.qwencodeandgithub-actions[bot] 726e8d77f7 chore(release): v0.25.1 (#13904)
* chore(release): v0.25.1

* docs(changelog): sync for v0.25.1

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-11 12:00:47 +00:00
Shaojin Wenandjinye 2a69a2ff22 fix(managed-agent): commit oversized message records as inline chunks (#13375)
* fix(managed-agent): commit oversized message records as inline chunks

A Hosted answer past the 64 KiB inline resource limit streamed in full
through message.delta events and then failed its turn when the final
message.committed record exceeded the limit with OSS disabled. Split an
oversized record body into ordered managed-message-part resources behind
a managed-message-chunks manifest — the shape Hook message snapshots
already use — and reassemble on every read path (projection hot and cold
reads, the hosted SSE feed, workspace recovery). Each part stays under
the per-resource limit and the transaction stays within the server
budget, so a long answer completes instead of failing after publishing.

Fixes #13326

* fix(managed-agent): preserve long answers through recovery

Refs #13326. Builds on #13375 with the 64 KiB inline boundary, sequential chunk reads, and full-stack integrity regressions.

* test(managed-agent): allow streamed message settlement to finish

* fix(managed-agent): address long-answer PR review feedback

Strengthen recovery, streaming and transaction-budget regressions, wire the long-output E2E into CI, and clarify the supported persistence scope.

Refs #13326, PR #13375

* test: address long-answer persistence review feedback

* codex: address PR review feedback (#13375)

---------

Co-authored-by: jinye <[email protected]>
2026-10-07 19:40:46 +00:00
Shaojin Wen 4970bfa172 feat(managed-agent): adopt the next Hosted Harness generation (G3) (#13174)
* feat(managed-agent): adopt the next Hosted Harness generation (G3)

A Hosted Session no longer dies with the Harness process generation
that first served it (#12952 G3, Steps 1+2). On a generation signal
the control plane renegotiates once through the connector instead of
failing every bound Session until its own restart; pending Turns
re-attach through the takeover load as their retries come due.
Takeover refusals split into a typed terminal decline (deterministic
in the journal) and the existing retriable code; the takeover load is
idempotent against a lost reply and gets its own timeout; negotiation
requires the journal-contract feature token so a rolled-back Harness
is refused once; a marked-but-never-admitted Turn withdraws its
submission mark, backstopped by journal commandId idempotency.

The failover runner gains a Harness-only restart arm on all three
scenarios (harness_boot_id moves with no Java restart) and a
SIGSTOP/SIGCONT frozen former-owner fencing arm, wired into
hosted-harness-mysql together with the previously local-only basic
mode. Design docs included in English and Chinese.

* fix(managed-agent): tolerate the bootstrap 404 of a restarting Hosted Harness

The serve delegating app answers every routed path with a bare
pre-contract 404 while its runtime is still starting. The Harness-restart
arms raced that window: a prompt submitted milliseconds after the new
process bound the port could meet the bare 404, which the client read as
a generation contract violation and ended the Turn as
hosted_harness_protocol_error without any retry. A missing boot header on
404 now maps to a transient transport error so the coordinator retries
through the window, and the failover runner waits on deep health
(?deep=1) before driving turns into a restarted Harness. Terminal turn
failures now also log their underlying exception.

* fix(managed-agent): freeze only the Harness writer in the former-owner fencing arm

SIGSTOPping both original owners left the original Spring's workspace
binding unreclaimable: the reclaim proves death from /proc liveness, and
a stopped JVM still reads as alive there, so the replacement's reconcile
timed out (runtime_broker_reconcile_timeout) and the Turn never
completed. The arm now stops only the original Harness (reclaiming its
workspace binding requires the Spring to die anyway — the continuation
mode's crashProcess), completes the Turn with a full replacement, then
wakes the frozen writer and asserts the journal, transcript, and
session binding are untouched. Design D7 updated in both languages with
the discovered boundary: resource-level takeover of a surviving owner is
structurally out of reach today, which is exactly what this issue's
sequential-generation scope excludes.

* ci: retrigger sdk-java workflow after event delivery stall

* fix(managed-agent): pin the fencing proof on writer identity, not head revision

The first merge-base run of the freeze arm failed its own gate: after
waking the frozen Harness the journal head moved (rev 30 -> 45) while
the writer generation, boot binding, transcript, and terminal count all
held. The moved revisions were the replacement's own legal churn, not
old-writer mutation, so the head-equality assertion conflated the two.
The exit-check claim is identity: no transaction out of the old writer
generation after it wakes. Count that straight off
qwen_managed_session_journal_tx (which carries writer_generation), keep
the binding/transcript/terminal assertions, and align design D7 in both
languages. The error string now also reports the old-writer tx count.

* fix(managed-agent): qualify the journal tx schema in the freeze-arm gate

The identity-based old-writer count queried qwen_managed_session_journal_tx
unqualified; the runner's MySQL connection has no default schema, so the
arm died with ERROR 1046 instead of evaluating the fence. Qualify it as
qwen_managed_agent.qwen_managed_session_journal_tx like every other query
in this runner. No behavior change to the assertions themselves.

* fix(managed-agent): harden takeover correctness from the R1 review

- settle turn_settled checkpoints through the load route's own tail
  instead of a terminal decline, and keep transient store verdicts
  (missing_state/missing_checkpoint) retriable on both authorization
  reads
- exempt only takeover-shaped 409s from the pre-admission retry budget;
  every other failure kind meets it
- scope the 120s load-timeout to recovery loads so plain attaches under
  the CHM bin locks stay on 30s
- adoptGeneration: move map churn out of the connector monitor
  (lock-order inversion), evict only stale-boot entries on equal-boot
  mismatches
- answer a duplicate submit of an already-settled prompt with a 202
  replay anchored at its input.accepted sequence
- withdrawSubmissionAttempted: guard on the same status window as its
  peer CAS methods; complete capability mismatch terminally in both
  remaining coordinator layers
- runner: freeze-arm pre-freeze liveness assertion, /health-proven
  wake non-vacuity, teardown SIGCONT, exact-string dispatch-generation
  bounds
- CI: job ceiling 95 with corrected ceiling math; pin all seven
  failover arms in hosted-process-ci.test.js
- sync both design-doc languages with the shipped behavior

* fix(managed-agent): close the R2 review round on the replaceable-host-harness slice

- withdraw + cancel interference (R2-1, Critical): a CANCELLING Turn
  whose never-admitted mark was withdrawn is fast-cancelled instead of
  being re-dispatched into a second live execution at the adopted
  generation
- the generation-mismatch catch escapes the pre-admission budget only
  on a bound Session, like every sibling arm (R2-3)
- bound the remote-supplied decline reason to the error_message column
  so the terminal write itself never throws (R2-5)
- the last fail(...) in the catch ladder is failTerminally with the
  exception logged, like its siblings (R2-6); the withdrawal itself is
  logged too (R2-7)
- freeze arm: the replacement Spring inherits the original port so the
  woken writer meets a live fencing control plane, not a dead socket
  (R2-2); the wake block asserts the head actually advanced past the
  frozen owner's writer generation, drops the wrong heartbeat claim
  (R1-8 follow-up), separates the two disk-audit meanings, and asserts
  the retained Harness home (R2-8)
- the hosted /capabilities feature list derives from the registry
  instead of a hand-maintained literal (R2-9)
- docs: D7 scoped to the journal half of the Q2 exit check, the binding
  half and the pre-contract-404 classification recorded in Boundaries
  (R2-4, R2-10)

* docs(sdk-java): correct the load-timeout coverage comment in the client test

* fix(managed-agent): reference the frozen Harness through the children registry in teardown

The R1-4 teardown SIGCONT referenced the try-block-scoped `harness`
binding from the finally block, so every freeze-arm run died with
ReferenceError after all fencing assertions had already passed (CI run
37009843147). Reach the child through the children registry by its
start() name instead.

* docs(managed-agent): scope the adoption sentence to this slice's takeover reach

* fix(managed-agent): close the R3 review round at its five shared roots

- retract the turn_settled settle tail to a typed decline: the tail
  answered 200 with no recovery watermark (Java then failed the bind,
  orphaned a completed journal turn), fabricated state:'completed' on
  phase alone, ran mutating work under a passive load, and retained no
  snapshot for the lost-reply retry. D10 already owns
  rebind-and-keep-reading as the Step 3 row (R1-25-re, R3-2 .. R3-5)
- the load route's restore guard applies the same durable-vs-transient
  verdict read, because the bundle collapses both into one blocked bit
  and the durable decline was unreachable from production (R1-26-re)
- the pre-admission exemption keys on lease-shaped 409 codes only:
  configuration-shaped 409s (e.g. hosted_tool_profile_conflict) meet
  the budget, and the same 409 exempts nothing on an unbound Session
  (R1-1-re, R1-30-re, R3-13)
- equal-boot adoption keeps pendingRecovery: the markers were minted by
  the same live client (R1-29-re)
- lifecycle capability mismatch: revert completing-as-unconfirmed —
  that skipped the drain, flipped the session, and recorded a clean row
  with no FAILED vocabulary to tell it apart; the reason-loud retry is
  the honest end of the line until an operator realigns versions, and
  the README sentence says so. Action-response outbox keeps its honest
  FAILED-with-code terminal (R1-45-re, R3-5, R3-15)
- test hardening: concurrency close-once now surfaces worker assertion
  failures via Future.get and pins that no real client build happened
  (R3-12); loadTimeout-vs-requestTimeout discriminated at the call site
  (R3-9); single-walk acceptedInputSequence (R3-18)
- runner: 5-column execution queries pin runtime_session_id unchanged
  under --harness-only (R3-14); frozen-arm teardown looks the child up
  by a shared label constant and fails the run on a drift (R3-17)
- CI: ceiling 105 covers the nine summed step ceilings (92) plus the
  uncapped setup steps (R3-6); the seven-arm pin also asserts each
  script's flags and the install step's existence (R3-16)
- docs: both languages re-synced (D5 retraction, D9a code allow-list,
  D7 teardown, D8 runtime identity, honest validation digests, the four
  missing changes-table rows, README retry clause)

* fix(managed-agent): replay takeover snapshots only to a same-shape request

A passive load only asks broker.status(), so its snapshot keeps
phase='await_runtime'; replaying it to a drive load handed the
coordinator a checkpoint that isContinuationReady() can never accept,
terminally failing an otherwise continuable Turn. Record the request
shape with the snapshot and replay only on a mode match; the clear
conditions stay promptId-only.

Also make the adoption race test deterministic: a package-private
createClient() seam lets the test inject the replacement instead of a
real /capabilities call (proven flaky on CI), worker assertion failures
now surface through Future.get, and the pendingRecovery fixture seeds
its marker through the recovery path.

* fix(managed-agent): close the self-audit round on the replaceable-harness slice

Three rounds of undirected plus adversarial audit over the whole PR diff
(31 files), with three parallel deep-dive passes over the Java, TypeScript
and runner/docs surfaces. Every finding was re-verified against the tree
before acting; five reported defects were rejected on evidence.

Correctness:
- the load route read the blocked-restore verdict AFTER managed.close();
  sealing the journal makes every later store read fail as "writer is not
  active", which the authority erases into missing_state, so the durable
  branch this PR added could never fire and a corrupt checkpoint stayed in
  the retry-forever class. Read the verdict first, and pin the order with
  a witness that goes red when the read moves back (the local authority's
  seal does not disable reads, so only the ordering assertion catches it).
- drop hosted_session_already_attached from the lease-bounded 409 set: the
  daemon removes an attachment only on an explicit detach or delete and this
  control plane never detaches, so a Spring restart against a surviving
  Harness retried a permanent refusal forever instead of ending the Turn.
- adoptGeneration evicts by boot identity in both branches instead of
  clearing, so a marker minted by a concurrent rebuild can no longer be
  erased between two adjacent clears (which left a live attachment with a
  null recovery report), and markers follow their attachment.
- the post-withdrawal fast-cancel reads the row again: a cancel landing
  after the claim is invisible in the claimed snapshot, and acting on it
  submitted a live execution only to cancel it below.
- a durable verdict re-read after settling now declines as
  checkpoint_blocked, matching the identical pre-settle classification.
- an unrecognized blocked reason throws instead of declining, so a reason
  core adds later cannot silently become a terminal Turn failure.
- the operator-settable load-timeout is validated at connector
  construction; a bad value previously threw on every client() call and
  surfaced as an endless transient retry naming only the exception class.

Tests: already_attached and lost-withdrawal-CAS witnesses; the fast-cancel
witness now pins the re-read (cancel after claim); the concurrent-adoption
test surfaces worker assertions through Future.get and injects the
replacement through a package-private createClient() seam instead of
standing up a live /capabilities call; the CI pin gains each arm's mode
flag and a ceiling-sum invariant.

Docs (both languages): D1 now describes the boot-identity eviction and
lists what this slice really added to HostedHarnessClient; D3 no longer
claims the bind-refusal path stops being reached (it is D4's trigger); D6
records the same-shape replay clause; D7's fence rationale no longer blames
heartbeats (lease renewal touches writer_lease_until only); D9a names the
two exempt codes and why already_attached is not one; the validation digest
states which claims are pinned and which are not; the CI arithmetic reads
12 + 8x10 = 92; the stale workflow nit is dropped.

* chore(ide): regenerate NOTICES for the merged dependency set (email channel)

* fix(ide): replace the workspace-polluted NOTICES with the pristine regeneration

The file committed in f7a960c559 was generated under a polluted module
layout left by an aborted install: its resolution hoisted
[email protected] and produced 663 total dependencies, so
CI's own regeneration (pristine: 662) mismatched and the up-to-date gate
failed. Replace it with the pristine worktree's output, which the gate's
own regeneration produces to a no-diff state.

* fix(managed-agent): close the R4 review finds on the takeover and refusal paths

- classify missing_checkpoint and a bare missing_state as durable verdicts
  so a permanently unrecoverable session declines instead of being retried
  past the lease window; only a missing_state carrying the erased store
  error's message stays retriable
- map a fenced predecessor's managed_session_writer_conflict to the coded
  retriable 409 in the load route so the wait provably ends with the lease,
  and allowlist that code in LEASE_BOUNDED_409_CODES (dropping the inert
  hosted_prompt_recovery_required, which can only arrive post-mark where
  the exemption gate is already bypassed); null-guard the membership test
  against codeless error bodies
- replay a takeover snapshot only to a request whose parsed store identity
  matches the attached session's, never to any takeover-shaped caller
- fetch the harness client AFTER resolving the attachment at the six entry
  points, so an adoption completed during a create/load round trip cannot
  strand a call on the closed client
- pin the reply watermark of the journal-replayed prompt admission, the
  construction-time loadTimeout guard, the marker-eviction half of
  adoptGeneration, and the withdrawal CAS ordering with witnesses, and
  widen the load-timeout test's durations so the capabilities negotiation
  keeps headroom on contended lanes
- narrow the README's typed-decline sentence to the surviving row and
  generation binding, and mirror the exempt-code list in both design docs

* fix(managed-agent): answer the round-5 waits without terminalizing them

- a takeover now splits three ways: drivable answers recovered, a
  deterministically unrecoverable drive keeps its typed decline, and
  anything whose wait belongs to someone else (a requested approval, or
  the whole non-drivable space on a cancellation-only load) answers
  inapplicable so the route falls through to the plain attach — a
  user-cancelled Turn cancels instead of failing as
  managed_runtime_recovery_blocked, and an approval wait survives its
  generation rather than dying while the answer is still deliverable;
  the coordinator keeps the await_action guard as a retriable path even
  if a Harness still declines it
- an unconfirmed cancellation re-drives with its snapshot restored: the
  cancel admission's rollback is now symmetric, so the retrying takeover
  load replays instead of being refused already-attached
- an unrecognised blocked reason at the final re-read stays retriable on
  the drive side like the pre-settle read, instead of terminalizing on
  unresolved_after_settle; only durable verdicts may end a Turn
- the hosted persona's curated tag set is declared once in the registry
  (hostedPersona descriptor flag) instead of being filtered by a third
  hand-maintained list at the route, pinned by the docs-contract test
- the harness-only arms pin a surviving worker through the runtime
  session row's own liveness metric, not the insert-frozen id column
- witnesses: declined reason on the wire, cancel-report replay after a
  lost answer, inapplicable shapes at the recovery kernel (both loads),
  the drive disjunct of the load-timeout predicate, the one rebuild per
  adoption race, and coordinator corners for cancelling/await_action

* fix(ci): qualify the runtime-session heartbeat query with its schema

The harness-only arms' liveness beat read qwen_runtime_session bare;
mysql runs without a default database, so the Harness-restart in-flight
arm died on ERROR 1046 with 'No database selected'. Every other probe in
the runner already addresses qwen_managed_agent.<table> — this one now
does too.

* fix(managed-agent): close the round-6 shape and wedge findings

- the pre-kernel no-tool refusal and the restore guard now honour the
  same shape rule the kernel does: a cancellation-only load answers
  inapplicable and attaches plain (or the baseline retriable refusal
  where no attach is possible) instead of terminalizing a Turn the user
  asked to cancel; a takeover that answers inapplicable no longer trips
  the bare-load refusal on its empty recovery
- the lease-bounded exemption is keyed on the writer lease's own wire
  code only: hosted_turn_recovery_required covers arbitrary takeover
  failures, and exempting it wedged durable refusals in an unbounded
  pre-admission retry; a refusal carrying that code now meets the budget
  like every other body shape while managed_session_writer_conflict
  keeps its D9a window
- witnesses: the no-tool plain-attach coincidence for cancel vs the
  typed model_start decline for drive, the durable restore verdict on
  the cancellation arm, and the budget-hit for a durable 409 on the
  bound path (the old takeover-409 premise rewritten, its writer-
  conflict twin already covered)

* fix(managed-agent): settle a cancel first on a plain-attached replacement

A real-stack round-4 wedge: cancelling a Turn whose Harness died mid
model wedge (Work realized after the merge) failed as
hosted_harness_generation_mismatch — the plain attach leaves the bind
CAS unwinnable (boot-old != boot-new), the cancel was never issued, and
everything queued behind the Turn then retry-forever: later prompts
409, a close 409, no public-API escape.

runClaimed's recurrence without recovery now cancels before it binds:
a CANCELLING Turn does not submit, and a bind exists for submissions —
the next Turn's takeover adopt moves the generation when something
actually continues. cancelledOnAttach suppresses the tail's later
re-cancel. The R5 witness now also pins: bindHarness is never asked,
withdrawSubmissionAttempted is never touched, and the Turn is neither
recovery-blocked nor generation-mismatched. Design docs D3/D4 gain the
bypass rule in both languages.

* fix(managed-agent): close the inapplicable aftermath the round-8 review found

- an inapplicable takeover must not latch session.blocked — a 200 answer
  that refuses every settlement route is a lie (R5-2's latch regression:
  nothing outside settleCancelledHookTurn clears it for a non-hooks
  session). The blocked latch now splits the same shape rule: only a
  genuinely unsettled takeover parks there, not an inapplicable answer.
- a plain attach of a bound Session after adoption rebinds the Session
  row with its own CAS (owner + live lease + expected = the row's boot),
  since bindHarness provably refuses a turn that already posted its epoch
  — the Turn is neither generation-mismatched nor stranded (R8-1).
- a Turn written turn_settled completes, so it is never stamped as
  managed_runtime_recovery_blocked: the kernel answers inapplicable on
  BOTH load shapes (the plain attach lets the daemon's own projection,
  not a declined verdict, write the terminal record), retaining the
  rebind-and-keep-reading rationale for Step 3 (R8-2).
- the #13388 pinning test's mock endpoint now advertises
  managed_session_journal_delta_v1 like every other Harness fixture of
  this branch — a merger's follow-up angry about the gate (D9c), not an
  upstream flake.
- witnesses: turn_settled pinned as inapplicable on both shapes at the
  kernel; plainAttachAfterAdoptionRebindsBeforeItSubmits asserting the
  store CAS adoption; R5-2's corner witnessing 200 without
  recoveryRequired; coordinator 46/46, hosted client + pinning tests,
  TS session+recovery fully green.

* fix(managed-agent): close the inapplicable aftermath the round-9 review found

- the no-tool arm now refuses with the baseline retriable one again
  instead of minting a plain attach whose parked Turn nothing may
  resolve (R5-2'), and a takeover load without broker options shares
  that refusal;
- the journal prompt replay answers 202 only when the request body's
  digest proves identity against the admission its input.accepted
  holds; carrying the same Id with different bytes is a real
  prompt_conflict, never a silent swap of watermark (R9-1);
- a plain attach after adoption moves the Turn's stream epoch through
  recordRecoveryAdmission to the attach's epoch, on both the binding
  ladder and the cancel-on-attach arm — a stream opened with the
  claimed epoch would be rejected on the new generation as epoch
  mismatch (R8-1').
- witnesses: the no-tool cancel arm refusing retriably while drive
  declines with its typed reason, the digest mismatched replay timing
  out as hosted_prompt_conflict, and R8-1 streaming the adopted
  generation's epoch on both arms (plainAttachMoves* and
  cancelOnAttachStreams*) with recordRecoveryAdmission pinned.

* deflake: carry #13403's pinning fixture under the journal-delta gate + harden the 13328 mount-holder waitFor

The merge of #13403 turned the Java CI lanes red with
DaemonProtocolException: Endpoint does not advertise
managed_session_journal_delta_v1 on every burst caller: this branch's
D9c capability gate (from #13388's pinning work) rejects endpoints
without that token, and #13403's new /capabilities fixture did not
advertise it. Same fixture-parity fix as #13388's — the token is a
fixture property, not production behavior; the deliberate negative
stub (capabilitiesJsonWithoutJournalToken) stays as the gate's
witness.

The ubuntu Test lane red on the same head was the issue-13328
mount-holder vi.waitFor defaulting to its 1 s window against the
admission pipeline on the loaded runner ("expected undefined to be
defined", repeated across both retries). Give it an explicit 10 s
timeout, assertions unchanged — the same deflake pattern as
#13411/#13323/#13430.

* fix(managed-agent): close the R11 review round (inapplicable pays only what it can)

R11 (qwen-code-ci-bot review, 5 threads, 3 new + 2 re-checks) landed on
the shared consequence of R10's fixes: an inapplicable/plain-attach answer
stays honest only where a settlement route can pay.

- R11-1: the prompt route's session-scope guard now answers the
  SESSION-level wedge code (hosted_turn_recovery_required); the
  prompt-scoped code names only the requested prompt's own unsettled
  duplicate, so the duplicate-admission adopt branch can no longer be
  forged by a refusal about someone else's parked Turn — and the budget
  treats the session-level code as pre-admission whenever the Turn
  provably recorded no epoch, recording the wedge's own code on
  exhaustion (provenance-gated on an existing submission mark).
- R11-2 / R10-1 re-check: both takeover arms compute the bare branch's
  settle projection inline (`settleProjectablePromptId`): approval waits
  keep the plain attach (resolve pays), turn_settled is settled by the
  load itself via the extracted `runSettleProjection`, and an unpayable
  park keeps the baseline retriable 409 instead of a healthy-looking 200.
- R10-2 re-check: the kernel's passive inapplicable is narrowed to those
  two payable states; initial / durably blocked / model-start / unknown
  phase / wrong-checkpoint throw into the retriable refusal.
- R11-3: createOrLoad's final attachment put restores the pendingRecovery
  marker for snapshot-carrying passive re-attaches, or the cached branch
  of a later recoverManagedRuntime answers "nothing parked" over a
  snapshot it holds.

Witnesses: 245/245 CLI across the three hosted suites (2 new route
witnesses + updated session-code assertion + 2 recovery-suite flips),
QwenHostedHarnessConnectorTest 34/34 (marker witness),
HarnessCoordinatorTest 52/52 (session-busy witness; R6 budget witnesses
pin the load side as recorded). Docs updated in both languages.

* fix(managed-agent): settle the no-tool cancellation takeover instead of refusing forever (Arm B)

The round-6 real-stack wedge: a Turn with no Runtime work parked mid
model round, its Harness generation dead, cancelled — the no-tool arm of
the takeover branch answered the baseline retriable 409 on every
cancellation-side redispatch, and with the Turn still carrying its
submission mark, the retry budget never fired. Turn 2 stayed CANCELLING
forever and every later Turn met the same wall.

The fix keeps the wire honest and the settlement explicit:

- LoadHarnessSession carries `cancellationTakeover`, set ONLY by
  recoverManagedRuntime's cancellation arm. A plain passive re-attach
  shares the passive wire shape and deliberately never sets it — nothing
  may mint a canned CANCELLED record for a wait whose owner could still
  exist.
- With the signal present, the no-tool arm of the takeover branch
  settles the ownerless park into the daemon's journal itself
  (turn_result/cancelled, landing turn.settled); the writer fence on the
  very load is the safety boundary (it proves the producing generation
  can never write again). On write failure the arm keeps the baseline
  retriable 409. Without the signal the arm keeps its merge-baseline
  answers — retriable 409 on cancel, typed model_start decline on
  drive — so the collision guard holds by construction.

Witnesses: `settles the cancellation of an ownerless parked no-tool Turn
on the takeover load` (200 plain attach + durable settle tag + cancel
reads settled-at-tail as 204 + next prompt admits; the revert flips the
verdicts red) with the collision-guard passive-only rerun still at 409,
plus wire-side `carriesTheCancellationTakeoverFlagOnlyOnTheCancellationLoad`
asserting the flag travels only with the signal.

Verification: TS hosted suites 259/259, qwencode 32/32,
QwenHostedHarnessConnectorTest 34/34, HarnessCoordinatorTest 52/52,
typecheck, eslint, prettier. Design doc updated in both languages.

* fix(managed-agent): converge P1-1 (tool-config vs unpaid Runtime work) and P1-2 (stale approval copies)

Round-7 re-review of 275f934824 found two remaining wedge families
inside the cancellation settlements this PR introduced. Both converge on
durable proof, never on the stale checkpoint copy.

P1-1: a takeover cancellation must separate "the Session is configured
for tools" from "the Turn owes unpaid Runtime work". A files-profile
session whose owner died BEFORE its first tool call carried no unsettled
Runtime work (checkpointless or a bootstrap checkpoint naming no Turn) —
and the kernel's different-Turn check threw on the bootstrap's null
turnId, so the load answered the retriable 409 forever. The load route
now hoists that split ahead of the kernel: with the explicit
cancellationTakeover signal (and only with it — the collision guard
stays), any ownerless park without unpaid Runtime work settles into the
journal itself; work in flight follows the recovery-cancel report; the
no-signal shape keeps its baseline refusal unchanged.

P1-2: the durable action record — not the checkpoint's stale copy of the
approval — decides an owner-died approval. When the record says the wait
ended, the recovery kernel no longer maps it to the inapplicable plain
attach: expired/cancelled is transient (route refusal), decided is
advanced through its own durable gate ONLY on a drive load (a
cancellation never crosses it), and every other shape (still requested,
or undefined) keeps the plain attach a resolve route can pay. The
cancel/expired arrive by the same separation (settle with the signal;
decided is settleable with the signal too — a CANCELLING takeover does
not resume approved work no matter which side the user took).

Witnesses: `settles the cancellation of an ownerless parked Turn on the
takeover load (profile=null|FILE_PROFILE)` parameterized exactly as the
re-review demanded; three recovery-kernel witnesses for the cross-read
(resolves-inapplicable only while requested; throws for ended-without-a-
decision; drive declines checkpoint_blocked on an undrivable decided
wait); route-level `settles a cancellation takeover whose durable
approval already ended (state=cancelled|decided)`. Design doc updated in
both languages. Suites: hosted-harness-session 222→(+10 new),
hosted-runtime-recovery 36→(+3 new), issue-13328 33→, all green;
typecheck and eslint clean.

* chore(vscode-ide-companion): regenerate NOTICES for the merged dependency layout

* fix(managed-agent): read owed Runtime work off the work itself and pay coded plain-cancel refusals with the cancellation takeover (R9)

Round-9 real-stack measurement found the cancellation separation still
wedging on two families: every cancelled Turn after the first, on both
profiles, and every cancel row of P1-2 — where no load ever left the
coordinator, because a plain-cancel retry can never settle a parked
Turn and no redispatch was forthcoming.

- The park gate now reads owed work off the work itself: a checkpoint
  naming an earlier Turn whose tool items all settled and were consumed
  owes nothing to a cancelled Turn that never reached a tool call; and
  a no-tool Session cannot owe Runtime work by definition, so its
  cancellation arm settles unconditionally again (the round-8 behavior
  the gate had displaced onto a blocked restore basis).
- The admitted-cancel path turns a plain cancel's coded 409
  hosted_turn_recovery_required into an immediate cancellation
  takeover load over the same attachment (recoverManagedCancellation),
  and the connector sends it even with a healthy cached attachment —
  the shortcut would swallow it and the load would never leave the
  process. A recovered Runtime park it reports is cancelled through
  its checkpoint admission; the already-running stream lands the
  settle either way.

Witnesses: hosted-harness-session settles the second-Turn cancellation
park on both profiles (the round-9 rig witness adopted); the
coordinator drives the takeover load exactly on the coded refusal
(plain park and recovered park) and on no other answer; the connector
always sends the cancellation load over a cached attachment. Suites:
hosted 229, HarnessCoordinatorTest 55, QwenHostedHarnessConnectorTest
35, ManagedAgentServerIntegrationTest 29.

The decided-approval drive arm of P1-2 (rows where the user answers
allow or the approval expires unanswered) remains open: it needs the
plain-attached replacement to drive the parked Turn itself once the
durable wait ends, and lands as its own slice.

* fix(managed-agent): pay the cancellation signal identically on an attached re-answer (R9-2)

A cancellation takeover load the coordinator forces onto a Session
ALREADY attached in this daemon used to skip the entire settle
separation: the attached branch called the passive kernel directly,
which never advances a durable wait passively — so a park whose
approval ended after the attach (the round-10 allow, then cancel
repro) was thrown back as an unknown phase and refused retry-invariant,
while the first-load branch settled the identical shape. Hoist the
separation above the attachment split: the attached branch now reads
the signal too and runs the same no-tool arm, owed-work gate and
durable cross-read, settling through settleCancelledHarnessTurn into
the journal the already-running stream lands.

Witnesses parameterize both ended states: attach plain while the
durable approval says requested, end the wait after the attach with
the stale checkpoint copy untouched, then redrive with the cancel
signal — 200 with the redriven-load settle log, and the plain cancel
route reads settled-at-tail. Suites: hosted session 231, recovery 44.

* fix(managed-agent): close the durable wait before the cancelled terminal lands (R9-3)

settleCancelledHarnessTurn only wrote turn_result/cancelled into the
journal, and the record sink's turn.settled only advances next-turn
checkpoints at a model-start phase — so a cancelled Turn parked at an
approval wait ended in the journal while its checkpoint stayed at
await_action, and the Session's next prompt met
`ManagedHarnessBlockedError: await_action is not a model-start phase`
(the round-11 repro: old Turn completed cleanly, new Turn failed as
hosted_turn_failed before the model was ever called, closing 1 P1).

The cancelled settle now closes the ended wait first through the
wait's own durable gate (resolveDurableWait → model_output_committed,
a model-start family phase) whenever — and exactly when — the durable
approval already ended, mirroring the cross-read the caller's gate
performed. The decision remains the USER's and nothing resumes: the
Turn dies immediately after. Runs of requested waits or of in-flight
Runtime work keep the kernel's existing paths.

Witness (both answers): requested approval attaches plain on the
passive takeover, the record ends after the attach with the stale
copy untouched, and the attached cancellation takeover settles —
200 with the redriven-load settle log, plain cancel reads 204, and
the settle provably commits the model_output_committed boundary
ahead of the terminal record. Suites: hosted session 231, recovery
44; typecheck, prettier, eslint clean.

The unit suite cannot stage "approval-first park + literal owner
death" today (DELETE refuses active Turns, and abort unwinds the
wait cleanly), so the end-to-end relief-Turn acceptance stays with
the real-stack probe; the annex-level witnesses cover the settlement
payment and the wait-close commit.

* chore(vscode-ide-companion): regenerate NOTICES from a clean-room install

The previously committed file was generated in the long-lived
worktree whose pnpm layout had drifted ([email protected]
+ [email protected] + fdir/picomatch slot differences) even
though pnpm-lock.yaml is byte-identical to main. Regenerated in a
fresh worktree with a frozen-lockfile clean install: 662 deps, and
the output matches the lint lane's renewed file byte for byte, which
the polluted layout could not reproduce.

* fix(managed-agent): pace forced takeover loads per Turn and make the cancelled settle idempotent (R9-4)

Two round-10 verdict items:

1. The coded-409 escalation (R9-P1-2) ran on every ~500ms lease-renewal
   tick, so a Turn cancelled while its wait still owns a live durable
   record paid ~4 requests/s of daemon work forever (21 forced loads and
   20 cancels in the verdict's captured window). cancelAdmittedTurn now
   paces the forced cancellation takeover load per Turn (5s minimum
   interval): the cheap plain-cancel retry keeps carrying the wait, and
   a wait that ends inside the interval still settles on the next paced
   attempt — far below the approval timeout's 45s budget.

2. A settle that already landed was not idempotent: the coordinator's
   next forced load inside the stream's landing window met
   `event id turn:<id> is already committed` (3/3). The cancelled settle
   now re-reads the journal's own turn.settled for this prompt before
   writing, so the redriven load is a re-answer, never a second
   turn_result racing the event-id CAS.

Witnesses: three paced coded-refusals (t0 / t+500ms / t+5s) yield three
plain cancels but two forced loads (HarnessCoordinatorTest 60); the
round-9-2 witness pair now races a second identical load inside the
landing window and asserts one cancelled record and no
takeover_unavailable line (hosted suites 275).

* test(managed-agent): commit authority-valid deltas in the restore byte-budget test

How: holdsRestorePagesInsideThePerPageByteBudget now builds each dense
transaction from 210 well-formed message.delta lines (text at the
reader's 4096-byte cap, ~1 MB per transaction) plus the commit marker,
through a shared TurnEventLines composer, and asserts that revisions
1..9 fit the 8 MiB page budget while a tenth does not. The reader replay
IT now also replays that delta shape. The class turns off MockMvc
print-on-failure.

Why: #13348 added the test with placeholder lines ({"subtype":"event_v1"}
plus a padded commit_v1 line), and #13355 (merged later, never run
against it) made the commit gate refuse any non-event line inside the
event range, so every commit after genesis answered 409 and main's SDK
Java lanes went red. The failure was masked as timeouts: Spring Boot
printed the failed test's ~1.3 MB request body as one log line, which
stalled the Actions log pipeline for ~45 min, so the Hosted Verify and
O4 steps blew their ceilings and the MariaDB lane hit its job timeout.

Test: ManagedSessionStoreIntegrationTest and ManagedExtensionRecordStoreTest
green; full managed-agent-server surefire (1077) and checkstyle green;
HostedCommittedEventLineReplayIT green through the real TS reader and
red when the delta text exceeds 4096 bytes; dropping the page byte
budget in ManagedSessionStore fails the test (10 rows instead of 9)
with no log line over 400 chars.

* fix(managed-agent): propagate the settle's own wait-check fault instead of degrading it to a successful cancel (R9-5)

The cancellation settle's authorization re-read was wrapped in
`.catch(() => undefined)`, so a transient store fault surfacing
between the caller's gate and the settle's own wait-check degraded
into 'nothing to close' — the cancelled terminal landed over a wait
the fault hid, and the Session's next prompt then found the same
obsolete checkpoint that family wedges on (the round-11 fault
injection at this exact boundary: authorization flips
runnable → blocked/missing_state → runnable once, the settle still
mints turn_result, and the run dies on `await_action is not a
model-start phase`).

A read failure is a retriable store fault, not 'nothing to close': it
now propagates into the caller's try-catch, which keeps the baseline
retriable refusal — the fault's own retry ladder owns the retry, and
the replayed shape then settles identically.

Witness mirrors the probe: the wait-gate reads fine, the settle's own
check faults once — load refuses hosted_turn_recovery_required with no
Cancelled record and an honest 409 on the plain cancel, then the same
shape settles after the fault clears (settle log, the
model_output_committed boundary, plain cancel 204). Suites 276/276
(hosted session 232, recovery 44), typecheck/prettier/eslint clean.

* fix(managed-agent): refuse the cancelled settle on fault-shaped authorization verdicts too (R9-5')

R9-5 propagated THROWN authorization faults into the caller's retriable
refusal, but the authority never throws the same class of failure: the
real path takes a retry-exhausted ManagedSessionStoreTransportError and
converts it in place into `{status: 'blocked', reason: 'missing_state'}`.
From there the cancelled settle still marched on — it skipped the wait
advance and wrote the cancelled terminal anyway, spending the Session's
next prompt on the obsolete `await_action` checkpoint the verdict hid
(the round-11 verdict at `964b87a50a`, reproduced with the production
error type: 200 on the takeover, the terminal lands, and the next turn
dies `await_action is not a model-start phase`).

Fault-shaped authorizations are never "nothing to close":
`missing_state` (the TransportError conversion), `opaque_state` and
`invalid_state` now all answer the same retriable refusal — the settle
refuses rather than minting what it cannot prove. `missing_checkpoint`
stays payable on purpose: it is the Arm B park's honest form (a no-tool
Session with history provably has no checkpoint) and its settle is the
already-verified behaviour.

The witness covers both faces of the store fault
(`it.each(['exception', 'missing_state'])`): same park, same stage,
either a thrown fault or a folded `blocked/missing_state` — the load
refuses with zero settle and an honest plain cancel 409, and the replay
settles the moment the fault clears. Suites 277/277 (hosted session
233, recovery 44), typecheck/prettier/eslint clean; design docs note
the boundary in both languages.

* docs(design): split the R9-5' verdict paragraph into its own bullet

The R9-5' text rode mid-bullet after R9-5's run-on, which Prettier
then flattened to column zero — breaking the list structure the lint
lane rejects outright. Both languages carry the split now, and the
lint-facing check passes again.
2026-10-07 03:38:25 +00:00
Bob.qwencodeandgithub-actions[bot] 8d864a9f21 chore(release): v0.25.0 (#13448)
* chore(release): v0.25.0

* docs(changelog): sync for v0.25.0

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-05 10:07:16 +00:00
Shaojin WenandQwen-Coder aca4d03c7e feat(managed-agent): Add dev:managed-agent one-shot dev launcher (#13218)
* feat(managed-agent): Add dev:managed-agent one-shot dev launcher

Developing against the Managed dual-path WebShell previously meant
starting an ordinary daemon, a private Hosted Harness, and the Spring
control plane by hand, keeping their ports, tokens, and capability
digest consistent by copy-paste.

The new launcher mirrors daemon-dev: it serves the daemon and the
Hosted Harness from TypeScript source with rotating tokens and digest,
writes the Spring wiring to the git-ignored (0600)
.qwen/managed-agent-dev/spring.env, waits for /actuator/health on the
Java side (--skip-java-wait bypasses), then opens the WebShell Vite dev
server with the Managed panel selected. Ports auto-increment when busy,
and any child exiting aborts the group so the stack cannot half-start.

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

* fix(managed-agent): Harden dev:managed-agent against review findings

Round-1 review found 4 Critical and 14 Suggestion-level issues in the
launcher; this commit addresses all 18:

- Windows teardown now uses taskkill /T so the dev.js grandchild that
  owns the sockets cannot be orphaned (R1-1).
- Equal --daemon-port/--harness-port is rejected, the daemon probe
  excludes an explicit harness port, and both health gates assert
  responder identity through /capabilities (the harness must present
  this run's capability digest), so a busy-port bump can no longer be
  reported as a healthy mis-wired stack (R1-2).
- The two serve children boot in serialized stages: dev.js destructively
  re-stages the shared browser-use runtime on every boot, and the
  parallel spawns raced on it (R1-3).
- The token-bearing open path travels via QWEN_WEB_SHELL_OPEN_PATH,
  consumed as server.open in the web-shell Vite config, instead of a
  --open argv that npm echoed and cmd.exe would split on '&' (R1-4,
  R1-17).
- spring.env moved to the OS temp directory (mode 0600) so the harness
  bearer token no longer sits inside the served workspace; a PowerShell
  sibling is written for Windows, a warning fires when a previous run's
  file (and a possibly stale Spring) exists, and the Spring recipe now
  carries the datasource exports and one-time sibling installs in both
  README and the startup banner (R1-5, R1-6, R1-11, R1-12, R1-14).
- --tenant is validated against Spring's own pattern, waitForHttpOk
  carries the last HTTP status/error into its timeout, a Java timeout
  no longer tears down the healthy local stack, signal handlers live
  inside the entry guard, and the isMain guard is symlink-safe (R1-8,
  R1-9, R1-10, R1-16, R1-18).
- Exported builders (kill plan, harness wiring, serve stages, web
  launch, URL) with mutation-witness tests pin the contracts the first
  round showed unpinned (R1-7, R1-13, R1-15).

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

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

* fix(managed-agent): Probe the web port on Vite's bind family

Vite binds 'localhost', which resolves to ::1 only on IPv6-first hosts,
but the web-port probe listened on 127.0.0.1 and called a held port
free — observed in a two-launcher dry run where the second launcher's
probe passed and Vite silently bumped past the probed value (the
printed URL then names the wrong port; the TTY URL also used the IPv4
spelling that cannot connect to an ::1-only bind). The probe now takes
a host parameter and the web probe uses 'localhost', matching Vite's
own bind attempt by construction; the printed URL uses the localhost
spelling. A unit witness binds a server on 'localhost' and requires
the probe to walk past it.

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

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

* fix(managed-agent): Close round-2 review findings on dev:managed-agent

The second review round (2 Critical, 8 Suggestions) found the round-1
remedies themselves creating new edges; this commit addresses all ten:

- The env-file write is now fail-closed: the directory is keyed by uid
  and a hash of the checkout root (multi-user and multi-worktree runs no
  longer collide or cross-truncate), created 0700, verified owned, each
  target lstat-checked and refused when it is not our regular file (a
  planted symlink is bounced, its victim untouched), and 0600 is applied
  by chmod so it holds on the overwrite path too. POSIX-only
  enforcement: NTFS owns the boundary on Windows, where stat mode masks
  and chmod read-only would have broken the launcher outright (R1-11).
- Teardown now covers SIGHUP plus a synchronous exit hook, so a closed
  terminal tab or dropped SSH session no longer orphans the credentialed
  serves that detached:true would strand (R2-1); registrations stay
  inside the entry guard and are pinned via TEARDOWN_SIGNALS.
- Explicitly pinned ports are probed and rejected when busy, because
  qwen serve bumps on EADDRINUSE regardless, which previously meant a
  60-second misleading timeout (R2-2); Vite gets --strictPort and a
  pinned --host so it fails closed instead of drifting past the probed
  port (R2-3).
- waitForHttpOk captures err.cause.code (ECONNREFUSED now visible
  through undici's generic 'fetch failed') and clears stale accumulators
  so a fresher error supersedes an older response (R1-9 residual).
- The Java gate applies its siblings' identity logic through
  springHealthExpectBody instead of certifying any 2xx (R2-6).
- The PowerShell recipe line is uncommented and self-enabling
  (Set-ExecutionPolicy -Scope Process), emitted by an exported
  buildSpringRecipeLines that also carries the database/user creation
  step matching the reference e2e, mirrored into the README (R1-14,
  recipe gap).
- Effectful decisions moved into exported, injectable helpers
  (executeKillPlan, runServeStages, ensurePortFree) with interleaving
  and argv witnesses, and nine loose oracles tightened (rotation, ::1
  family fixture, daemon argv, launch env/argv, sharper retry message,
  the vite env-consumer case in web-shell, the Authorization header, the
  harness port in both base URLs, ambient-credential collision) (R2-4,
  R2-5).

Dry-run witnesses: staged healthy with digest pairing, recipe lines and
the env-file layout (0700 dir, 0600 file, no ps1 on POSIX) verified,
pinned-busy fails in milliseconds with exit 1, and a live SIGHUP drains
daemon, harness and Vite with no orphaned listeners.

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

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

* fix(managed-agent): Skip the web-shell spawn once teardown has begun

Self-audit after the round-2 fixes found two edges: a child dying
during the Java wait now lands in the non-fatal continue branch, which
would print the Managed URL and spawn Vite into a group teardown was
already reaping (the exit hook caught it, but the output misleads); and
killChild's re-entrancy comment overclaimed a no-op second pass — the
group/taskkill plans do not set child.killed, so a second pass is a
redundant signal to a dying tree, which is safe but not a no-op.

Guard the spawn on shuttingDown and correct the comment. No behavior
change on the happy paths; the script suite stays at 60/60.

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

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

* fix(managed-agent): Close round-3 review findings on dev:managed-agent

The third review round (4 Critical, 18 Suggestions), including three
defects the round-2 fixes introduced, now addressed together with the
review's batching recommendation:

- shutdown() counted only signal-dead children (the >>continue<<
  predicate read 'signalCode === null'), so Ctrl-C/SIGHUP launched
  process.exit immediately, orphaned every live server, and a sole
  signal-killed child exited 0. The probe now reads isLiveChild, which
  the suite pins in both directions (R2-1).
- The credential writer's checks resolved through symlinks: a planted
  link at the directory walked the lstat refusal, and a dangling link
  at a target skipped it entirely. The writer now lstats everything and
  refuses any symlink up front (R1-11), with planted-link victim-
  untouched cases.
- Teardown now hears SIGQUIT (Ctrl-\) and escalates to SIGKILL after
  the 5 s grace instead of just exiting (R2-1/R3-1); the exit-hook
  comment narrows to what an in-process hook can promise instead of
  claiming coverage for unhandled signals and hard aborts.
- Pinned-port checks moved from launcher start to each stage's own
  spawn step, so a port taken between the daemon's 60 s boot and the
  harness's spawn can no longer be silently bumped past (R2-2); the
  web port resolves at spawn time through an exported launch seam, so
  a squatter in the Java window cannot turn --strictPort into a full
  teardown (R2-3, R3-2).
- The Java gate's teardown path is silent: no 'stays up' continuation
  line while children are reaping (R3-4), extracted into a runJavaGate
  seam with an injected health wait so the path is test-reachable.
- healthFailureAction left only a literal-'continue' call site; the
  dead policy table, the unreachable abort branch and its describe are
  removed per the review's KISS option (R3-5).
- The MySQL recipe grants the 'qwen' user at both 'localhost' and
  '127.0.0.1' (official images enable skip-name-resolve) with a
  container-topology note, and a pairing test ties the granted
  hosts/password to the exported SPRING_DATASOURCE_* literals (R2-7,
  R3-10).
- Test-oracle batch (R3-3, R3-6..R3-9, R3-14, R3-15): default-runner
  witness for the kill plan, detached flag via an exported
  spawnDevProcess, first-run mkdir 0700 gate, stalled-responder
  rejection, both uid arms, stages[1].args pin, : prefix pins for
  the ps1 BASE_URL/digest lines, the printed ps1 path tied to the
  writer's filename, '503' replaced by the port-safe 'last response'
  suffix assertion with an in-range budget and a missing-finally close,
  the ephemeral-port-safe family fixture (property, not exact port),
  usable-on-Windows branch-shape cases decoupled from POSIX mode
  assertions, and parsed-actionable-line checks for the PowerShell
  recipe. ensurePortFree's unused probeHost parameter is gone (R3-13),
  and the env-transport comments now record the browser-hop argv the
  token still rides (R3-12).

78/78 in the script suite; live dry run healthy (dual-user recipe,
argv clean), and a real SIGQUIT drains daemon, harness and Vite with
no orphaned ports.

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

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

* fix(managed-agent): Wire the Session Store every Managed Turn needs

Real-stack verification on the PR thread found F1: since #12754 the
Hosted Harness answers 400 invalid_managed_session_store to any POST
/session without a managedSessionStore descriptor, and Spring only
sends one when its HTTP Session Store is enabled — so the launcher's
recipe brought up a perfectly healthy stack in which every Managed
Turn failed with hosted_harness_rejected. The stub-Spring dry-run and
env-name checks in earlier rounds never drove a real Turn through the
wiring. Adopts the reviewed candidate patch (wenshao assets-pr13218
@ ef748da, measured 3/3 Turns against a freshly created DB) and adds
the remaining notes:

- spring.env/ps1 now also carry QWEN_MANAGED_AGENT_SESSION_STORE_*
  (enabled, base URL = --java-url since the Harness calls Spring back,
  and a dev workspace id), so the recipe by itself produces a usable
  Session Store (F1).
- SERVER_PORT is emitted from --java-url for plain-http loopback URLs,
  so a non-default --java-url no longer leaves Spring on 8080 while
  the launcher waits elsewhere (N2).
- Thrown messages no longer carry the '[managed-agent-dev]' prefix that
  main()'s catch prepends once (N3).
- '--java-url' is trimmed before validation, so surrounding whitespace
  can't pass the checks and then fail every health attempt (N5).
- The README states the hosted_harness_rejected diagnostic mapping
  (Harness 400 = Session Store not loaded; 401 = stale Spring) (N1).
- The first-run 0700/0600 writer case is re-gated for POSIX semantics
  (R4-1); content/branch-shape cases keep running everywhere.

The supervisor data point from N4 (SIGKILL of the launcher) stays a
deliberate follow-up on the launcher family, unchanged.

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

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

* fix(managed-agent): Gate the POSIX-arm writer case on the host, not the flag

The round-5 review found the split's new case reproducing R4-1's exact
failure on the Windows lane one level up: 'writes the env content and no
ps1 sibling on the POSIX arm' injected isWinPlatform: false unguarded, so
on a real Windows runner the production 0700 gate reads the synthesized
777 directory mode and writeSpringEnvFiles throws before either
assertion — the mechanism and message R4-1 described, on the case I
believed the split had made portable. It now gates on the host like its
six siblings.

Also adopts the round-3 verification's wording note: the two sibling
installs are labeled once per clone, and re-run after pulling changes
to qwencode or runtime-broker, since a snapshot install before a main
pull leaves spring-boot:run compiling against stale artifacts (the
banner builder and the README recipe say the same thing either way).

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

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

* fix(managed-agent): Close the actionable slice of the round-4 deferred list

Five items from the round-4 review body's deferred Suggestions were
covered here because each is a small consistency hole in the seam this
PR just built:

- main() re-checks pinned ports at t0 before writeSpringEnvFiles runs,
  so a busy --daemon-port/--harness-port no longer rotates credentials
  and prints the banner before failing at stage spawn the same run
  (R4-14); the per-stage recheck stays for the boot window it was added
  for.
- launchWebShell re-consults the teardown flag after resolveWebPort, so
  a launcher dying mid-probe no longer prints a Managed URL for a group
  the exit hook is about to reap (R4-10), with a witness that flips the
  flag mid-resolve.
- findWebPort is now held against a stand-in bound to this host's
  localhost resolution at DEFAULT_WEB_PORT, so the family walk is
  operational rather than vacuous (R4-17).
- installTeardownHandlers' captured handlers are asserted to be
  functions, not only named functions (R4-19).
- The vi.mock child-process fixture spawns fake children with
  pid=undefined, so teardown code can never signal a real process
  through the registry it is pushed into (R4-20).

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-04 14:41:46 +00:00