mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
main
1281
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f7885f9fa9 | fix(verify-pr): bind reports to their round snapshot (#15746) | ||
|
|
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. |
||
|
|
7fc327825e |
fix(ci): exempt surefire-bound IT classes from the failsafe roster (#15830)
* fix(ci): exempt surefire-bound IT classes from the failsafe roster The non-hosted roster required every *IT class to appear in target/failsafe-reports, but packages/managed-agent-server binds ManagedAgentPackagingIT to the artifact-packaging surefire execution and excludes it from the mysql-integration failsafe (#15718). A class that is forbidden to run under failsafe therefore could never satisfy the roster: Maven reported BUILD SUCCESS with the class's 3 tests passing, then the job failed on 'ran no test in this job'. It reproduces on main with no PR code present (#15820 item 1) and turned the full tier red for every managed-agent PR. Name the surefire-bound classes per module, skip them in the roster, and fail the check if the module's pom stops binding such a name to both a surefire include and a failsafe exclude, so the exemption cannot drift away from the pom it describes. * test(managed-agent): use managed JDBC for message fixture Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
b969de851a |
ci(java): remove MariaDB full integration lane (#15827)
Keep the complete MySQL 8.4 integration profile and per-fork isolation. Retain JDBC precision and CAS coverage on the supported database and align workflow guards and documentation. Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
3308bd2fda |
fix(release): unblock Managed runtime 0.26.3 backfill (#15825)
* fix(release): repair managed runtime 0.26.3 verification Co-authored-by: Qwen-Coder <[email protected]> * style(release): format managed runtime regression tests Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
9f79669a9d |
test(core,cli): stop flat 30s budgets shadowing the raised ECS timeout ceiling (#15801)
* test(core,cli): stop flat 30s budgets shadowing the raised ECS timeout ceiling
Both vitest configs raise testTimeout/hookTimeout to 60s on the shared
ecs-qwen-* pool and fall back to 15s elsewhere. Three suites instead set a
flat per-file or per-describe `vi.setConfig({ testTimeout: 30000, ... })`,
which overrides the config unconditionally, so on the pool the raised 60s
ceiling never applies and a slow-but-healthy run is killed at 30s.
Reshape each clamp to the repo's existing `isSlowTestHost() ? slow : fast`
idiom instead of deleting it. Deleting a flat 30s clamp would drop the
off-pool budget to the config's 15s floor, a regression for fork PRs and the
macOS/Windows runners, so the fast arm stays at a non-trivial 30s everywhere:
off-pool is unchanged at 30s and the ECS pool rises from 30s to 60s.
In server-default-bridge-wiring.test.ts the file-level clamp already used the
idiom but with a 15s fast arm, and a describe-scoped 30s clamp shadowed it.
Raise the fast arm to 30s and drop the shadowing clamp plus its now-orphaned
comment, leaving the file-level setConfig as the single source of truth.
Refs #10922
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmvs4muntns
* test(managed-agent): qualify the api-contract fixture's JdbcTemplate
Same latent wiring as #15809 fixes on main: after the managed template became
defaultCandidate=false (#15718), this unqualified autowire resolves to the
plain template in the full CI context, and H2 refuses String-vs-VARBINARY
comparisons on the identity columns. Pin the fixture to the managed template
as in #15809; identical hunk, keeps both trees agreeing when this branch next
merges main.
* test(core): sync the slow-test-host threshold with its cli mirror
packages/core/src/test-utils/slow-test-host.ts documents that it mirrors
packages/cli/src/test-utils/slow-test-host.ts and that both must stay in
sync, but its fallback required the full available parallelism while the
cli copy splits at half. The cli side explains why 0.5 is load-bearing:
the review-address verification gate re-runs package tests through an
`env -i` child that drops RUNNER_NAME and clamps vitest to
--maxWorkers=25%, so the gate's own workers never lift the load average
to the full core count.
The two packages/core call sites this PR adds therefore resolved the
weaker predicate, and in exactly the gate the docstring is about the
raised budget this PR exists to grant never fires. Add the two
assertions the cli copy already carries so the sync claim is pinned.
Verified discriminating rather than decorative: with the threshold left
unchanged the two new rows fail (40>=64 false, 32>=64 false) while the
three existing rows stay green; after the change all five pass.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvsi7j9czz
* test(core,cli): pin the slow-test-host threshold and its mirrored copy
R3-1: port the cli copy's contention rationale into the core docstring, which
still called the predicate "measured saturation" while shipping a `* 0.5`
multiplier that fires at half the core count.
R3-2: add a parity guard beside the existing permission-bit probe guard. The
two slow-test-host copies are not byte-identical (only the cli copy also
exports slowHostBudgetMs), so it compares the shared isSlowTestHost body.
R3-3: add an 8-core case to both mirrored test files. Every existing case
mocks availableParallelism to 64, so the multiplier was indistinguishable
from a hardcoded 32.
Mutation witnesses, each mutant applied and run separately then restored:
- threshold hardcoded to 32 in both copies -> the new 8-core case fails in
core (1 failed | 5 passed) and in cli (1 failed | 9 passed) while the five
pre-existing cases stay green; the parity guard stays green because both
copies moved together.
- `* 0.5` dropped from the core copy only -> the parity guard fails.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvsv2hwh0e
---------
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
|
||
|
|
ca204559a7 |
feat(verify-pr): make "re-verify after update" a follow-up trigger (#15816)
* feat(verify-pr): make "re-verify after update" a follow-up trigger After a local verification round, a maintainer who sees the PR move says "有更新了,重新验证". That phrase was not a documented trigger: the skill could miss it entirely or start a fresh round, losing the previous-finding status table and the delta classification. The local follow-up lookup only searched PR comments, so a round that was never published was invisible and silently degraded to a fresh round, and nothing checked whether the head had actually moved before spending a full round. Add the re-verification phrases to the skill description, and a manual re-verification contract: resolve the PR from the session or the newest local artifact dir (ask when ambiguous), take the previous verified head from the newest substantive round across local artifacts and published comments, and fetch the current head before scoping. An unchanged head gets an answer, not a round — comments and CI results are data, the same rule the watch gate applies. A moved head runs the existing follow-up protocol. Coordinates inherit from the previous round; publishing stays a per-invocation request. A completed manual round records its head in the watch dedup file so an installed watch does not re-fire for it. The local follow-up lookup now searches both sources and takes the newest substantive round, so unpublished rounds participate in the protocol. * fix(verify-pr): register a manual round in the watch state before it starts The manual re-verification contract recorded its head in the shared watch dedup file only after the round completed, covering just the watch-first ordering. With the watch state still on the previous head, a manual round running in another session registered nothing while it worked, so the next watch fire read the stale head and started a second round for the same head. Make the check-and-register a step of its own ahead of running the round: read the shared file, defer when it already records the head or marks a round in flight, otherwise write the head as in-flight before starting — the same write-before-start rule the watch prompt follows — and record it as verified once the round completes. Both orderings now dedup through the one file. Pin the contract, including the registration-before-round ordering, in the skill-text suite so the race cannot return unnoticed. |
||
|
|
35fd1f44ff |
feat(ci): bound autofix takeover with a round budget (#15388)
* feat(ci): bound autofix takeover with a round budget
`@qwen-code /takeover` engages a persistent loop capped at 100 rounds,
so a maintainer who wants one bounded pass has to spend the whole cap
in a single Critical-only round and accept a "reached its round cap"
pause, or watch the run and stop it by hand.
Add `<cmd> for N` (1-99), which engages takeover and records a
window-scoped round budget on the acknowledge comment. The scan lowers
the effective cap to N for that window; once the budget is spent, a
human-authored PR is released — one bilingual note, the label removed,
no needs-human escalation — while a bot-authored PR keeps the paused
path, since releasing it would hand the PR back to standard management.
Failed, timed-out and crashed rounds consume budget, so N means N
attempts.
The workflow sits against its 470 KB start-runs gate, so the duplicated
env commentary moves into the rationale record (af-160), leaving a
one-line summary plus a pointer per constant.
* fix(ci): count attempts and keep breaker stops in the takeover budget
Three review findings on the round budget:
- A no-op round writes its eval marker with the round UNCHANGED, so a
budget riding on ROUND never spent for a PR whose rounds kept finding
nothing — with `for 1` it stayed managed indefinitely. Spend is now
the number of eval markers in the window: one per attempt, no-op
included.
- The same reading separates a breaker stop from real exhaustion: a
breaker stamps its terminal marker at round=MAX_ROUNDS after far
fewer attempts, and that sentinel jump adds no marker, so a
twenty-round window that hit five failures stays paused and escalated
instead of being released with a "nothing is waiting" note.
- A queued conflict round carries the OLD cap, and conflicts were
exempt from the superseded-window check, so after a smaller re-arm it
could run past the new limit. A budgeted live window makes it stale
too; the next scan re-selects the conflict under the new window.
The workflow sits on its 470 KB gate, so the env commentary these reads
pushed past the limit moves into the rationale record the same way as
af-160.
* fix(ci): keep the budget bounded across audit re-arms and queued rounds
Two review findings on the round budget:
- The growth audit appends `<!-- autofix-rearm -->` to open a fresh
window for its own round accounting. A window opened that way carries
no budget marker, so an automatic re-arm turned a `for 2` engagement
back into a 100-round one with no maintainer asking. The report script
now skips that automatic re-arm while the current window is budgeted,
so the audit round stays inside the budget it was given. Both copies
of the helper (script and the failure-path inline one) change together,
which the existing equality test pins.
- A queued round can outlive the sibling that spent the window's last
attempt: a no-op leaves ROUND at 0 and the window key unchanged, so
neither the superseded-window check nor the round cap discarded it and
with fresh feedback it ran one more attempt. `review-address` now
recounts the window's eval markers and discards on a spent budget.
The new tests also caught a real defect in the first draft of the
re-arm guard: a `[[ ... ]] && echo` as the function's last command
returned 1 under `set -e` and would have aborted the report.
* fix(ci): scope the budget to the engagement, not the round window
Third review round. Three correctness findings, all real:
- Suppressing the growth-audit re-arm (the previous round's fix) also
froze the growth baseline: it is the only automatic mover of that key,
so after one over-budget round every later round recomputed growth
against the pinned baseline and went Critical-only. The re-arm is
unconditional again. The budget survives it because budget and spend
are now read per ENGAGEMENT: a maintainer command opens one, while an
automatic re-arm (whose comment also carries that round's eval marker)
does not.
- The attempt census was a raw marker count with no identity, so a
GitHub "Re-run failed jobs" re-posted a round's report and charged
that round twice — releasing the PR early. Attempts are counted as
distinct (ts, round) markers in both the scan and prepare.
- The release fired on a spent budget alone, so a circuit breaker that
tripped exactly on the last budgeted attempt was reported as a
finished engagement with "nothing here is waiting on a human". A
trailing un-evaluated-failure streak of at least TIMEOUT_WINDOW_CAP
now skips the release and lands on the paused path with
autofix/needs-human; a provenance-verified forced dispatch likewise
falls through to its own refusal instead of being preempted.
Design decision 7, its Chinese twin and the operator guide now describe
the carry-forward instead of the suppression.
* fix(ci): end the budget with the engagement that opened it
A spent budget marker outlived the label it belonged to. `stop` posts a
`takeover-ack released` ack, which is not one of the window-key anchors,
so the marker kept reading back: on a bot-authored PR the scan re-entered
the cap branch on every tick and re-applied `autofix/needs-human` at N/N,
and a hand-removed label had the same effect. The budget is now honoured
only while `autofix/takeover` is present, which ends it in both cases.
The regression harnesses now set HAS_TAKEOVER for the managed case.
* fix(ci): repair the two checks the last commit broke
Both failures were introduced by the label-gate commit and are now fixed:
- The pointer suite stopped recognising `autofix.yml`'s env pointers
because they had been shortened to `→ af-NNN`. Restored the full
`→ qwen-autofix.md#af-NNN` form the suite matches, so every section is
pointed at again.
- `HAS_TAKEOVER` was read unguarded while a pre-existing replay harness
runs its shell with `set -u`, which aborted that harness with
"unbound variable". The read is `${HAS_TAKEOVER:-false}` now: callers
that never set it keep the pre-budget behaviour.
Verified locally: 72 focused workflow/size cases pass; the file is
469,982 bytes, under the 470,000 gate.
* fix(ci): attribute budget attempts to the window that dispatched them
The engagement census filtered markers by publication time (`.at >=
floor`), so a round dispatched under the previous engagement and posted
after a fresh `for N` ack was charged to the new allowance: `for 1` could
be spent before its own work ran. Markers carry the window they ran
under, so the census now selects `.win >= floor` (and still skips the
legacy `win=none` markers), which associates an attempt with its
originating engagement while keeping the automatic child windows of that
engagement inside it.
Verified: 73 focused workflow/size cases pass; 469,943 bytes.
* test(ci): pin the budget's lifetime to the takeover label
Audit finding: the label gate that ends a budget when `autofix/takeover`
goes away had no regression. The budget-read harness now takes the label
state as a parameter, and a fixture asserts that the same anchor yields
the takeover cap (100) once the label is absent — the shape that used to
re-park a bot-authored PR at N/N with autofix/needs-human on every scan.
* fix(ci): read the carried budget per engagement in prepare too
`review-address` still resolved `LIVE_ROUND_BUDGET` from the latest growth
window, so after an audit re-arm a queued task read `live_budget=0` and
the attempt cap could not see the engagement's spend. The leg now uses
the scan's engagement scope: a maintainer command opens one, an automatic
re-arm (whose comment also carries an eval marker) does not. That also
makes the superseded-conflict clause see the real budget instead of the
audit window's empty one.
* docs(ci): align the budget record with what the loop now does
Three claims had drifted from the implementation: the effective-cap
bullet said nothing else reads a budget (prepare and the report script
both do), the growth-audit paragraph still described the suppressed
re-arm (it is unconditional; budget and spend are read per engagement),
and the queued-round paragraph said a no-op leaves the round "at 0"
(it leaves it UNCHANGED).
* fix(ci): let the shepherd see the parameterized takeover commands
The auto-release lever builds its resume candidates by exact body match
against the bare takeover and retry commands, so a paused PR re-engaged
with `@qwen-code /takeover for 2` (or `from N`) matched neither and the
release could fire after the pause window despite a fresh engagement.
The predicate now also accepts the two parameterized forms as prefixes of
the configured command, interpolating TAKEOVER_COMMAND rather than adding
a literal.
* docs(ci): name every site that carries budget logic in af-159
The site index listed the three original steps while the prepare leg and the report script's guard read and enforce the same budget.
* docs(ci): name each af-160 entry by the constant it records
The section's own preamble promises that every entry names the constant it
documents; none did, so a maintainer following an env pointer had to read
all eight to find the one they came for. Each header now leads with its
constant. The command entry also drops its "ONE parameterized form" claim,
which this PR's second form falsified, and names both.
* docs(ci): nest the seed sections under their own heading
The guide was retitled for both forms, but the seed's sections stayed at H2, so a document-wide reading of 'does not shorten the round cap' sat directly above the form that does. The seed's sections are now nested and its Picking N no longer collides with the budget one's anchor.
* docs(ci): complete the budget record's inventory and risk list
The files-affected inventory omitted the report script and the prepare
leg's reads, and the risk list only covered the DELETE-failure direction.
Both now name the release's actual shape: the label gate ends the budget,
and the note-then-DELETE order means a failed note still releases.
* docs(ci): bring the Chinese budget record in line with the English one
The previous commit's script aborted after writing only the English file, so the pair disagreed. This lands the same inventory and risk additions in zh-CN.
* fix(ci): keep the budget release note ahead of the label, and read dispatch identity correctly
The release note is the only terminal comment the budget path ever posts;
once the label is gone the PR is in neither selection pool, so a failed post
could never be retried. Skip the DELETE when the note fails, so the next scan
retries both.
The release gate keyed on FORCED_PR being empty, but route publishes pr_number
for every targeted event, not just workflow_dispatch. A pull_request or
pull_request_review run that reached the spend-to-release window therefore fell
through to the round-cap copy and applied autofix/needs-human to a PR that hit
no cap. Test the dispatch identity the way the sibling refusal gate does.
* docs(ci): correct the budget record against the shipped mechanism
The record and the guide disagreed with the code in four ways, all of them in
the direction that misleads whoever debugs it next:
- the budget is read per engagement (the eval-marker exclusion is what tells a
command from the automatic re-arm), not by window-key equality;
- the re-arm is not the only thing that re-anchors the growth baseline — a
base-branch update and the scan's heal ack do too, so the exclusivity claim
is gone while the per-engagement conclusion stays;
- a cancelled run that reaches the finalizer DOES spend an attempt, so the
error direction is an early release, not a late one;
- a crash, timeout or breaker stamps the cap, ending a budgeted window at once
rather than spending the rest of the budget.
Also: the label — not the command — is what engages management (a hand-applied
label cannot be bounded), the 470 KB number is this repo's gate rather than
GitHub's limit, /retry does nothing on a released human PR, and the queued
conflict discard is this change's new clause rather than the window-key check.
af-160 gains the TRUSTED_ASSOC entry the prose move deleted, af-007 names both
parameterized readers, and the guide index advertises both forms.
* docs(ci): label the takeover input by its scope, not by a window
* test(ci): stop calling the seed the one parameterized form
* fix(ci): read prepare's budget only while the takeover label is present
The scan zeroes a spent budget once the label is gone, but prepare still read
it from the comment history alone. A bot-authored PR whose label was removed
after its budget ran out was therefore dispatched by the scan and discarded by
prepare on every tick, with no marker written - a livelock rather than a
release. Gate the read the same way the scan does, on the same live label
re-read prepare already performs.
* fix(ci): count attempts per run, park breaker stops, keep a spent budget
The budget census keyed on `ts|round`, so two executions reporting the same
watermark and round — the conflict no-op, which re-dispatches every tick —
collapsed into one attempt and a for-N engagement could never spend. The eval
marker now carries `run=GITHUB_RUN_ID`, the dedup identity the growth marker
already uses, and both censuses key on it with `ts|round` as the legacy
fallback, so a re-run of one run still charges once.
A breaker stop also spent the budget on its way out. The consecutive-failure
breaker jumps the round to MAX_ROUNDS with a real timestamp, the cumulative
timeout breaker counts timeouts across the window with pushes between them, and
the convergence breaker only pushes — none leaves the trailing sentinel run the
release gate tested. The gate now reads BUDGET_HALTED: a sentinel anywhere in
the engagement, a round past ROUND_START + attempts, or a convergence-break
notice newer than the window key all park the PR instead of releasing it.
Finally, the shepherd's three-day auto-release strips the label from a paused
bot PR. The budget used to die with it, dropping the effective cap back to 10
and re-selecting the PR for rounds N+1..10. A spent budget now survives for a
bot-authored engagement; an unspent one still dies with the label, and a human
release keeps no cap at all.
* test(ci): pin the budget's lower-only clamp and both census filters
The cap clamp's -lt half was exercised by no fixture: the replay hardcoded
EFF_MAX_ROUNDS=100 and every budget is <=99, so deleting the comparison left
the suite green while a stale marker could raise a strict cap. Parameterize the
incoming cap and replay a 20-round marker against a 10-round cap.
The census filters were equally unpinned: BUDGET_FLOOR was frozen at '' and no
fixture carried win=none, so dropping either clause stayed green. jq compares
window keys as strings, so 'none' sorts above every ISO timestamp and only the
win != none clause keeps pre-takeover rounds out. Thread the floor through and
add the two cases plus an in-engagement one.
* chore(ci): re-baseline after merging main
* fix(ci): describe a budget stop as a budget, and record the release only once it happens
Two operator-facing copies lied at the boundary. A bot-authored PR whose
BUDGET_SPENT brought it there fell through to the round-cap body and printed
'hit the round cap (0/3)' - a round count of zero against a cap of three, on a
PR that never reached a cap. It now gets its own line and fleet state naming the
attempts used.
The fleet row claiming a release was written before the block could skip it five
ways (dry run, unreadable labels, changed consent, PAT identity, failed DELETE),
so a dry-run scan reported every budgeted PR as released. The row now follows the
DELETE that actually happened.
Reclaimed the bytes from three pointer comments whose rationale lives in their
sections (af-045, af-118, af-120).
* test(ci): replay the budget boundary end to end, bot path included
The release harness stubbed fleet_row to a no-op and extracted from the inner
if, so neither the fleet row nor the bot-authored half of the boundary was
executed: a dry-run scan could report a release it never performed, and a
restructure that stopped the bot check reaching the branch would release a bot
PR's label with the suite green.
The stub now records rows, every skipped arm asserts no release row, and a
second replay runs the whole outer block with a bot author: no DELETE, and the
notice names the budget instead of a round cap the PR never reached. The outer
entry condition is replayed on its own for the no-op-only window, whose ROUND
never advances past 0 - the only reason a for-2 PR that found nothing twice can
still end.
* fix(ci): state the ack's one exception instead of promising an unconditional release
The budget ack said 'the loop removes the label' for every reader, but a
bot-authored PR keeps the label and pauses for a human - the exact case a
maintainer reading their own ack would mispredict. Both languages now carry the
exception in the same sentence, so the ack is accurate whichever PR it lands on
without a second authorship read at the ack site.
* test(ci): feed the parameterized commands to the shepherd's resume detector
The widened matcher was exercised by nothing: deleting either startswith arm
kept both suites green, and the anti-drift cross-check pins only the two exact
forms. A future grammar change would leave the shepherd blind between a
maintainer's command and the ack landing, auto-releasing a just-re-engaged PR -
the regression the cross-check exists to prevent. Add the two forms plus a
near-miss that must not count.
* fix(ci): match the shepherds resume commands as strictly as the router does
The startswith arms accepted bodies the router rejects - for 0, for 100, for 3
please, from 3 for 2 - so a command that did nothing (no label, no ack, no log
line) vetoed the auto-release for the whole grace window, renewable by any
further malformed variant, and three such bodies from distinct authors in one
window tripped the two-read budget into a permission-fault deferral.
The matcher now strips the prefix and requires a 1-2 digit tail, with the
for-arm also requiring a value >= 1 - the same grammar the parser enforces,
including leading zeros read as decimal.
* docs(ci): correct the prose move's warranty and the inventory's map
af-160 claimed the block was moved verbatim, but the TAKEOVER_COMMAND entry is
the one rewrite - and the rewrite had dropped the base's assurance that the
captured value reaches nothing but an integer comparison. The preamble now names
that exception and the entry restores the clause.
The inventory named the report script, which this change does not touch, and
omitted the fleet shepherd, which it does; the scope boundary repeated the
omission. af-159's site index pointed at the script instead of the shepherd's
resume detector, and still credited the script with carrying the budget.
* test(ci): pin each language of the budget ack separately
Both ack bodies are single printfs with positional arguments, and the only
assertions on the budget copy searched the whole posted body - so the exact
mutation this file documents elsewhere (swapping the EN/ZH arguments, shipping
the Chinese sentence inside the English paragraph) stayed green. Split at the
details summary and assert each language names its own sentence, for both the
engage and the re-arm ack.
* test(ci): replay the prepare budget reads instead of injecting them
Both discard tests handed LIVE_ROUND_BUDGET and LIVE_ATTEMPTS in as literals, so
the two jq programs decision 8 rests on were executed by nothing: a wrong window
key or a dropped marker scan stayed green while a queued round either ran past
the budget or was discarded on an unbudgeted PR.
Replay both reads against a real census: a budgeted ack plus an eval-carrying
re-arm must still yield the budget (the re-arm does not open an engagement), a
human release must zero it, and a spent budget on a bot PR must survive that
removal.
* test(ci): read the report script through the module constant
The re-arm test resolved the script path a second way (resolve/dirname/
fileURLToPath) where ~35 sibling sites use the module-scope read, so renaming or
moving the script meant finding a second spelling whose stale literal would only
surface as a lone ENOENT in this test.
* fix(ci): keep legacy eval markers distinct and migrate the last reader
Two defects in the run= marker format, both from readers that assumed the old
shape:
- The marker builder stored an absent run= as "", and jq's `//` does NOT fall
back for an empty string, so two DIFFERENT pre-change reports collapsed onto
one dedup key: a for-2 window under-counted its attempts and could run one
more round. Both builders now keep a missing run null, so `//` falls back to
the ts|round key as intended, and a replay of two distinct legacy reports
asserts 2 (a re-posted one still asserts 1).
- OVER_BUDGET_AUTHORS still required ` -->` immediately after the optional
win=, so it rejected every new report and left the per-maintainer regular-
feedback batch limit disabled during Critical-only rounds. Its scan now
accepts the optional run= field like the other readers.
* test(ci): pin the eval marker's run field where the assertions read it
Appending `run=${GITHUB_RUN_ID}` to the eval marker moved five expectations out
from under the tests: the report step's terminal-handoff marker and the report
script's two pushed/no-op markers. The assertions still pinned the pre-`run=`
text, so `Test (ubuntu-latest)` failed on `posts a human-handoff marker…` and
`stamps the push marker…` while the writers were correct.
The markers themselves are unchanged — only the expected literals now carry the
field the writers emit.
* fix(ci): let an explicit release close the budget, and keep idle wedges out of the halt
Two defects at the engagement floor, both reported against the shipped code:
- The spent-budget survival clause read label absence plus bot authorship, so it
could not tell the shepherd's silent removal from a maintainer's `stop` or a
hand-removal. A bot PR that spent `for 2` stayed parked at 2/2 after `stop`:
the release leg was skipped by the bot check and the paused leg re-applied
needs-human, contradicting the ack's "standard management continues". The
floor now treats `takeover-ack released` as a boundary, so an explicit release
ends the engagement while a silent removal still keeps a spent budget.
- The halt census counted idle (silent-sandbox) sentinels, which the cumulative
breaker deliberately excludes, so three infra wedges parked a bounded
engagement instead of releasing it. Their runs are mapped from the idle
headlines and subtracted, mirroring the breaker's own `TIMEOUT_N - IDLE_N`.
Reclaimed ~2 KB from three pointer comments (af-155, af-149, af-152) whose
rationale stays in their sections, taking the file from 113 bytes of headroom
to 1,451.
* docs(ci): state the budget's real lifetime, halt thresholds and prose warranty
Four records told the operator the opposite of what shipped:
- the design record (both languages) and af-159 said the shepherd's auto-release
ends the budget and a paused bot PR returns to ten-round management. A SPENT
budget now survives any removal that leaves no `takeover-ack released` marker,
and only an explicit `stop` posts one — so `stop` is the release that ends it.
- af-159 and the guide said any sentinel halts the release, and that the first
crashed or timed-out round parks the PR. The halt needs TIMEOUT_WINDOW_CAP (3)
sentinels (idle ones subtracted), or a round past ROUND_START + attempts, or a
convergence notice; one crash only spends its attempt.
- af-160's warranty promised ONE exception; TRUSTED_ASSOC is a second, now
marked at its entry and counted in the preamble.
- af-157's quoted block had lost its first line, leaving an orphaned fragment
under a heading that still quotes it.
The pointer convention now says a summary line may be rewritten once its block
was compressed (the section is authoritative), and af-159 points at the
growth-audit record for the re-arm invariants it depends on.
* test(ci): round-trip the run field and witness both release suppressors
Two gaps the last round opened:
- The `run=` format was pinned only on the writer side. Both builders now get a
round-trip fixture: a body carrying `run=1111` parses round, window and run
through the real jq, a legacy body still parses round and window with run
absent (null, not ""), and the census keeps two distinct runs apart. Dropping
the optional group from either reader now reds it instead of silently
emptying the census.
- The release gate's two suppressors had no failing witness: `runBoundary`
hardcoded `BUDGET_HALTED: 'false'` and `runRelease` omitted the variable. Both
are parameterised now, with a halted engagement and a real forced dispatch
asserting no label DELETE, a passive event carrying the same pr_number still
releasing, and `runRelease` passing the literal production always has.
The halt census also gains its false side: one sentinel is not a halt (the
threshold is TIMEOUT_WINDOW_CAP), and a convergence notice older than the window
key is not one either.
* fix(ci): retry the handoff post once
The handoff comment is the only carrier for a round that errors, times out or
crashes: its gate excludes fixed/noop rounds, and the always() finalizer only
patches the status comment. It posted single-shot, so one transient 5xx during a
comment-API blip left the round uncounted — the census did not move and the next
scan re-dispatched the same round, a free attempt.
It now retries once, five seconds apart, like the re-arm ack. A sustained
outage still costs the attempt, which the guide already states: a lost marker
delays the release, it never ends the engagement early.
* test(ci): watch the seed beside the budget, and both notice-dedup filters
Three assertions that could not fail before:
- the route replay printed only TAKEOVER_CMD and TAKEOVER_FOR, so "the two
forms never co-emit" was pinned in one direction only. It now prints the seed
too: `for N` must leave the seed empty and `from N` the budget.
- the budget-spent dedup fixture was newer AND bot-authored, so dropping either
filter stayed green. A human-quoted marker and an older bot notice must both
fail to suppress the note now.
- the growth marker's window prefers GROWTH_BASE_WIN over WINDOW; every existing
case passed the same value for both, so swapping them was invisible.
* docs(ci): record what happens when a breaker meets the spending round
The boundary table covered a breaker that stops the loop BEFORE the budget is
spent and said nothing about its condition being met exactly on the attempt that
reaches N. That attempt is already counted, so the PR takes the release path and
no breaker diagnosis is posted: the consecutive-failure gate cannot fire on the
cap round. Stated in both languages rather than left to be inferred from code.
* test(ci): witness a failed label DELETE without aborting the scan
The release harness's gh stub exited 0 on every write, so the DELETE's failure
arm was dead in the suite: unwrapping the 'if !' would have shipped green, and in
production one transient 502 would abort the scan job — costing every other PR in
that tick its round.
The stub can now fail a label write; the case asserts the warning, no fleet row
claiming a release, and that the block still reaches its continue.
* test(ci): inline the round-trip inspection command
A single-use constant left over from iterating on the fixture; the comment above
it already says why the command is what it is.
* docs(ci): finish removing the claim that the budget ends with the label
Two sites still carried the old rule. The guide told the operator that the
shepherd's auto-release returns a paused bot PR to ten-round management, and
af-160's TAKEOVER_MAX_ROUNDS entry promised that removing the label restores the
strict cap — which a still-live for-N budget keeps clamping, exactly in the
spent-bot case af-159 now documents. Both now say what the code does.
Found on an audit pass over this round's diff; no behaviour change.
* docs(ci): list the released ack among the floor's boundaries
Decision 2 enumerated two of the three markers that move the engagement floor;
the explicit-release marker, which is what tells `stop` apart from a silent
removal, was missing in both languages.
* fix(ci): clear the escalation a deferred budget release applied
A forced dispatch that meets a spent budget answers the dispatch on the
paused path, so the release lands one scan later — and left the
needs-human label that pause applied on a PR whose release note says
nothing is waiting on a human. The release now deletes it too, 404
tolerant and only after the takeover DELETE landed. The notices on that
deferred pass quote the attempts the budget counts: a no-op-only
engagement reaches the gate at round 0, and 0/N named a cap the PR never
reached.
* docs(ci): record the forced-dispatch deferral of a budget release
The guide, af-159 and both design languages promised an unconditional
release with no escalation label. A forced dispatch is the one pass that
cannot release, so the deferral, the cleanup it now owes, and the
attempts numerator are written down where a maintainer reads them.
* docs(ci): stop claiming the breaker is the only thing that withholds a release
The guide said the release is withheld ONLY on the three breaker
thresholds, and read any pause as 'a human should look'. A forced
dispatch withholds it too, for one scan, and that pause means the budget
finished. Both design languages lose the same exclusivity claim.
* docs(ci): spell out the label pointer like every other one
The compressed TAKEOVER/SKIP comment kept a bare 'af-160' while the
other 184 pointers in the file name the record they live in.
|
||
|
|
6ef3eaa2f2 |
feat(managed-agent): add an admin launcher for the maintenance entry points (#15772)
* feat(managed-agent): add an admin launcher for the maintenance entry points The server has twelve offline maintenance entry points, but the reference distribution only shipped a launcher for operator recovery. The docs ran the rest from a module build, sometimes through a classified jar the distribution does not ship. bin/qwen-managed-agent-admin <command> now runs each entry point by a short name from the server jar, through PropertiesLauncher, since every classified jar is the same application with another main class. It lists its commands without starting Java, exits 2 on an unknown command, and loads config/application.yml as the other launchers do. Packaging fails when a mapped class is missing or has no main method. The deployment guide gains a maintenance-commands section that names the W1_JDBC_* and K2_JDBC_* credentials the commands read. The server and K2 READMEs show the distribution form first, and the K2 module-build commands use the plain server jar. * docs(managed-agent): name the admin commands that read the Spring datasource operator-recovery and resident-retirement start the server's Spring configuration and take its datasource, so they read SPRING_DATASOURCE_* and the other control-plane settings, not W1_JDBC_* or K2_JDBC_*. The deployment guide and the design said every database command read its own credentials; they now name the two exceptions, say to run them with server.env loaded, and note that csi-protection opens no database. The config template's list of launchers that default SPRING_CONFIG_ADDITIONAL_LOCATION now includes the admin launcher, which shares the server launcher's preamble instead of a copy of it. A test now also fails when a class with a main method has no admin command. |
||
|
|
21c8b594db |
feat(verify-pr): attribute failures, close the publish guard, enumerate coverage (#15764)
* feat(verify-pr): attribute failures, close the publish guard, enumerate coverage Three gaps measured on a maintainer round, each a deterministic rule that lived only in prose and failed silently. The counting rule was taught in two halves that never met: one paragraph showed how to prove a red environmental with an A/A control, another said expected reds are passes. Nothing joined them, so a host-environment red had no category. The round booked the same repository test twice — head and its base control — and published fail: 2 with findings while its own verdict line said neither reason was a defect in the PR. The base cell asserts that base fails; it failed as predicted, so it passed. assertions gains an optional failures ledger attributing each red to pr, harness or environment, and the checker refuses findings or blocked when nothing is attributed to the PR. The verdict vocabulary stays at four words. The pre-publish guard asked "anything new?", and a failed fetch returns zero items, so the answer was "no". Measured 324 review comments down to 0 on a retry that never reached the API. It was the only local-path safety rule with no script behind it. The guard now owns both halves: a failed call is fatal and writes no file, and the compare side independently treats a short list, a vanished id, a moved head or a changed state as dirty. No retry flag — a silent retry budget hides a machine that cannot reach the API. "Counts are sacred" bound only numbers. A universally-quantified coverage claim over an enumerable set was asserted, not generated, and two production commits had no mutation row. mutations.json now carries the claim and the enumeration it is about, and the checker names any listed commit no row covers. Verified: 230 pin tests across the three suites, an 11-row vacuity matrix with 11 killed including a control on a pre-existing guard, and a real round re-encoded from findings to merge-ready at 548/548. Three of the matrix rows exist because self-audit found defects in this change itself. * fix(verify-pr): summarize the ids a short list lost Driving the new guard against a live PR to capture evidence for this change showed it printing the short-list reason and then 321 "disappeared" lines. The one actionable line was buried under its own echoes, which is the same failure the guard exists to prevent: a maintainer who cannot read the output cannot act on it. The absent ids are a consequence of the truncation, not independent reasons, so a short list now reports its reason plus a count. At equal length the missing id is the whole signal and is still named — a second pin keeps the suppression from swallowing a genuine deletion, and a vacuity pair proves both halves (2 rows, 2 killed). * fix(verify-pr): enumerate the branch's own commits, and ask for the files the checks read A second audit pass over this change found five defects in it, two of which defeated the rules the change exists to add. The coverage rule told the round to enumerate with `git log --no-merges`, which drops a merge commit but keeps every commit that merge brought in. On the very branch whose wrong claim motivated the rule, that recipe lists 85 production commits where `--first-parent` lists 24; the 61 extra come from one merge of main. A pin was defending the wrong command. The recipe now carries `--first-parent`, explains why the other flag does not substitute for it, and quotes the measurement. The checker read `mutations.json` and the failures ledger, but the artifact contract never asked for either, so a round following the contract would produce neither and both new checks would sit silent. The contract now names both, with a pin per file. A per-commit claim over an empty `productionCommits` list passed: it is vacuously true, and the empty list is the one incomplete enumeration that satisfies every other check. It is now refused, while making no claim stays silent so a docs-only round is unaffected. Also corrected a cross-reference to a report item that does not exist, and dropped a `warnings` return value no caller could ever observe. The vacuity matrix was rebuilt to cover every guard this change adds: 25 rows, 25 killed, including a control row on a pre-existing guard. Each row carries the commit that introduced its clause, verified against that commit's blob and its parent's, so this change's own per-commit coverage claim is checked rather than asserted. * fix(verify-pr): ask for the mutation matrix only when a round ran one The artifact contract listed it unconditionally while the checker treats it as optional, so a documentation-only round with no matrix was told to produce a file it has nothing to put in. The ask is now conditional on having run mutations, which is what closes the gap: a round that ran them must write them down. * docs(design): count the third-pass finding in the self-audit enumeration The validation section enumerates what self-audit found in this change, so leaving one out is the same incomplete enumeration the change is about. * docs(design): report the matrix as per-guard, which is what it is Applying the enumeration this change teaches to the change itself names docs-only commits that carry no mutation row, and the checker refuses a per-commit claim over them. Narrowing the claim is the honest response; the refusal is recorded as the rule catching its own author. * fix(verify-pr): the per-commit claim is earned by the enumeration The paragraph told every round to record coverageClaim "per-commit", which is the same overstatement the artifact contract had: a round whose enumeration has an uncovered commit would be instructed to assert coverage it does not have. The claim is now conditional on every enumerated commit having a row, and a round with a gap reports per-guard and names what it did not reach. Pinned, and added as a vacuity row. * fix(verify-pr): a review edited without its stamp moving is still an edit The guard compared ids and timestamps, and a review carries submitted_at with no updated_at: editing its body moves neither, so a CHANGES_REQUESTED review rewritten to LGTM compared clean. The snapshot now carries each item's body next to its stamp and compares both, and refuses a fetched list whose items have no body rather than reading it as unchanged. * fix(verify-pr): a dismissed review moves neither stamp nor body On PR 15388 review 5673309455 was submitted CHANGES_REQUESTED and dismissed five and a half hours later with its 4838-character body and submitted_at unchanged; only state moved, and that dismissal unblocked the merge gate. The snapshot now carries each review's state beside its body and stamp, and refuses a review list without one. The compared fields are the ones that change what a reviewer said or decided; reaction counts stay out, so a thumbs-up is not a dirty round. * docs(design): cite what the dismissal record actually proves The dismissed-review note claimed its 4838-character body was unchanged. The API records only state and dismissal_message for a dismissal, so the body could have been edited too and nothing would show it. What the record does prove is that submitted_at still reads the submission time 5.5 hours before the dismissal, which is why state is the field that has to be compared. * docs(verify-pr): say which refusal applies to which list A comment list has no state, so 'refuses a fetched list missing either' overstated what the guard refuses. It refuses an item with no body, and a review with no state, and the sentence now says that; 'a changed state' in the same paragraph becomes 'a changed PR state' so it cannot be read as the review state the snapshot now compares. * docs(design): finish the self-audit enumeration and scope the refusal The design doc repeated the scope error the skill just dropped: a comment item has no state to be missing. And the enumeration of findings fixed without a row stopped before the last two corrections, so the count of self-audit work was short of what the branch carries. * docs(design): state the interval the record shows, not a rounded one 19:47:16Z to 01:22:57Z is five hours and thirty-six minutes. 'Five and a half hours' understates it by six minutes, which is the sort of figure a reader checks. * refactor(verify-pr): LISTS has no consumer outside the guard It was exported and nothing imported it, which is the same shape as the unobserved return value an earlier pass removed. * docs(design): enumerate the non-guard findings instead of counting them 'Five further findings' is a count over a set that has grown twice since it was written, which is the failure this change exists to name. The list is now the list, and it carries the three findings that landed after it. * docs(verify-pr): name the matrix the survivors belong to Inserting the coverage rule between 'pinned / not-pinned' and 'Survivors are not noise' left the second sentence opening a paragraph with no antecedent. It now says which matrix. * docs(verify-pr): keep the no-projected-entries rule with the rule it belongs to The coverage sentence was inserted between 'maps to a check that ran' and the prohibition that continues it, so the prohibition read as part of the aside. * fix(verify-pr): subtract the target instead of cutting at the first parent --first-parent removes the over-count from a main merge by discarding every second parent, which also discards the PR's own commits when it merged one of its own feature branches: on a four-commit graph with one feature merge and one main merge, --no-merges lists 5 (2 of them main's), --first-parent lists 1 (dropping the 2 the PR wrote), and --not <target> lists the right 3. The undercount is worse than the overcount, because a complete-looking enumeration lets the checker accept a claim over commits that have no row. --not is a live-round recipe: it subtracts what the target already contains, so a branch that has landed enumerates empty. Measured on this change's own merged range, 85 / 24 / 0 for the three recipes, and 15 / 15 / 15 on its live one. The rule now carries that precondition, and the empty-enumeration refusal is what catches a round that runs it too late. * fix(verify-pr): the refusal recommends the recipe this change now teaches The empty-enumeration refusal still told the round to run `git log --first-parent --no-merges`, which the skill now documents as dropping the PR's own feature-merge commits, and a test pinned that advice. A round that hits the refusal, follows the hint to the under-counting recipe, can then file a per-commit claim the checker accepts, because the omitted commits are simply not in the list. Message, doc comment and pin now name `git log --no-merges <previous>..<head> --not <target>`, and the pin forbids the old form. * docs(design): catch the matrix counts up with the rows that exist The two numbers in the design docs had drifted three revisions behind the body and the comment (32/16 against 35/19). The claim checker now parses all four surfaces, so the next drift fails a check instead of surviving. |
||
|
|
3de07f37eb | fix(ci): reset desktop release runner Git metadata (#15761) | ||
|
|
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. |
||
|
|
db88f2da5e |
fix(ci): Decouple managed runtime distribution from Maven Central (#15782)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
11ef1988d8 |
ci(sdk-java): run the Runtime Broker fault gates when the Hosted verify fails (#15776)
The fault-gate step had no condition, so it ran only when every step before it passed, the 201-minute Hosted verify included. Across the nightly SDK Java runs from 10-14 to 10-27 that verify failed on 10 nights and was cancelled on one, and each time the 44 Broker fault gates were skipped: they ran on 3 of the 14 nights. PR #15437's own full-tier run skipped them the same way. The gates need only the CLI bundle and the installed SDK, so give them the condition the O4 step in the same job already uses: run unless the run was cancelled, provided the SDK install succeeded. A failed setup step still skips them, because the install step runs only after the bundle has succeeded. hosted-process-ci.test.js pins the condition, the install step's id (an unknown id reads as '' and would skip the gates on every run) and the setup order the condition relies on. |
||
|
|
a6ed0518df |
feat(managed-agent): support opt-in same-context Java embedding (#15718)
* feat(managed-agent): support opt-in same-context Java embedding
* fix(managed-agent): isolate embedded callback filter
* test(managed-agent): qualify fixture clock
* fix(managed-agent): preserve executable maintenance artifacts
* test(managed-agent): finish the by-name bean migration at test injection points
This PR moves managed-agent infrastructure to name-only beans
(@Bean(defaultCandidate = false) plus @Qualifier("managed*") at the
production consumer sites). The migration stopped short of the test
fixtures, which left two classes of breakage:
- ApplicationContextRunner slices registered unnamed JdbcTemplate,
Clock, ObjectMapper, DataSource and PlatformTransactionManager beans
that no longer match the production qualifier, so the context failed
to load (EventTransportConfigurationTest,
RuntimeRetentionConfigurationTest,
ToolPublicationStorageConfigurationTest,
ToolPublicationAsyncVerificationTest, EventTransportBrokerGate).
- Full-application suites autowired JdbcTemplate by type and silently
got Spring Boot's plain auto-configured template instead of the
schema-aware ManagedSchemaJdbcTemplate, which H2 rejects against the
VARBINARY identity columns as "Values of types BINARY VARYING and
CHARACTER VARYING are not comparable".
Apply the pattern the PR already establishes in
ManagedArtifactApiIntegrationTest (registerBean("managedAgentClock", ..))
and StandaloneDataSourceConfigurationTest (@Qualifier on the injection
point): name the fixture beans, qualify the injection points. Test-only;
no production code changes.
The 18 suites named by SDK Java (fast) go from Tests run: 583,
Failures: 25, Errors: 79 to Failures: 1, Errors: 0. The remaining
failure is acceptsClientBuiltCensusAfterTheDefinitionIsCommitted, which
needs packages/core/dist and fails identically before this change.
mvn checkstyle:check reports 0 violations.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvr7204nyh
* test(managed-agent): qualify the last three plain-JdbcTemplate test injections
The by-name bean migration landed at fourteen injection points but left
three full-application suites still binding Spring Boot's plain
auto-configured jdbcTemplate instead of the schema-aware
ManagedSchemaJdbcTemplate. H2 rejects the resulting String-to-VARBINARY
comparisons, so SDK Java (fast) kept reporting Tests run: 5805,
Failures: 1, Errors: 3 across ManagedApprovalSelectionTest,
SurfaceAdmissionAcceptanceTest and WorkspaceSessionRetentionTest.
Apply the identical @Qualifier("managedJdbcTemplate") pattern at those
three field sites, plus the retention fixture's @Bean ManagedAgentStore
parameter: that suite's CI stack runs through
ManagedAgentStore.syncLifecycleClaim, and the store is built from that
parameter, so qualifying the field alone would not have cleared it.
Test-only; no production code changes.
Local managed-agent-server reactor on this change:
WorkspaceSessionRetentionTest 23/1/1 -> 23/0/0, the BINARY VARYING
signature drops from 7 occurrences to 1, and checkstyle reports 0
violations.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvrdhhf0yo
* test(managed-agent): inject the broker gate's clock by its bean name
ExecutionConfiguration now registers managedAgentClock and
managedAgentExecutor with defaultCandidate = false so an embedding
context's own Clock/ExecutorService neither shadows nor is shadowed by
the control plane's, and every consumer resolves them by qualifier.
EventTransportBrokerGate still injected Clock by type, so the RocketMQ
lane failed to build the test instance: "No qualifying bean of type
'java.time.Clock' available". Qualify the field the same way the
production consumers and the gate's own startup() helper already do.
The other fields keep by-type injection: Boot's auto-configured
JdbcTemplate and ObjectMapper are default candidates, and this test
runs the whole application on H2.
* build(managed-agent): keep the packaging IT out of the failsafe selection
ManagedAgentPackagingIT reads the jars the package phase wrote and
needs managed.artifact.version, so the pom runs it from the dedicated
surefire artifact-packaging execution at integration-test with both
supplied. Its *IT name also matched failsafe's default includes, which
ran it a second time without the jars' version or the property:
standaloneArtifactsIncludeConfigurationWhileLibraryDoesNot opened
target/qwen-managed-agent-server-null.jar and
retirementLauncherLoadsFromExecutableJar could not load
org.springframework.boot.loader.launch.PropertiesLauncher from it.
Exclude it from failsafe next to the existing Hosted*IT exclusion; the
surefire execution stays the single, property-bearing run.
---------
Co-authored-by: Qwen-Coder <[email protected]>
|
||
|
|
b66711972c |
ci: run installed npm acceptance nightly instead of on PR updates (#15719)
* ci: run installed npm acceptance nightly instead of on PR updates * ci: report nightly installed acceptance failures as issues |
||
|
|
9acb3a5a1d |
fix(ci): pin PR retries to the triggering head SHA (#15734)
* feat(managed-agent): materialize inert external workspaces * feat(managed-agent): enable external files with verified close (#15685) * feat(managed-agent): add the external-workspace provisioning prerequisite (#15512) Step 3a of the external Hosted Workspace plan: a new `external-workspace` Runtime provisioner kind that places a session-isolated worker on a registered EXTERNAL storage tree and mints the boot document the shipped worker already admits (`managed-context/1` plus the existing `storage` member). The storage identity comes only from the durable registration: a read-only primary-key lookup that admits `REGISTERED` rows, bounds the revision to 2^53-1, joins the owner row on its own primary key, and fails closed when a persisted row disagrees with itself. A registration that cannot be read to completion stays retryable (`runtime_external_unavailable`) so the Broker keeps the placement adoptable instead of burying it. `external-workspace` accepts only the closed evidence set {storageRef, mountRoot, continuity}; `mountNonce` from the design is deferred and explicitly refused, because the registration record cannot persist it yet and an unverifiable claim must not be ignored. Hosted tools, EXECUTION admission, publication consumption, `stopDrained` and close gating are not enabled: `supportsDrainedStop()` stays false, every managed Workspace feature remains refused, and preflight now states the prerequisite in an `external-prerequisite` warning. No Session can be placed yet, and every placement fails closed. `external-scratch` behavior and wording, the shipped worker boot/storage/registration.json contract, the schema and the CLI are unchanged. * test(managed-agent): pin external storage refusal in lock canary * fix(managed-agent): require exact external workspace digest * fix(managed-agent): reject storage evidence for non-running workers * feat(managed-agent): add inert exact-worker external drain * feat(managed-agent): fence external workspace ownership and drain * feat(managed-agent): enable external files with verified close * docs: record verified external files and remaining phase two work * fix(web-shell): avoid recovery warning for closed sessions * test(ci): pin HostedExternalPublicFilesIT in the hosted-process class list #15685 adds HostedExternalPublicFilesIT, so hosted-process-ci.test.js's expected class list must carry it; the meta-test pins which IT classes the hosted-process lane runs and had 35 entries against 36 discovered. * fix(managed-agent): restore admission precedence and honour a LOST drain claim The external-files increment resolved the Workspace above the Agent pin, so a request whose stored definition cannot take effect, or whose revision cannot be selected, was refused on Workspace grounds first: it answered 404 workspace_required and a bare IllegalStateException instead of 409 agent_definition_unsupported / 400 unsupported_feature. Resolve the pin first again and keep the external pin rewrite once the Workspace is known. The close-claim validation admitted only DRAINING, but drainClaimedBinding deliberately keeps a LOST binding in LOST until its stop proof commits, so the arm refused its own claim with runtime_close_claim_pending instead of surfacing the stop's runtime_broker_recovery_blocked. BindingRenewal now records that its claim was taken on a LOST binding and admits that state. The settlement probe reads the storage kind to refuse an EXTERNAL tree the bridge holds no mount for, so its fixture needs the managed catalog rather than an empty database. The lock-order canary gains the new ExternalWork.apply/3 ambiguity, whose callee locks the Session row first and never reads ma_turn. * fix(ci): pin PR retries to the triggering head SHA * docs(ci): correct two comments the head-SHA pin made stale Pinning the four pull_request lanes to github.event.pull_request.head.sha left two comments describing the old checkout: the one above test_macos still cross-referenced the Ubuntu gate's superseded wording, and check-lint-gate-freshness.mjs still said the lint lane checks out refs/pull/N/head. Comment-only. ci.yml shrinks by 5 bytes (156090 -> 156085), so neither the .size-baseline ratchet nor the .ci-budget ledger moves. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvr2rokbyb * test(ci): pin all four PR checkout lanes to their whole ref expression The whole-expression `toBe` pin covered only `test` and `lint_and_static`. `web_shell_e2e_smoke` and `integration_no_ak` were guarded by `toContain(head.sha)` plus `not.toContain('refs/pull/')`, neither of which can see arm order: moving `|| github.ref` ahead of the pull-request arm keeps both green while the lane checks out the asynchronously rebuilt merge ref this PR exists to get away from, and the ancestor guard still passes because a newer merge tree contains the expected head. Pin both lanes against their own full ref literal, so arm order is part of the contract the same way it already is for the push-arm lanes. Verified one mutant at a time on each of the two lines: the new assertion turns red (1 failed | 79 passed) while the two substring assertions stay green, and the tree is byte-identical after reverting. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvr7204nyh --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
e305a774be |
fix(release): use the run-scoped token for the publish checkout (#15728)
* fix(release): stop authenticating the publish checkout with the bot PAT (#15726) The publish job's read-only checkout used secrets.CI_BOT_PAT while every other checkout in the release workflow uses the run-scoped token. Two release runs failed at exactly that step with "could not read Username for 'https://github.com': terminal prompts disabled" (git exit 128), and both passed on re-run. The bot PAT is already exported to the push and release-creation steps that write, so the read-only fetch does not need it. * test(ci): correct the release workspace wipe rationale (#15728) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
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]> |
||
|
|
dee6ace9d8 |
fix(ci): Preserve canonical artifacts across job retries (#15713)
Download the immutable artifact emitted by the successful producer when only a distribution consumer is rerun. Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
b227af2b58 |
fix(release): close the failure issue once a re-run passes (#15711)
notify_failure files an issue whenever the latest attempt of a release run fails, but the job is skipped as soon as a later attempt succeeds, so nothing ever closes the issue it filed. v0.26.2-preview.0 shipped on a successful re-run while #15672 stayed open, was picked up by the scheduled autofix agent, and produced a PR against a release that had already landed. Add a success-gated sibling job that reconciles that issue: it comments the passing attempt, clears the autofix labels, and closes as not planned. Closing requires bot authorship, so a human's same-titled issue is never touched, and the job also stays away when any validation job failed so it cannot race the notifier under force_skip_tests. |
||
|
|
7a9a88938e |
feat(cli): complete OpenTUI extension discovery and source management (#15671)
* fix(cli): align OpenTUI cancellation and retry with Ink * fix(cli): discard abandoned OpenTUI tool attempts * feat(cli): preserve OpenTUI session replay and rewind * fix(cli): preserve checkpoint restore ownership and cold continuation * test(cli): cover committed conversation rewind across renderers * feat(cli): capture and restore OpenTUI checkpoints Candidate checkpoint before parent test integration; full normal and combined product validation are still pending. * test(cli): remove duplicate checkpoint regression declarations * fix(cli): preserve Ink compact tool summaries after restore * fix(cli): preserve OpenTUI clipboard source through draft recovery * test(cli): cover OpenTUI raw recall and framed Vim submissions * fix(ci): register clipboard regression in TUI parity gate * feat(cli): complete OpenTUI extension discovery and source management * fix(cli): consolidate catalog interactions and focused key subscriptions * fix(cli): release catalog keyboard subscriptions during consent * fix(cli): preserve complete OpenTUI source information * test(cli): wait for restored assistant before recovery assertions * fix(test): preserve runner Git in installed CLI isolation |
||
|
|
7b09c0a940 |
fix(ci): keep the sdk-java cache write-back on a failed suite (#15555)
* fix(ci): keep the sdk-java cache write-back on a failed suite The self-hosted test step exits on the first Maven failure under errexit, so the cache write-back below it never ran for a red suite. The downloads that run had already made were dropped, and the next leg re-fetched them. Record each Maven failure in mvn_status instead, stop the remaining suites, and apply that status after the write-back. A tolerated write-back skip now reports the same status rather than turning the step green. This is the residual of #15289 after #15254 landed the suite-outside-the-lock split on main. Verified on Linux CI: the write-back publishes a stub Maven's artifact and the step exits 7 with no second Maven run; reverting the final exit, or one status capture, reddens the new pins. * fix(ci): close the sdk-java deferred-status review gaps Two harness defects shipped with the deferral: the new case never created its runnerTemp, so its load-bearing "one Maven call" assertion threw ENOENT, and the older write-back harness sliced below the `mvn_status` assignment, so the deferred exit aborted it with status 2. Both reddened the ubuntu lane, which is the only one that executes them. The tolerated-skip arms were only pinned as literal strings, so a skip that reported 0 on a red suite would have passed every test; they now run with both a green and a red status. A red run can also leave Maven's resolver tracking markers in the job-local repository, which the delta filter would publish into the host cache one consumer seeds without stripping, so both write-backs prune the names the seed already drops. The deferred exit emits an annotation naming the failing stage, because the write-back output now sits between the failure and the log tail. * fix(ci): bound the red-path wait and name the stage at every write-back exit A red leg reached the write-back but then queued for the full 9600 s behind a failure that was already decided, holding a pooled runner to publish what is often nothing; it now waits only the 300 s the copy itself may run. The stage annotation only covered the final exit, so a skip left the false "the gate result above is unaffected" warning as the last word and the fatal-flock arm reported 1 without naming the suite. One reporter now runs at each of those exits. The annotation also lands where the rerun classifiers grep, so the behavioral test pins it against INFRA_FAILURE_SIGNATURES, and the MariaDB lane's inline-failure premise — which the whole two-lane asymmetry rests on — is pinned too. A hard-killed transfer leaves *.tmp fragments that never resume, and the delta filter published them; both write-backs and every seed now drop that name, with the marker/tmp exclusions exercised by a red-suite fixture instead of only by string pins. |
||
|
|
74efa46640 |
fix(ci): preserve setup-java JDKs during pool fallback (#15623)
* fix(ci): isolate pool JDK fallback cache publication
* fix(ci): use the aarch64 JDK mirror directory
* fix(ci): normalize setup-java cache versions
* fix(ci): match setup-java's real tool-cache directory name
The pool fallback probed setup-java's cache at
Java_Temurin-Hotspot_jdk/${dir%%-*}/${arch}, stripping the build suffix.
setup-java does not strip it: getToolcacheVersionName() rewrites the first
'+' of Adoptium's semver to '-' BEFORE tc.cacheDir() applies semver.clean(),
and clean() keeps a prerelease -- so the real directory keeps the build
suffix (11.0.32-101, 21.0.13-9.0.LTS). The probe path never existed, the
adoption branch was dead code, and every fallback re-downloaded the pinned
archive even on a host that already held that exact JDK.
Key the probe by the full ${dir}. Our own namespace is unchanged: the script
still publishes only into qwen-pool-jdk/, never into setup-java's.
The test fixture derived the same directory with semver.clean(setupVersion),
encoding the same wrong premise, so both sides agreed by construction and the
suite stayed green over a dead reuse path. Derive it the way setup-java does
instead; reverting only the script line now turns 8 of 14 tests red.
Also assert GITHUB_PATH and toolchains.xml's jdkHome on the reuse path, and
make success() report stdout -- the script writes its ::error:: diagnostics
there, so the old stderr-only message was empty exactly when it was needed.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvp8bpg2w9
* fix(ci): defer private JDK staging until cold install
* fix(ci): reinstall JDKs with orphaned cache markers
* test(ci): pin the cold-install failure path of the pool JDK fallback
Deferring JDK staging into the cold-install branch left `::error::cannot
create` reachable with no witness. Add the cold-path arm: no shared entry,
private namespace shadowed by a regular file, assert the script fails closed
on its own marker and publishes nothing to GITHUB_ENV.
The assertion has to name `::error::cannot create` rather than the bare
phrase: `mkdir` prints "cannot create directory" itself, so `toContain
('cannot create')` still passes when the guard is dropped and the script
falls through to `::error::cannot stage the download`.
Also drop `expect(f.downloads()).toBe(0)` from the sibling-marker case.
Under `MODE=network` the curl shim exits 22 before it appends to the
counter, so that assertion could not fail; `success()` already pins zero
downloads there, because any curl attempt fails the script closed.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvprm5gzwv
---------
Co-authored-by: Qwen-Coder <[email protected]>
|
||
|
|
c9197a2dce |
fix(release): extend standalone runtime download retries (#15673)
* fix(release): extend standalone runtime download retries (#15672) Co-authored-by: Qwen-Coder <[email protected]> * docs(release): state retry-budget evidence without claiming the failing leg (#15672) The v0.26.2-preview.0 archive step aborted and passed on an unchanged re-run, but its job log was never retrievable, so raising the download retry budget from three to five attempts is defence-in-depth, not a confirmed fix for the download leg. Record that in the source comment and drop the same unconfirmed attribution from the retry test. * fix(release): correct retry-budget comment with attempt-1 log evidence The comment claimed the v0.26.2-preview.0 archive-step log was not retrievable and the failing leg unpinned. The job log is available via the Actions API and shows three consecutive 504s from the GitHub Releases CDN downloading the bun darwin-x64 runtime, so state the pinned leg directly. --------- Co-authored-by: Qwen Autofix <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: 易良 <[email protected]> |
||
|
|
51c59e69bb |
test(integration): widen the ready-prompt budget on GitHub-hosted macOS x64 (#15677) (#15692)
The tui-parity installed-distribution darwin-x64 leg (macos-15-intel) stalled past the 30s default on 5 of its first 6 runs with the banner already rendered, so hosted macOS x64 joins the 90s stall tier the self-hosted hosts already carry. Every other hosted host keeps the tight 30s fail-fast budget. |
||
|
|
e24f9c48fd |
fix(ci): deepen until the lint scope's own acceptance holds (#15644)
* fix(ci): deepen until the lint scope's own acceptance holds The deepen loop stopped as soon as `git merge-base --all` exited 0, but scripts/lint-scope.js also rejects an ambiguous merge base and a shallow boundary above it. A branch that merged the base tip proves a merge base immediately while its head-side boundary still sits above it, so the loop broke with zero deepens and lint-scope fell back to a full scan with three unused attempts. Mirror the consumer's acceptance instead: ambiguity stops the loop (deepening cannot fix it), a shallow boundary keeps it going. The two give-up paths — cap reached and an aborted deepen fetch — end in the same downstream warning whose remedy (a larger cap) is wrong for the aborted one. Emit the deepen count and the base depth reached so they are distinguishable in the log and the step summary. Pins now assert the mechanism rather than a bare `--deepen=` substring: the blobless deepen, the merge-base guard, the shared predicate call, `|| break`, the 3-round cap, and the give-up line. * fix(ci): report completed deepens and ambiguous history |
||
|
|
eea44208b8 |
ci: run every suite but CLI and core in the side test lane (#15570)
* ci: run every suite but CLI and core in the side test lane
The Linux test job's main lane ran the CLI suite and then, one after
another, core, about twenty smaller workspace suites and scripts/tests,
while the side lane (web-shell, then the standalone regression) went idle
long before the CLI suite finished. The main lane now runs only the CLI
and core suites; the side lane runs every other workspace's test:ci, the
standalone regression and scripts/tests, each at half the Vitest worker
cap. Every side-lane suite runs even after another fails.
* ci: print the side lane of a test step killed before its replay
The lane script replays the side lane's log only after the main lane
ends, so a step timeout or cancel lost every suite but the CLI and core
ones. The script now writes that log to SIDE_LANE_LOG when the caller
names one; ci.yml names it under runner.temp, outside the TMPDIR the step
removes, and a failure()/cancelled() step prints whatever is left of it.
The lane test also checks that the main lane's output is present before
it checks its order against the replay, and a comment that still placed
scripts/tests in the main lane now names the side lane.
* ci: keep the lane script's log out of a nested run
scripts/tests runs `npm run test:scripts:unit`, which runs the lane script
again, inside the step that names SIDE_LANE_LOG. The nested run inherited
that name and truncated, replayed and removed the outer side lane's log, so
the outer replay had nothing to print. The script now unsets the variable
once it has read it, and the lane test drives a nested run with a named log
to pin that: the nested run must not see the caller's log, and the outer
side lane's own output must still reach its replay.
The print-step test also pins the `if [ -f "${side_log}" ]` guard around the
cat, which is what keeps that step quiet on an ordinary red test job.
|
||
|
|
34769f9d41 |
fix(ci): Isolate Managed runtime release test classes (#15676)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
2f3d1b2ff5 |
feat(verify-pr): add a local preflight and a verdict check (#15614)
* feat(verify-pr): add a local preflight and a verdict check A maintainer round resolves the metadata snapshot, the base and head OIDs, the artifact directory name, and the verdict/count agreement by hand. The CI lane supplies all four; a workstation supplies none, and every one of them fails silently rather than loudly — a snapshot from the wrong repository, a parent commit taken for the base, a directory the collector never sees, or counts that contradict the stated verdict. Add a preflight that resolves the coordinates and reports whether this process is isolated, and a checker that applies the publisher's consistency rules to a local artifact directory. Point the skill at both where the flow needs them. * fix(verify-pr): address the local-helper review round The macOS sandbox marker is not an isolation boundary: its default profile still reads host files, so it is reported as a caveat instead of a pass. The coordinate path no longer gates on isolation. It must reach `gh`, which the boundary excludes, so it always runs on the host; gating there fired on every round and made the override the normal path. The check moved to `--require-isolation`, meant to run inside the boundary. A null or array assertions.json is now rejected instead of skipping every count check, the missing-OID hint names the `--repo` repository rather than `origin`, and the untrusted PR title is stripped of control characters before it reaches a terminal. The shared entry-point check moved under scripts/lib, and a workflow meta-test pins the checker's verdict vocabulary to the publisher's case labels. * fix(verify-pr): gate on credential-free readiness, not a container marker The CLI's own Docker/Podman launcher mounts ~/.config/gcloud and the ADC file, copies model API keys it can see, and then sets SANDBOX to the container name. Counting that marker as a passing boundary let a credentialed container through, which is the same class of false pass the Seatbelt fix removed. A marker is now an observation. Passing requires the explicit QWEN_VERIFY_CREDENTIAL_FREE=1 attestation and no known credential signal (cloud, SSH, kube, docker, git and gh variables plus their well-known paths, and SANDBOX_MOUNTS, which can carry anything). An attestation contradicted by a visible signal exits 4. * fix(verify-pr): close the three findings from the verification round The boundary gate's credential list now names ~/.config/gh and ~/.npmrc — the persistent form of the gh token and a registry token, already covered in their environment form. A report now has to cite the verified head when the round snapshot is present, not merely some 40-hex token that a base OID satisfies. An unreadable snapshot is reported instead of silently downgrading the check. A boolean-looking SANDBOX value no longer counts as a container marker. |
||
|
|
0539160f35 |
fix(cli): preserve previews and verify installed OpenTUI distribution (#15655)
* fix(cli): preserve previews and verify installed OpenTUI distribution * fix(ci): record installed distribution workflow growth * fix(ci): record installed distribution matrix budget |
||
|
|
2c9fe85a58 |
fix(ci): resolve the pool's Maven dependencies from the intranet mirror (#15641)
The pool's public bandwidth is 100 Mbps for 32 runners, so a burst of dependency fetches saturates it, and the resulting loss turns into the setup-java stalls and 429s seen on the Hangzhou hosts. Alibaba Cloud serves Maven from mirrors.cloud.aliyuncs.com inside the VPC, where the traffic never touches that cap: byte-buddy 1.17.8 arrives as the same 9,018,553 bytes in 0.54 s, faster than the public cap allows. Probe a real artifact (the directory root answers 404) and keep the public mirror for the hosts that cannot reach the intranet one. |
||
|
|
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]> |
||
|
|
71e35eb263 |
test(lint): pin the .gitignore grouping arm and the batch stride (#15640)
Three gaps from the review of #15531: - The `.gitignore` arm is the only grouping boundary that can decide a non-root group in this repository (26 tracked nested .gitignore files, zero nested prettier configs), and the only fixture that placed one sat next to a .prettierrc, so deleting the arm kept the suite green. New case groups a package by its own .gitignore alone; dropping the arm now collapses it to one group and fails. - Nothing covered a target set larger than one spawn batch. New case changes 51 files at the root group and asserts both spawns and that their union is the whole selection; a stride or slice edit that drops the tail now fails instead of reporting green on unchecked files. - The appended correction left the previous rationale in place, so the `toEqual([lane])` assertion sat under two mutually exclusive explanations. Remove the stale lines. |
||
|
|
edd1282a1a | feat(cli): align OpenTUI foreground shell controls with Ink (#15642) | ||
|
|
49056c09e9 |
feat(ci): canary-split the PR review model by PR number (#15538)
* feat(ci): canary-split the PR review model by PR number
The review job read its model from a single repository variable, so
evaluating a second model on real review traffic meant flipping every
review at once and flipping back to undo it.
Read a canary model and a percentage alongside the default, and route
`PR_NUMBER % 100 < percent` to the canary. The bucket is per PR, so a
PR keeps one model across its review rounds. Both variables unset (or a
zero/non-numeric percent) leaves today's behaviour byte-identical, and
reverting a rollout is one variable write.
The run also announces the model it chose: nothing else records it (the
cost ledger carries tokens, not a model name), so a rollout nobody can
attribute afterwards is unmeasurable.
* fix(ci): fail the canary percent safe to off, and pin the clamps that guard it
Out-of-range canary percents clamped to 100 — maximum rollout. Both
out-of-range paths failed open: a 4+ digit value hit the digit-count bound
and became 100, and a 1-3 digit value above 100 hit the >100 clamp and
became 100. Fat-fingering 50 as 500, or pasting a per-mille 1000, would
switch every PR review in the repository to the unproven model at once.
The block's own invariant is "the canary must never be the reason a review
breaks", so out-of-range now lands on off.
That diverges from SALVAGE_ELAPSED_PERCENT deliberately — there 100 means
cede, here it would mean full rollout — and the comment now says so.
Tests, each mutation-verified one mutant at a time:
- The clamp table pinned the old direction ('250' and '1000' both expected
'canary'), so it flipped with the fix. Each row now isolates one clamp
step: '0050' the leading-zero strip, '250' the >100 clamp, and
18446744073709551666 the digit bound — bash wraps that to 50, so
deleting the bound is a silent half rollout rather than off. The '1000'
row discriminated nothing and is gone.
- The "Sticky per PR" assertion compared two identical calls into a pure
function, so it could not fail. Replaced with a sweep of the whole bucket
range at 10% asserting the selected set is exactly 12300..12309. That
dominates the two literal boundary pins it replaces (12309 in-set, 12310
excluded) and additionally catches a nondeterministic bucket.
Mutation matrix (focused run, one mutant each, restored byte-identical):
revert clamp direction / delete strip / delete digit bound / delete >100
clamp / %100->%10 / -lt->-le / RANDOM bucket — all RED. RANDOM killed 3/3,
where the old self-equality assertion was measured at 22.5%.
Full file: 263 passed | 4 skipped (267). check-workflow-size.sh and
check-ci-budget.mjs rc=0; .size-baseline bumped to the file's exact 270879
bytes.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvo5qtfnv2
* fix(ci): report the canary percent actually applied, and pin the call site
The cohort notice interpolated the raw REVIEW_MODEL_CANARY_PERCENT while
review_model_for_bucket applied a clamped one. Since the clamp-direction
fix, out-of-range values clamp to off, so a repository-variable typo (250,
1000, abc, "10 ") announced `canary 250%` while every PR routed to the
default model — the notice asserted a cohort that never ran, which is the
inverse of the truth in exactly the case the clamps exist to absorb. The
clamp chain moves into canary_percent_effective so the router and the
notice read one normalized value.
Also trims the canary model name before the call. A padded paste reaches
--model verbatim, the provider rejects it, and that failure is terminal
for the whole cohort on every re-review round while reading as flakiness.
Whitespace-only still means "no split" rather than an invented cohort. The
default model's untrimmed path is main-side at two call sites and is left
to its own change.
Adds an executed call-site test: it slices the real block (helper, router,
trim, call site, notice, MODEL_ARGS) and asserts on stdout, so the four
arguments the workflow actually passes are pinned for the first time — the
unit rows above supply their own positional arguments and cannot see them.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvoc6arqva
---------
Co-authored-by: Qwen-Coder <[email protected]>
|
||
|
|
57e71d7fbb |
fix(ci): resolve libfaketime for the host architecture (#15609)
* fix(ci): make the full-tier fixtures portable across OS and architecture
Two full-tier legs fail on main for reasons unrelated to the change under
test, and both block every PR that reaches an approval.
The Windows leg dies in ManagedExtensionExecutionProfileTest: the fixture passes
POSIX literals such as /private/cache, which Path.of does not consider absolute
on WindowsPath, so the linux-cgroup assertion throws 'Managed extension cache
directory must be absolute'. Build the paths from java.io.tmpdir instead.
The EventTransport multi-node leg hardcodes /usr/lib/x86_64-linux-gnu/faketime:
the aarch64 pool hosts install libfaketime under aarch64-linux-gnu, so the
preload check and the -Dqwen.gate.faketime.library coordinate both point at a
file that cannot exist there. Resolve the library with find and pass it through
GITHUB_ENV.
* fix(ci): install the multi-node faketime from the pool's intranet mirror
The pool hosts sit inside Alibaba Cloud, where the internal Ubuntu mirror
answers from the VPC; the public archive is slow from that egress and the
install step is one of the two failures that reddened the full tier. Point apt
at the internal mirror when it answers, keep the hosted runners on their own
sources, and probe it first so the branch cannot run away from a reachable
archive.
* Revert "fix(ci): install the multi-node faketime from the pool's intranet mirror"
This reverts commit
|
||
|
|
09b1244150 |
feat(mods): add ui.render, $.ui.resolve and $.ui.invalidate to the runtime (#15591)
* feat(mods): add ui.render, $.ui.resolve and $.ui.invalidate to the runtime The first stage of terminal rendering (#13749): the worker raises ui.render for a site the host offers in its load request, and judges each hook's tree with Claude Code 2.1.310's rules for Box and Text. Mods get $.ui.resolve's frozen element table, the h and Fragment globals, and $.ui.invalidate, which reaches the host after the dispatch that called it. No host offers a site yet, so ui.render hooks are still skipped and listed at startup. The design for the whole stage is in docs/design/2026-10-26-mod-terminal-render.md. * fix(mods): keep render text cuts, notes and h children aligned with Claude Code - Cut text past the cap before a high surrogate, so the adjusted tree never holds a lone surrogate the validator refuses, and report the characters actually drawn. - Say each kind of adjustment (border style, text cut) once per load and site, so a text cut whose length changes on every render is told once. - Hand a function component its children flattened, always an array, as Claude Code's h does. - Freeze ENGINE_NODE, and document Claude Code's invalidate backoff for the Ink scheduling step. |
||
|
|
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. |
||
|
|
ef4c13283f |
fix(managed-agent): pass the pool Harness the Broker pair serve removes (#15394)
* fix(managed-agent): pass the pool Harness the Broker pair serve removes The Kubernetes launcher handed `qwen serve` the Broker URL and token as QWEN_HOSTED_HARNESS_BROKER_URL/_TOKEN. runQwenServe read that pair as a fallback but never removed it from process.env, so the token stayed in the Harness environment for the life of the process, and children started with that environment inherited it. The systemd Harness passes QWEN_MANAGED_RUNTIME_BROKER_URL/_TOKEN instead, which the serve handler reads and deletes at startup. - The launcher and harness-pool.yaml use QWEN_MANAGED_RUNTIME_BROKER_URL and _TOKEN, so both deployments share one pair and one removal. - Drop the QWEN_HOSTED_HARNESS_BROKER_* fallback and its process.env guard allowance. - Test the launcher's child environment against the names serve deletes, the manifest against every variable the launcher reads, and the handler's removal of the pair. * fix(managed-agent): name a missing pool Broker URL - The launcher reports a missing QWEN_MANAGED_RUNTIME_BROKER_URL as "The pool requires a Broker URL." rather than the generic configuration error, so a manifest that still sets the old name shows up in the Pod log. - The handler test records the Broker pair when the runtime starts, since the runtime snapshots process.env for every process it starts. Checking after the handler returned could not tell a delete before runQwenServe from one after it. - The Kubernetes README no longer says the systemd Harness uses these names; on main it still passes the pair as command-line flags. * fix(managed-agent): name the pool Broker URL variable in launcher errors The Broker URL variable is the input this change renamed, so each launcher error about it now names QWEN_MANAGED_RUNTIME_BROKER_URL, and a URL that does not parse gets its own message instead of the generic configuration error. Messages still never include the value. The Kubernetes README now limits the startup-scrub statement to the pool's `--profile hosted-harness` launch, which always reaches the full serve handler; the serve fast path does not read the pair. * fix(managed-agent): name the Broker URL rename and refuse a path in the pool launcher The v0.26.1 manifest sets the Broker URL as QWEN_HOSTED_HARNESS_BROKER_URL, which nothing reads any more, so a Deployment created from it stops at the launcher. The launcher now names the rename when only the old name is set, and both Kubernetes READMEs say to rename it before rolling out a later image. qwen serve takes an origin only, so a Broker URL with a path passed the launcher and then stopped serve at boot without naming the variable. The launcher now refuses it with its own message. The READMEs also say that the environment qwen serve started with stays readable in /proc/<pid>/environ to processes of the same user in the container, so removing the pair stops inheritance, not that exposure. * docs(managed-agent): rename the pool's Broker URL in the same apply as the image Renaming the Deployment variable on its own rolls Pods that still run the v0.26.1 image, whose launcher reads only the old name and exits with the generic configuration message, so the rollout stalls without naming the rename; an image rollback produces the same mix. Both Kubernetes READMEs now apply the rename and the new image digest together, describe the stalled rollout, and say that a rollback restores the old name with the old image. The launcher test also pins the full qwen serve argv, including --profile hosted-harness, and checks the credential, query and fragment arms of the HTTPS Broker URL guard, not only the protocol. * test(managed-agent): say why the pool launcher test pins the serve profile serve.ts removes the Broker pair whatever the profile. The profile is what lets validateHostedHarnessProfile accept the pair, and a launch without it exits instead of starting a default daemon, so the comment over the argv pin now says that. |
||
|
|
06d82d786a | feat(managed-agent): support Workspace file uploads (#15586) | ||
|
|
9c0414ab42 |
feat(managed-agent): let the bootstrap set the service account and MySQL up (#15543)
* feat(managed-agent): let the bootstrap set the service account and MySQL up With --service-user, run as root, the bootstrap writes the systemd layout's files with their final owners and modes: the account owns harness.env and application.yml, server.env is 0640 root:<group>, and a new directory is 0750 root:<group>. It refuses an unknown account, a malformed name, a non-root run, or a directory path the account cannot enter, before writing anything. server.env records the account as QWEN_MANAGED_AGENT_SERVICE_USER. The bootstrap also generates the database password and writes mysql-setup.sql, readable by root only, which creates the database and the qwen account for localhost and 127.0.0.1 with the performance_schema grant, and fails rather than changing an existing database or account. Preflight gains a service-user row, and the config and credentials rows check access as that account, including every directory above the file, so a root --check reports a file the service cannot read. * fix(managed-agent): check service-account access along symbolic links Both access checks judged directories from one side of a symbolic link only. Preflight resolved the file's real path first, so it skipped the directories on the way to the link: a link in a root-only 0700 directory passed although the account cannot enter that directory. The bootstrap stat'ed through the link but walked up the path as given, so it skipped the directories above the link's target: a configuration directory linked under a root-only 0700 directory passed, and the bootstrap wrote files the service could not read. Both now walk the path the way the kernel resolves it, one name at a time, following each link's target, and check every directory they look a name up in. More than 40 links count as a loop, as ELOOP does. |
||
|
|
b3ab3ec64c |
feat(mods): observe turn start and completion (#15443)
* feat(mods): observe turn start and completion Mods written for Claude Code follow the session's turns. Token Weather reads $.session.usage() in a turn.complete hook, summary Mods return a line from turn.complete, and guards end a turn with $.turn.abort. Qwen Code skipped those hooks at load, and $.turn.abort threw. turn.start now fires once UserPromptSubmit admits a prompt, before its first model request, with the prompt and a new turn id. The request does not wait for the hooks; the turn's tool middleware and its end queue behind them. turn.complete fires once when the turn ends, with the last response's visible text, the duration, whether it answered, was aborted or failed, and the token counts of the turn's own model responses. A different text a turn.complete hook returns is shown beneath the answer, as a Mod transcript line, and the record keeps the answer. $.turn.abort ends the running turn and its tools as a cancel does, without the cancel notice or restoring the prompt. The host, which owns the turn, judges the id. In a headless run the turn ends with the error result "A Mod ended the turn." and exit code 130. Turn events cover the prompts a person submits, in headless runs and in the Ink and OpenTUI terminals. Core reports admission through a new onPromptAdmitted option of sendMessageStream, so a blocked prompt starts no turn and the events do not depend on classic hooks. * fix(mods): address the turn events review - The Responses API counts reasoning inside output_tokens, so turn.complete usage counted it twice for openai-responses. The session and the uiTelemetry token fallback now share one rule, outputIncludesThoughts. - In OpenTUI, an Esc and a $.turn.abort on the same turn, in either order, lost the Esc's settle: the queued prompt did not go back to the composer, and the composer's cancel stayed pending, so later Esc presses did nothing. An interrupt now settles as a cancel even when a Mod ended the turn too. - A prompt or an answer past the worker's payload limit made the request fail, so every Mod missed that event with only a debug line. The session now keeps the head and tail of such a text within 128 KiB as JSON, as tool results are cut for the headless and ACP transports, and the event carries isTextTruncated or isAnswerTruncated. - turn.start hooks ran under the session's signal, so Esc did not end a slow one and the turn's tool calls waited behind it. They now also end with the turn, as in Claude Code; turn.complete keeps the session's signal. - The 4,096-character note and the terminal's 2,000-character log line are cut by code point, so a surrogate pair is never split. - The bundled-worker script test expects the turn fields of the load reply. * fix(cli): end an interrupted shared-loop tool batch with its result On the shared agent loop, an interrupt that landed while a tool ran made runNonInteractive reject with the TurnInterruptedError instead of ending with the interrupted result and exit code 130. The 'cancelled' case returned routeAbort()'s promise without awaiting it, so its throw skipped the run's catch. A stream-json session then got no result for that turn, and in a -p run a Mod's $.turn.abort from a tool.call hook ended the process with "An unexpected critical error occurred". The case awaits routeAbort() now, as every other call site does. A test runs the interrupt on both loops. * fix(mods): follow turns on the shared agent loop and OpenTUI retries main now runs headless rounds on the shared agent loop behind experimental.unifiedAgentLoop (#15369), and OpenTUI retries a failed request (#15283). The merge left the turn events wired to the current loop only. - On the shared loop, the main loop's model events reach the turn through a new onEvent scope hook, as they do on the current loop. Drain items stay out of it. - A tool that ends the person's Goal turn ends the Mod turn before a queued Goal continuation runs, as on the current loop. That path had no test on either loop. - An OpenTUI retry starts a turn with no typed prompt, like Ink's, though it reuses the failed turn's submit options. The headless Mod turn tests now run on both loops, with new cases for a Mod ending its turn from a tool.call hook and for a Goal continuation. * fix(mods): report run-budget stops as errors and reject no-op turn aborts - A headless run budget (--max-wall-time / --max-tool-calls) aborts the run's own controller, so turn.complete reported reason 'aborted' for it. It now reports 'error', as routeAbort already treats it. - $.turn.abort marked the turn as ending before the host cancelled anything. Ink's quiet cancel returns early once the stream is idle, so a call landing between the idle commit and the turn's completion resolved, and the turn that answered reported 'aborted'. The host's abort callback now returns whether it ended the turn; when it did not, the call rejects as "already ending" and the turn keeps its reason. - The Mods guide says a turn.start hook that aborts its own turn is cancelled with it. - Drop a duplicate `failed = true` in the OpenTUI turn's catch. * fix(mods): classify every Ink turn ending the Mods hear - A turn cancelled without an Esc, such as a Goal preemption, now ends as aborted. Its stream returned UserCancelled with no ending recorded, so turn.complete heard an answer. - A tool continuation whose delivery fails no longer records an error. That callback also reads a flag a concurrent /btw failure can set; the continuation's own failure already marks the turn through its events. - A failed request records its ending before the delivery callback runs. - A slash command whose prompt reaches the model waits for the Mods to start, as a typed prompt does, and is refused the same way when they failed to start. It used to run with no turn events. New Ink tests cover a cancel without an Esc, cancelled tools, tools that cannot go on, a failed continuation, the slash command wait, and that a Mod's end leaves the self-paced loop's wakeups alone. * fix(mods): end the running turn when the session closes - Closing the session, as /quit does, ends a turn still running as aborted before session.end, within the same budget. A turn whose end was queued when the close began still runs it. Both used to be dropped once the close shut the queue. - In headless text mode a turn.complete note waits for stdout to drain, so it lands beneath the answer. - In a terminal a turn.complete text keeps its own 4,096 characters instead of the 2,000 a $.ui.log line gets. Session tests now pin the turn.start guard after the queue runs, the session token limit as an error, the last model of a fallback, and no longer wait out the stderr flush timeout. * fix(cli): let a structured-output run end its Mod turn The three `return emitStructuredSuccess()` paths returned the promise unawaited from inside the run's try/finally, so the finally closed the Mod session before the result's completion ran and turn.complete was dropped. They are awaited now, as routeAbort is. New headless tests, on both loops, cover a structured-output answer and the continuation of an interrupted turn, and check the second $.turn.abort's return outside the stream, where the run's catch cannot swallow it. * test(core): cover a continuation send and an unanswered turn abort - A send that continues itself reports its prompt once. - A $.turn.abort the host never answers is settled when its dispatch ends, and the next dispatch still runs. * docs(mods): correct which turns $.session.turns() counts A retry and a headless continuation fire turn events but add no user turn to the transcript, so they are not the turns $.session.turns() counts. The guide and the design also say a turn still running when the session closes ends as aborted, and the validation lists name the new tests. * refactor(telemetry): share the reasoning convention with cost estimates The billable output estimate main added in #15464 repeats the list of auth types whose output count already includes reasoning. It now uses outputIncludesThoughts, the helper turn usage and the token total use, so the three read one rule. * fix(mods): finish turn ends on close and count turn usage as the session does Closing the session: - A close no longer cancels a turn.complete that is already running. Turn ends run under a signal of their own, which the close aborts only once their budget has passed, so the hook finishes and its note still shows. A hook that ran past the cancel's 250 ms grace used to have its worker terminated, and session.end failed with it. - Every end the close finds queued runs, in order, before the end of the turn the close cuts short. Only the last ended turn was kept, so in a terminal an earlier turn's end could be lost once the next turn had started. - A turn ends once, even when the host reports its end during the close. Turn usage: - `usage.model` names the model of the turn's own prompt. Each host passes the prompt id its requests carry, so a concurrent /btw that answers last on another model no longer names the turn's model. - Output tokens follow the rule #15464 gave the session's billable output: a total in the band both conventions allow decides whether reasoning is already in the output, the provider's convention otherwise. `outputTokenCount` now holds it for telemetry and the Mod session alike. `see()` also stops copying the stream once the turn has ended, or when no turn.complete hook will read it. * fix(mods): end Ink's turn.start hooks for a cancel in any round Ink gives each tool round its own controller, and the turn.start dispatch ran under the first round's. A cancel in a later round, by Esc or by the Mod, left a slow turn.start running for up to its budget, with turn.complete queued behind it, and a hook that called `$.turn.abort` went on after the call. The Mod turn now holds a controller of its own that every aborted ending cancels, as OpenTUI and headless runs already do with the turn's single signal. Ink also passes the turn's prompt id. The repeated "Mods failed to initialize" handling becomes one helper. * docs(mods): describe turn ends at close and the new usage rules The guide and the design say that a close lets a running or queued turn.complete finish within the end budget, and the validation lists name the new tests. * fix(mods): align turn endings across hosts and sanitize turn usage - OpenTUI ends a turn whose tools were all declined as `aborted`, as Ink does, through a new `onToolsCancelled` live prompt option. - Ink no longer lets a `$.turn.abort` on a turn the person already cancelled act as a second Esc, which aborted the detached work (such as a running side question) the first cancel spared. - Turn usage takes each provider count as a whole, non-negative number, so a hostile count no longer fails the `turn.complete` result check and drops the hook's note. * fix(mods): end loop-detected turns before close and keep Ink prompt text Headless returned emitLoopDetectedResult() without awaiting it inside the run's try/finally, so the finally closed the Mods first and the turn ended as aborted instead of error. All five sites now await it. Ink built turn.start text only from humanInput or submittedPrompt, so a prompt whose composer provenance was lost (a restored prompt stash, an edited restored submission) reached the Mods as "". It now falls back to the query it sends; a retry still carries "". |
||
|
|
8d213ba641 |
fix(lint): group incremental Prettier by the nearest config directory (#15531)
* fix(lint): group incremental Prettier by the nearest config directory Prettier's experimental CLI only loads a config or ignore file in the directory it starts from, so the lane spawned one process per touched directory. Each spawn pays a cold start (0.24 s measured) against a 37 s whole-repo budget, which the finding showed inverts around 154 directories; this repository holds 655 directories with prettier-eligible files, so a wide diff could cost more than the scan this feature removes. Group by the nearest ancestor that actually carries a Prettier config or ignore file instead, falling back to the repository root: a diff with no nested configuration collapses to one spawn, while a nested config keeps its own group (pinned by the real-Prettier nested-config case). Also documents the 50-target spawn batch, which the same finding flagged as unexplained. * fix(lint): resolve Prettier targets relative to the group directory Grouping by the nearest config-bearing ancestor moved the spawn cwd away from dirname(file), but the targets stayed bare basenames — a pair that only holds while the file sits directly inside the cwd. Any changed file below its group directory was therefore handed to Prettier as a path that does not exist, and the lane checked nothing. In this repository that is the normal case: the root carries .prettierrc.json, .prettierignore and .gitignore while scripts/ and docs/ carry none. Name each target relative to the group directory instead, using the POSIX form so the same string is produced on every platform, and pin it with a real-Prettier case whose changed file sits below its group. The existing argv assertions could not catch this — they run against mock binaries that never stat a target — and the matrix case that asserted "./below-file.ts" recorded the bug as expected behaviour; both are corrected. Also restate the motivation in the measured numbers: break-even is ~154 touched directories and recent diffs top out at 39, so the honest saving is about half a second at the median, not a 157 s inversion. |
||
|
|
22c89204f0 |
fix(ci): deepen the incremental lint history until the merge base is provable (#15525)
A fixed 100-commit window expires in about a day at this repository's merge rate, after which merge-base finds no common commit and every longer-lived PR silently ran the full scan (measured when the finding was filed: 68.7% of recently-updated branches were more than 100 commits from their base, median 196). Keep the fetch bounded: after the depth-100 blobless fetch, deepen in capped steps until Git can prove the merge base. Each deepen transfers trees only, and the existing history-fallback warning still fires if the cap is reached. Verified with the literal step body under bash -e -o pipefail against a local filter-capable server whose fork sits 300 commits behind the base tip: the current single fetch leaves merge-base unprovable, the new loop proves it after one deepen round and yields the expected changed-file list. |
||
|
|
f885f39ee9 |
fix(ci): price ARM64 fleet lanes from the self-hosted label (#15529)
The two Linux lanes that raise their timeout on the shared self-hosted pool -- the merge-gating lint lane and the no-AK integration gate -- decided "reserved pool" by looking for the x64 fleet's name inside the classifier's runner label set. The ARM64 fleet renders ["self-hosted", "linux", "arm64"], which carries no fleet name, so the same contended machines were priced as hosted: lint at 45 minutes and no_ak at 30 instead of 90/60. An ARM run killed by the shorter ceiling reads as an ARM compatibility failure, which is the signal the fleet pilot exists to collect. Scope on the routing property that matters -- self-hosted versus GitHub-hosted -- and model the new key in the routing meta-test with an explicit ARM64 case. |
||
|
|
bdd4a8fa8a |
test(lint): observe the --setup installer guard (#15533)
* test(lint): observe the --setup installer guard setupLinters skips a lane with nothing to lint before it checks for that lane's binary, and no test could see it: the check is `command -v <bin>`, a shell builtin that never executes the mock binary, so every assertion saw an empty call log regardless. A regression there restores the actionlint and shellcheck archive downloads and the pip install of yamllint on every PR — the install cost this lane exists to remove — with nothing turning red. Give the fixture an 'omit this mock' mode plus a constrained PATH (the mock bin, Node's own directory, git's, and the base system dirs, with no user bin), add a pip3 mock, and assert both directions: one changed .yml installs exactly yamllint, and a diff that selects no lane installs nothing. The constrained PATH is self-checked so an ambient yamllint fails the test loudly instead of making it vacuous. * test(lint): make the installer PATH hermetic and check the real one The constrained PATH kept dirname(process.execPath) and /usr/bin, and the ECS images carry a real yamllint in both, so on the pool this case could never reach its own assertions: `command -v yamllint` succeeded before the child ever ran. Mirror the system executables into a fixture bin with every yamllint entry filtered out instead — the installers still get the real coreutils and tar, and the ambient binary is provably unreachable. The self-check had the same shape of flaw the PR exists to fix: it validated the constrained PATH on its own, while the child's effective PATH is getLinterPath() over it, which appends $HOME/.local/bin on Linux — exactly where `pip3 install --user yamllint` lands. Pin HOME to a fixture with no .local/bin and assert that, so the net and the tested PATH agree instead of the net vouching for a stand-in. |
||
|
|
1876eb4997 |
test(managed-agent): Add shared Harness Kubernetes qualification tools (#15519)
* test(managed-agent): Add Kubernetes pool qualification tools Build from immutable source, constrain deployment to explicit business workers, and verify deployment identity plus signed multi-tenant public flows. Include the cross-Pod foreground publication configuration exercised on ACK. Co-authored-by: Qwen-Coder <[email protected]> * style(managed-agent): Match Kubernetes qualification YAML lint rules Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
e7a67ee4e3 |
feat(managed-agent): give the Hosted Harness its Broker through the environment (#15244)
* feat(managed-agent): give the Hosted Harness its Broker through the environment
The Hosted Harness reached the Runtime Broker only through command-line
options, so the shipped unit expanded the Broker token into argv, where
any local user can read it, and the token was stored again in
server.env for the control plane. A busy Harness port also moved the
Harness to another port that the control plane could not reach.
harness.env now carries QWEN_MANAGED_RUNTIME_BROKER_URL and
QWEN_MANAGED_RUNTIME_BROKER_TOKEN, which qwen serve already reads and
deletes from its environment, and the unit no longer passes Broker
arguments. Every secret gate lists the token too: core's child and
${VAR} denylist, the ACP scrub list, the channel worker environment and
the control plane's Runtime worker filter. The Hosted Harness profile
fails on a busy port instead of moving.
The Harness credentials file may carry the Broker token for the control
plane, refusing a second direct copy and binding nothing on a conflict;
both Broker lines follow the strict credential rules. Preflight adds a
broker-url row that compares the file's URL with the Broker's address
and warns when the file has none, and the broker row's fix names the
file only when startup read it. The bootstrap, templates, systemd unit
and guides keep the token in harness.env only and tell upgraded hosts
to replace QWEN_HOSTED_HARNESS_BROKER_ARGS.
* fix(managed-agent): check the Harness Broker URL against the actual listener
The broker-url preflight row matched loopback addresses by class, so a
Broker bound to 127.0.0.1 passed a file URL on 127.0.0.2. It failed any
URL that differed from the bind address, although a proxy or TLS front
end may forward it. And it ignored the scheme once a port was given, so
https to the plain-HTTP Broker passed.
The row now passes only a direct http URL: the Broker's port, and a host
that resolves to the bound address itself or, for a wildcard bind, to an
address of this machine. https to that same listener fails, since the
embedded Broker serves plain HTTP. Any other URL warns that a proxy may
forward it, which preflight cannot verify, so it no longer fails --check.
* fix(managed-agent): hold the Harness Broker URL to the Harness's own rules
- broker-url first applies the rules managed-runtime-broker-url.ts applies
at Harness startup (an http or https origin, plain http only on loopback),
reads the host of a registry authority such as an underscore name, and
names the direct loopback URL, or the TLS-terminating proxy for a
non-loopback bind, in its fix.
- A broker check fails a non-loopback bind without
runtime-broker.allow-non-loopback, which the Broker refuses at startup.
- The Harness credentials file requires the Broker URL and token together,
refuses a key containing whitespace such as `export KEY=value` (systemd
ignores it, a shell assigns it), and lets the URL bracket an IPv6 literal.
- qwen serve warns when --managed-runtime-broker-token is on the command
line; serve tests cover the environment fallback, its removal and flag
precedence.
- The upgrade runbook names the server.env token to delete, the server
README lists the Broker prefix among the stripped ones, and the bootstrap
test checks stdout for every generated secret.
* fix(managed-agent): read the Harness Broker URL the way the Harness parses it
The broker-url preflight row predicted the Harness with java.net.URI and a
hand-written loopback check, which disagree with Node's WHATWG URL parser in
both directions. It failed http://127.1:4182, http:/127.0.0.1:4182 and
http://127.0.0.1:4182/./, which the Harness takes as its loopback origin, and
passed http://::1:4182, http://127.0.0.08:4182 and port 99999, which the
Harness refuses before it starts.
- HarnessBrokerUrl follows the URL Standard for the part of an http(s) URL a
credentials-file value can reach: the scheme, the slashes after it, the
IPv4, IPv6 and ASCII-domain host parsers, the port and the path's dot
segments. An xn-- label needs IDNA, so the row warns that it cannot read
that URL instead of failing.
- managed-runtime-broker-url.fixtures.json records the Harness's verdict for
each spelling; the CLI test holds the Harness to it and HarnessBrokerUrlTest
holds preflight to it.
- The fix's direct URL is built from the address the bind resolves to, so a
::ffff:127.0.0.1 bind yields http://127.0.0.1:4182.
- Docs: the upgrade backs up server.env and harness.env and a rollback
restores them; the guide names the row's no-URL warning; the operator pages
allow [ and ] in the URL key; the server README states the URL/token pairing
rule.
* fix(managed-agent): keep the Broker pair across a daemon update restart
A daemon self-update replaces `qwen serve` through execve, which keeps
the PID, so systemd never hands the replacement the Broker pair from
harness.env again, and serve has already removed the pair from its own
environment. Hand the update restarter the resolved pair and add it to
the replacement's environment only, beside the server token.
Also from the same review round:
- preflight decides the plain-http, path and ends-in-a-number refusals
for a host with an xn-- label without IDNA, and leaves only an https
URL to such a host unclassified;
- the Java fixture loop allows only the two documented approximations, a
scheme or path read before the host and an xn-- host read without
IDNA; the shared fixture gains cases for both;
- the CLI fixture loop fails on a verdict it does not know instead of
passing on any throw;
- the broker-url row brackets an IPv6 Broker address;
- the credentials file refuses an empty Broker URL or token, which the
Harness takes as set and then refuses to start with;
- the deployment guide no longer says a wildcard Broker bind is reached
through a proxy.
* fix(managed-agent): warn on an IPv6 URL for an IPv4 wildcard Broker bind
The JDK binds 0.0.0.0 to IPv4 alone under -Djava.net.preferIPv4Stack=true,
so a Harness given http://[::1]:P cannot reach a Broker bound there, while
the broker-url row passed it as served. Preflight cannot see the server's
JVM options, which may come from an environment it does not share, so a
wildcard bind now serves only addresses of its own family, and an IPv6 URL
to the IPv4 wildcard warns and names the IPv4 loopback URL. An https URL
there still fails. The JDK binds :: only on a dual-stack socket, so the
IPv6 wildcard keeps serving IPv4 too.
* fix(managed-agent): warn on an https Broker URL across address families
When the Broker binds the IPv4 wildcard and the URL names an IPv6
address, the Broker's own socket takes it only from a dual-stack JVM.
Under -Djava.net.preferIPv4Stack=true another listener, such as a TLS
proxy, may hold that address and forward to the Broker, so preflight
cannot tell an https URL there apart from a working deployment. It now
warns for both schemes and fails https only for an address the Broker's
plain-HTTP socket takes in either mode.
---------
Co-authored-by: 易良 <[email protected]>
|
||
|
|
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. |