mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
main
1755
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5b44cfad71 |
fix(web-shell): include channel sessions in release dialog (#16537)
* fix(web-shell): include channel sessions in release dialog The release dialog inherited the sidebar's default source filter, which excludes sourceType: "channel". A workspace filled by idle channel sessions therefore opened an empty recovery list. Query the workspace catalog without a source filter for this dialog and keep the default, channel, and qwen-live rows. The sidebar and every other useScopedSessions caller keep the default filter. Add hook/dialog regressions and verify banner -> release -> create with a real sourceType: "channel" session filling a one-session workspace. * fix(web-shell): keep live sessions visible in release dialog Expanding the recovery list from the sidebar's default source filter to persisted history can still bury the sessions holding slots: closed channel rows fill the first page before they are reached. Add a liveOnly session-list option on the daemon route and SDK so callers can read the workspace bridge instead of persisted history. The release dialog requests the shared page size with that option and keeps listing default, channel, and qwen-live rows, so closed history can no longer crowd out what can be released. Co-authored-by: Qwen-Coder <[email protected]> * fix(sdk): serve liveOnly session lists over native REST The ACP session/list mapping drops liveOnly, so a DaemonClient configured with an AcpHttpTransport silently returned persisted history for the release dialog's live-only request. Route liveOnly through the native REST transport for both the legacy and workspace-qualified list methods, and cover the real ACP transport with a regression test. Co-authored-by: Qwen-Coder <[email protected]> * test(web-shell): pin the release dialog's source whitelist Live-only listings can include side_task, scheduled_task, and standalone sessions that the server does not hide. Add those rows to the dialog test so widening the whitelist fails, and state the 100-row live-page ceiling next to the page-size request. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
708ba347d3 |
feat(ci): add disposable external PR runners (#16513)
* feat(ci): add disposable external PR runners * test(ci): align platform lanes with disposable runner routing --------- Co-authored-by: yiliang114 <[email protected]> |
||
|
|
d85e911c78 |
fix(cli): restore OpenTUI dialog editing controls (#15908)
* fix(cli): restore OpenTUI dialog editing controls * fix(cli): preserve dialog Ctrl+C editing ownership * fix(cli): clear OpenTUI Source fields on Ctrl+C * fix(cli): flatten pasted breaks and read live focus in OpenTUI dialogs * test(cli): pin column-exact dialog rows and the second-Ctrl+C exit * test(cli): correct the row-oracle comment to the measured geometry * test(cli): name the measured phantom-column shape in the Sources oracle --------- Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
cf29f90062 |
fix(ci): route the review/resolve permission gate to the ECS pool (#16482)
* fix(ci): route the review/resolve permission gate to the ECS pool The authorize job gates every @qwen-code /review and /resolve command, but its ECS routing required github.event.pull_request, which issue_comment and workflow_dispatch payloads do not carry. Every command therefore fell through to ubuntu-latest, and the gate sat in the hosted queue for hours (PR #15286: 3h10m for a 4-second job) while the ECS pool was idle. Route issue_comment and workflow_dispatch to the persistent ECS pool, matching qwen-autofix.yml's route/review-scan pattern for PAT-bearing API-only steps. authorize runs no agent, checks out nothing, and processes no PR content, so this does not widen the fork attack surface. PR-payload events keep the same-repo fork guard; the hosted fallback and ECS kill switch are unchanged. resolve-pr and publish-resolution stay on hosted by design (ephemerality); this removes the gate wait only. * fix(ci): harden the authorize ECS-routing pins and risk docs Address review feedback on the gate routing change: - Pin the checkout-free invariant the ECS routing depends on: a uses: step on authorize would execute contributor-authored files on the persistent pool with CI_BOT_PAT in the step env while the expression pin stayed green (R1-1). Scoped to authorize; mutation-witnessed (adding actions/checkout reds it). - Record the trigger-set widening in the design-doc Risks (both languages): issue_comment makes the gate fireable by any GitHub user on a fork PR, and the should_review authorization fact is now produced on the shared host and consumed downstream without re-deriving (R1-2). - Correct the precedent wording: route loads only the read-only github.token; review-scan is the job that loads CI_DEV_BOT_PAT on the pool. * fix(ci): harden the shared-pool authorize gate against gh config reroute Address the round-2 CHANGES_REQUESTED review (R2-1 Critical, R2-2): R2-1: routing the gate to the persistent ecs-qwen pool moved its CI_BOT_PAT gh api call onto a host with a shared, writable ~/.config/gh. A co-resident survivor could plant http_unix_socket there, receive the PAT, and forge the permission response downstream consumes as should_review. Prepend the same pins as qwen-autofix.yml's route/review-scan — export GH_HOST=github.com plus a private, guarded GH_CONFIG_DIR (a failed mktemp exits 1; the gate fails closed). No `unset GH_TOKEN`: it is this step's only credential. Witnessed with real gh 2.103.0: the as-written step leaks the PAT to a planted socket and approves a stranger; the hardened step denies with the socket untouched. R2-2: extend the checkout-free guard to the run: half — pin the step inventory to the single permission step and forbid `${{` spliced into any run body (event values must arrive via env:). Mutation-witnessed: removing the preamble and adding a run-only second step each red the test. Also correct the design-doc parity claim (both languages): the exposure matches review-scan only WITH these gh pins; document the hardening. * fix(ci): extend the ECS gate routing to triage and harden the gate pins Two batches, verified together before push: 1. Extend the command-gate routing to qwen-triage.yml's authorize (the /triage, /tmux, /verify sibling blind spot): the same issue_comment / workflow_dispatch disjuncts, and — because that job has three steps that spend CI_BOT_PAT on gh — the same self-contained GH_HOST + guarded GH_CONFIG_DIR preamble on each, per the repo's per-step convention. Raise the recorded workflow size baseline to match the added hardening. 2. Address round-3 review (R2-2 fix-induced, R3-1): the gate pins tested substring presence only. Now evaluate the comment-stripped live lines and pin the guarded mktemp form + fail-closed shape, require GH_HOST / GH_CONFIG_DIR exported before the first gh api, and forbid executing repository code via relative paths or interpreters (the pool cwd is the reused $GITHUB_WORKSPACE). Applied to both the review and triage gate tests. Mutation-witnessed: an unguarded one-liner, a relocated preamble, and an appended `node ./…` line each red the tests. Tests: qwen-pr-review-workflow.test.js 271, qwen-triage-workflow.test.mjs 131, ci-runner-routing.test.mjs 103 — all pass; prettier + ESLint clean. * fix(ci): supply RUNNER_TEMP to the triage gate replay tests; harden the gate pins R4-1 (Critical): the new ${RUNNER_TEMP} gh-config preamble is the first RUNNER_TEMP reference in the triage permission step, and three replay harnesses in scripts/tests/qwen-triage-workflow.test.js execute that body verbatim without setting RUNNER_TEMP — off-CI (npm run test:scripts) the fail-closed guard exits 1 before writing should_run. Set RUNNER_TEMP to each harness's existing mkdtempSync dir (the :1773 precedent); the workflow's fail-closed guard is unchanged. Verified: the file is red under `env -u RUNNER_TEMP` before and 203/203 green after; dropping the line reds it again. (The review named two harnesses; running the full file surfaced a third.) Also clear the round-4 deferred notes: - R4-2: the /verify ack comment no longer promises a receipt "even when the sandbox lane is backed up" — the gate now also queues with the pool. - R4-3/R4-4: the triage routing pin is now an exact expression equality, the step inventory is pinned, and PAT-bearing steps are identified by env GH_TOKEN rather than gh subcommand spelling. - R3-1: tighten the no-repository-code deny-list (quote boundaries + interpreter as a word) so `bash -c './x.sh'` no longer slips past; mutation-witnessed in both the review and triage gate tests. Tests: triage vitest 203 (off-CI), triage .mjs 131, review vitest 271, ci-runner-routing 103 — all pass; prettier + ESLint clean; size gate green. * fix(ci): correct the gate-routing narrative; close the round-5 verify gaps R5-1 / F1 (narrative): the motivating incident is a /resolve, whose end-to-end latency this PR does not change — resolve-pr stays on ubuntu-latest, so only the gate wait is removed. State this in both design-doc languages and the pinning test comment; the saving lands on /review, /triage, /tmux, /verify (pool-routed downstream). Correct the "cancelled without ever running" sentence to the run's timeline (resolve-pr started when the gate finished and was cancelled ~29m36s later). S1: the no-repository-code deny-list missed absolute-path interpreters (/usr/bin/node x.js) and versioned Python (python3.11 x.py); widen the regex (optional path prefix + python[0-9.]*) in both gate tests. Mutation-witnessed: both previously-surviving mutants now red, clean tree green. S2: each hardened gh step minted a $RUNNER_TEMP/*gh-config.* dir that nothing removed; add trap 'rm -rf "$GH_CONFIG_DIR"' EXIT to all four steps (RUNNER_TEMP survives between jobs on the pool). S3: record the exact final workflow sizes in .size-baseline. Tests: triage vitest 203 (off-CI, RUNNER_TEMP unset), review vitest 271, triage .mjs 131, ci-runner-routing 103 — all pass; prettier + ESLint clean; size gate green. * fix(ci): pin the no-repository-code invariant on every triage gate step The triage gate meta-test applied the relative-path/interpreter denylist only to PAT-bearing steps, so a repository-code execution line added to the credential-free budget step survived the suite (witnessed by mutation). The no-repository-code invariant is a job-wide safety premise of the ECS routing: even a credential-free step that runs a planted file leaves a survivor on the persistent host. Pin it on every step's comment-stripped live lines, and align both design-doc versions with the widened coverage and the witnessed mutation set. Co-authored-by: Qwen-Coder <[email protected]> * docs(ci): correct the routing-matrix coverage claim for the triage gate The validation plan said the triage gate is pinned by the triage meta-test AND the ci-runner-routing matrix, but the matrix never reads either authorize job (it pins the sibling pool jobs). State the actual coverage in both language versions. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
4c8ebeab4d |
feat(core): view one pull request by URL for the PR-state watch (#16530)
* feat(core): view one pull request by URL for the PR-state watch Add fetchGitHubPullRequestView(url, cwd, env?), the single-PR view the daemon's PR-state watch will use (step two of PR A in docs/design/2026-11-04-daemon-pr-state-transitions.md, section 1). It runs `gh pr view <url> --json url,title,headRefOid,mergeable,state, statusCheckRollup` and returns the canonical URL, title, head SHA, open/merged/closed, mergeable (absent while GitHub reports UNKNOWN), the CI state and the failing check names (`<workflow> / <job>`, the check name or the status context; once each, at most 20). Only an http(s) /owner/repo/pull/<n> URL is accepted, and it is rebuilt without credentials, query, fragment or tab path before it reaches gh, so an issue URL, /PULL/7 or /pull/0 never becomes a branch lookup. The working directory must exist, so a missing one is not reported as gh being unavailable. Failures carry a reason (not_a_pr_url, cwd_missing, gh_failed, timed_out, unparsable). There is no host check; the caller restricts URLs to the allowed GitHub hosts. aggregateChecks is exported with unchanged behaviour; its failing-entry predicate is shared with the failing-name extraction. Every rollup entry still counts: gh's JSON exports no event, app or workflow id, and a dedupe by names and times would let a skipped or other-trigger run hide a current failure. The design doc's Files-affected row for core names the real signature and fields. * fix(core): report gh's diagnostic, not Node's argv, when gh fails execFile's callback error has no stderr property (only the promisified form attaches it), so ghErrorMessage fell back to Node's "Command failed: <argv>" message: a failed PR list, view or create reported the gh command line (for create, the PR title and body) ahead of gh's diagnostic, which the 4,096-character raw cap and the route's 512-character display cap could then cut off. The three callback wrappers now attach the callback's stderr, as the PR issues wrapper already does. When gh exits or is signalled with nothing on stderr, the message names the exit code or the signal instead of falling back to the argv, and a failure Node raises itself (a string code such as a spawn error or an overflowing buffer) keeps Node's message even if gh wrote something. `gh api graphql` failures of the PR issues fetch, which share the helper, get the same treatment. The test helper now fails the way execFile does: an exit 1 unless told otherwise, with stderr only as the callback's third argument, so the stderr tests exercise the wrappers and the stderr-over-exit-code order rather than the mock. Tests cover a silent exit, a signal, a buffer overflow with stderr, and a silent create that must not echo the title and body; the view's failing-names test gains a failing check run with an empty name and a status context with an empty context, which fail the head but are not named. |
||
|
|
978d9a23c3 | fix(web-shell): respect optional transport capabilities and local turn timing (#16536) | ||
|
|
0807f5d391 |
refactor(channels): share the bound-session prompt contract via channel-base (#16523)
* refactor(channels): share the bound-session prompt contract via channel-base Move the GitHub channel's operator instruction template contract (key validation against a caller-supplied event vocabulary, template validation, the role/event/lane resolution chain, the %var% renderer) and the bound prompt envelope (platform header, untrusted-data and not-published notices, labelled trusted-instruction block, sanitised data lines, Role line, visible 6,000-code-point body cap) into @qwen-code/channel-base, so the daemon's PR-state watch can reuse them instead of keeping a copy (step one of PR A in docs/design/2026-11-04-daemon-pr-state-transitions.md). The adapter keeps its vocabulary, the deliverToBoundSessions precondition, the comment-lane class, metadata parsing and provenance line, and its output is unchanged except for two rendering fixes: %constructor%-style names now stay literal like any other unknown name (they rendered Object.prototype members), and every substituted value is sanitised, as the templates design specified (only %title% and %sender% were; those two are still also capped at 200 code points). Tests pin the legacy, role-aware comment and role-aware watch prompts byte for byte (strings captured from the previous adapter), %url% on a GitHub Enterprise origin with a port, and the exact validation messages. The design docs, the template editor design and the user guide are updated to match. * fix(channels): keep the trusted instruction label inside the tag peel window The bound prompt labelled its trusted block `[Operator instruction — trusted, from <source>]`. With the channel's source that label's content is 65 characters, one over the 64 that sanitizePromptText peels from a start-of-line tag, so an event body that began with a copy of the label kept its brackets and read as a second trusted block. The label is now a fixed `[Operator instruction — trusted]` with the source on a `Source:` line under it, so it stays peelable whatever source a producer names. The platform the header names becomes a closed list (BOUND_EVENT_PLATFORMS) for the same reason: the header's content is the name plus 36 characters. A test runs over that list and feeds a copy of the header and of the label through the body and a data line; another puts a copy at the start of a substituted value. Also from review: pin the untyped-winner guard of the resolution chain, empty values rendering as empty, and misspelt `watch:` types being rejected, and align the templates design's validation plan with the cap that applies only to %title% and %sender%. The templates design, the daemon design and the user guide show the new label; the templates design's prompt example gains the not-published notice it was missing, and the daemon design's validation plan now expects only the prompt without a template to stay byte-identical. |
||
|
|
ced03aef5d |
feat(serve): reclaim idle channel sessions (#16532)
Channel workers keep an SSE event pump attached to every cached route, so the idle reaper treated those sessions as actively watched and never reclaimed them. A workspace could fill every maxSessions slot with sessions nobody was using and then reject new creates with 503. Let the reaper look past a channel-owned subscriber while preserving the local-work, active-prompt, and child conditional-close guards. Surface session_limit_exceeded as a banner with a release action. The release dialog falls back to the workspace daemon client when it opens from the empty new-session screen, where no session client exists. Design: docs/design/channel-session-idle-reclaim.md |
||
|
|
61aef512e7 |
feat(web-shell): offer a details side task from pending AskUser questions (#16458)
* feat(web-shell): offer a details side task from pending AskUser questions While an AskUserQuestion is pending, the Web Shell hides the composer and the btw shortcut ignores the pending-approval state, so a user who does not understand the options can only answer blindly or ignore the question. Add an "Ask for details" action to the pending panel. It opens a side task whose prefilled prompt snapshots the question and options and asks for a verified explanation — background, per-option meaning, differences, impact and risks, and a recommendation — delivered as a self-contained single-file HTML in controlled ASD-STE100-style sentences under .qwen/tmp/, previewable in the right panel. The side task inherits the parent context but never answers the question or touches the main session; the question stays pending until the user answers or ignores it. The action is main-view-only and gated on the session_side_task capability, matching /btw side. No daemon, protocol, or settings changes. * fix(web-shell): one details side task per question, reachable on narrow panels Post-publication audit of the AskUser details action found three issues: - Repeat clicks provisioned duplicate side tasks for the same pending question, each burning a model turn. Give the details tab a stable id derived from the parent session and the permission request id and upsert it, reusing the artifact panel's open-or-focus pattern; /btw side keeps its unique per-invocation draft id. - The question snapshot dropped multiSelect, so the explanation prompt did not say options can combine. Append a [multiSelect] marker (strict true). - On a 320px two-question panel the actions row overflowed to the left and pushed the details button out of the panel (left: -103px in a real-browser probe; scrollWidth misses left overflow on a flex-end row). Let the row wrap and pin a narrow-viewport regression test asserting every action button stays inside the panel and stays clickable. Each fix carries a red/green witness: the repeat-click App test, the multiSelect unit test, and a flex-wrap mutation of the narrow visuals test. * fix(web-shell): keep the details tab id within the daemon clientId budget The dedup fix derived the details tab id from the parent session id AND the request id. SideTaskPanel derives the pane's daemon clientId as side-task:<parentSessionId>:<tabId>, which with two uuids reached 138 chars — over the daemon's MAX_CLIENT_ID_LENGTH (128). The child session load then 400s, the pane never connects, and the prefilled prompt is never sent. Caught by re-running the real-daemon scenario on the post-fix head; mocked unit tests could not see it. Derive the stable id from the permission request id alone (a uuid; tabs are already bucketed per parent session), bringing the clientId to 101 chars. Add a SideTaskPanel regression test asserting the derived clientId stays within 128 for uuid-length inputs; the old id shape fails it with 138. * fix(web-shell): retain the details question-to-child association across tab close Review found that deduplicating only against open tabs loses the existing child when the details tab closes: clicking "Ask for details" again for the same pending request provisioned a second child, and reopening the child from the side-task catalog first did not deduplicate either (its tab id is side-task:<sessionId>, not the details-request id). Persist the pending-request-to-child association per parent session in localStorage, written when the details child is created. On click, focus whichever open tab shows the known child (details tab or catalog tab); with no tab open, reopen one attached to the existing child session — no draft, no initial prompt, so nothing is provisioned or sent again. One entry per parent supersedes the resolved question's association, keeping the store bounded; the association also survives reloads. Regression tests cover both reported paths (close + re-click keeps exactly one createSideTaskSession call and reopens with the child's sessionId and no initialPrompt; catalog reopen + click focuses the single existing tab). Red witness: dropping the association lookup fails the close + re-click test with two creation calls. * fix(web-shell): close the details association lifecycle gaps Re-review of the association persistence found three lifecycle gaps, all fixed here with regression tests and mutation witnesses: - A child exists before its explanation prompt is admitted. Reopening a closed details tab now retains the prompt on the tab; the pane's existing empty-session guard delivers it exactly once (only while the child has no user prompt) and never re-sends it. - Archiving a details child left its persisted association pointing at an unloadable session (the daemon's load route 409s session_archived). archiveSideTasks now invalidates associations pointing at archived children, so the next click provisions a fresh child. - Closing the draft tab before creation resolved skipped the association write, orphaning the child and provisioning a duplicate on the next click. The parent identity is captured when creation starts, and the write no longer depends on the tab still existing. Also pin two invariants the deep-verification report found unasserted: the derived clientId budget with uuid-length inputs through the real tab-id derivation (App test), and the formatter's array guard against non-array objects (formatter test). * fix(web-shell): supply the unsent details prompt to a catalog-reopened tab Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): treat media-only user prompts as interaction in the side-task guard Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): validate details child archive state at the read site and address review suggestions Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): replace stale details tab on fresh draft and re-check pending after the liveness query Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): replace a details tab bound to a different child than the validated binding Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
5b4dcbad19 |
fix(channels): watch every open bound PR and keep polling during public turns (#16502)
* fix(channels): watch every open bound PR and keep polling during public turns The PR state watch selected bound PRs purely by binding recency under a hard cap of 30, so older open bindings were never polled once newer bindings (including already-merged ones) filled the budget, and merged bindings outside the cap never received the terminal observation that would retire them. Selection now watches open PRs first and drains terminal-pending bindings with the leftover budget; the cap becomes the configurable prWatchLimit (default 30, range 1-500, fractional values round up), exposed in the channel descriptor and management API. Public-path tasks (review requests and assignments on unbound PRs) ran their full agent turn inside the poll loop, freezing watch detection, bound delivery, and every other notification for the turn's duration (observed: 64 minutes on a production daemon). They now execute in a background FIFO runner (concurrency 1); claims stay durable and synchronous, the cursor gate and crash recovery keep their semantics, and bound/watch deliveries stay inline. The first-contact body feed gains a thread-level gate so an async body turn cannot overtake and steer-cancel a still-retrying comment turn. * fix(channels): close review gaps in watch coverage and the async task runner Review round on the bound-delivery reliability fix. Three correctness findings, each reproduced against the base: - The first-contact body deferral gate could lose the body permanently: nothing reserves a deferred feed, and the end-of-poll cursor commit can mark the thread read once the blocking task settles. Remove the gate; the runner FIFO already serializes the body behind the comment turn. The absence gate now runs synchronously at dispatch so backpressure deferrals keep the false verdict the lane's body suppression needs. - disconnect retired the runner generation but the winding-down runner's restart hook drained the queued tasks anyway. Guard the restart by generation and resolve idle waiters at disconnect. - Recovery could re-resolve a backpressure-parked task whose public turn was running (the running transition merges patches) and deliver it bound a second time. Skip in-flight ids before any recovery branch mutates a task; crashed tasks without an in-flight entry still recover. Also: gone-session public fallbacks requeue through the serial runner instead of awaiting the turn in the poll lane (head-of-line block and steer-cancel on shared sessions); a same-instance reconnect keeps the orphaned runner's executing id in flight so recovery cannot double-run it; held threads skip redundant comment re-fetches via a poll-start snapshot; escaped runner failures are journaled once per task; the watch budget reserves a terminal slice (20%, at least one) so the drain progresses even when open PRs fill the limit; stale-open bindings tier as terminal once the watch observed closed; the limit constants are exported for the descriptor, the field is registered and translated in the Web Shell editor, and validation enforces the bound-delivery prerequisite; the rate arithmetic is documented at every surface. * fix(channels): drain terminal bindings outside the open watch budget The in-budget terminal reservation excluded up to ceil(limit*0.2) open bindings while a merged/closed backlog drained, permanently swallowing edge events on those bindings — the defect this branch fixes. Open PRs now take the full prWatchLimit; terminal-pending bindings drain in an additional slice outside it (20%, at least one, plus leftover budget), bounded at limit + ceil(limit*0.2) pulls.get per poll during the drain. Pins the verification fixture (30 open + 5 newer terminal at limit 30: oldest open observed, its head change delivered) and keeps the removed in-budget variant as a mutation witness. Rate arithmetic updated on the descriptor, i18n (EN/ZH), user guide, and bilingual design docs. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
4c5820c745 |
fix(web-shell): close out managed-host review threads from #15928 (#16203)
* fix(web-shell): close out managed-host review threads #15928 merged with 19 review threads left open. This lands the code fixes and the tests that actually pin them. Code: - shadowDom: the ref-counted release is now idempotent, so calling one install's cleanup twice no longer strips a sibling shell's stylesheet (R2-4). - ManagedArchivedSessions: the host status callback is held in a ref, so a host that stores the status in state no longer re-emits on every render and loops (R2-7). - ManagedSessionsPage: the pane measurement re-runs when the host supplies a session id after mount, so a host that starts in the waiting state still gets a ResizeObserver and the inline task panel (R1-2). - ManagedSessionsPage: the pending-owner link goes through openSession, so it clears the draft/error of the conversation being left and forwards the owner's workspace id (R1-13). - ManagedAgentWebShell: `baseUrl` is optional when `managedAgentProvider` is injected, and a missing one fails loudly instead of resolving requests against `<origin>/undefined` (R1-11). Tests: - shadowDom: a double release keeps the sibling sheet (R2-4). - ManagedArchivedSessions: a fresh inline callback settles (R2-7). - ManagedSessionsPage: pane measurement after a late session id (R1-2), pending-owner navigation carries the workspace (R1-13), the `:host:` pending key keeps sessions apart (R1-4), the empty-state test uses the real failure shape and asserts banner copy instead of a bare truthiness flag (R1-5), the welcome block is asserted absent next to a failure banner (R1-8), and a foreign error banner is dropped on session switch (R1-9). - ManagedAgentWebShell: no provider and no baseUrl fails loudly (R1-11). - managed-host.dom: the pending row assertion matches the row's actual copy instead of a substring it no longer contains (R1-7). Every new assertion was verified red with the fix removed. * fix(web-shell): keep the pending owner's workspace and late status callbacks Three review follow-ups from the #15928 threads: - The pending-owner link resolved the owner's workspace from the served session page, which is filtered, paginated and empty in host mode. When the owner was absent the click forwarded no workspace and the host's handler then cleared the selection. Capture the workspace id on the pending record at submit time and prefer it, keeping the page lookup as the fallback for records written before this change. - The `baseUrl` guard tested truthiness, so `baseUrl=""` — a supported same-origin value that resolves to the page origin — threw during render. Only a missing prop is a configuration error now. - Holding the archived-status callback in a ref keeps the loop closed but also dropped delivery to a host that supplies the callback only after the list settled. Re-emit on the absent -> present transition while keeping identity churn from re-running the effect. Each case has a regression test that fails without its fix. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw0nuit6aw * style(web-shell): apply prettier to ManagedArchivedSessions test The Lint & Static check fails on prettier formatting of the late-status callback's parameter type annotation. Formatting-only change; eslint and prettier now pass on the file. * fix(web-shell): close out managed-host R2 review threads - reject a null baseUrl, not only an absent one: a plain-JS host erases the prop type, so an unresolved config value arrives as `null` and every request resolved against `<origin>/null` - state the requirement in the README and both design-doc languages, which still promised that omitting the provider preserves Java behavior - pin the pending-record write half (the submitted workspace) and the readPending rejection of a non-string workspaceId; both tests fail when the guarded line is removed Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw0s4ubab2 * fix(web-shell): render the shell fallback for a missing baseUrl and pin banner clearing The `baseUrl` guard throws from the shell's own render body, above the ErrorBoundary it returns, so a host mounting before its config resolves saw an empty container instead of RootErrorFallback. The provider now resolves one level below an outer boundary, and the pending-owner navigation test also pins the `setError(undefined)` half of openSession. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw4plfohgl * fix(web-shell): name the shell fallback and assert the contained baseUrl failure The outer config boundary returns an unnamed arrow, which `react/display-name` rejects, so pin its name. The two baseUrl-guard tests still asserted the pre-boundary contract (the guard's throw escaping to the host); the boundary now reports that failure through the shell's own fallback, so assert both halves: the fallback renders and the guard's message still reaches `console.error`. The pending-owner test's added banner pin is reverted: the navigation error renders only when `interactive` is false, so that path cannot show it. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw4rqlfigo * style(web-shell): apply prettier to ManagedAgentWebShell The Lint & Static lane failed at "node scripts/lint.js --prettier" on packages/web-shell/client/ManagedAgentWebShell.tsx: the props interface `extends` clause and the RootErrorFallback JSX needed reflowing. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw52gecrh3 * docs(web-shell): describe the baseUrl guard's observable behavior The guard runs below the shell's own error boundary, so a host sees the fallback render and reads the cause from console.error -- never a throw. Correct the three copies of that contract and cover the resetKeys recovery path (missing baseUrl, then a present one) with a transition test. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw56qpv9h9 * test(web-shell): pin same-origin storage key and the provider reset path R5-1: assert the constructed provider's storageKey resolves against the page origin, so coercing `baseUrl` to undefined can no longer keep the test green while requests resolve against `<origin>/undefined`. R5-2: add a provider-only re-render case so the boundary's provider reset key is pinned, not just the `baseUrl` half. R5-3: document that recovery needs a re-supplied `baseUrl` or provider, because the boundary clears only through its reset keys. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw5q15wki1 --------- Co-authored-by: root <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
a12d6946c2 |
fix(managed-agent): mask digit-led passwords and refuse an empty Harness user info in the upgrade command (#16352)
* fix(managed-agent): mask digit-led passwords and refuse an empty Harness user info
Follow-ups to the review items deferred on #16110 and #16175:
- A refusal hides everything up to a URL's last @, also when the URL
parses: a password that starts with digits and holds a / ? or # reads
as a port to both the URL parser and java.net.URI, so
http://svc:2024#Secret@harness:4170 printed whole. The --server-url
query refusal masks too.
- Without the server's verdict, start refuses an @ before the Harness
host (http://@host, http://:@host), which java.net.URI reads as user
information and the connector refuses, instead of restarting the
Harness for it.
- The health row and start show a status of 0, false or "" instead of
dropping it, and print the URLs they dial through the control-byte
neutralizer.
- A Session id with a lone surrogate fails its row by name instead of
"URI malformed".
- The Flyway reader test covers a path two modules both ship; the user
guide names verify --tenant's --actor condition, and the design docs
state the masking, status and id rules as applied.
* fix(managed-agent): neutralize the DEL and C1 bytes a quoted value keeps
JSON.stringify escapes C0 controls but not DEL or C1 bytes, so a refused
URL or a Session id the verify row echoes could carry U+009B, an 8-bit
CSI introducer, to the terminal, and the id was not bounded. The four
refusals that quote a URL and the id row now go through printable()
after JSON.stringify: C0 controls keep their escapes, DEL and C1 bytes
show as U+FFFD, and the quoted text stops at 200 code points.
* fix(managed-agent): print no password a --server-url carries, and stop predicting java.net.URI
Review round 2 on #16352.
- A --server-url with an @ left after the URL parser reads no user
information is refused: it is the rest of a password that starts with
digits, which the parser reads as a host and a port, so the phase would
dial the wrong host and print the password. check judges --server-url
the same way, since its plan repeats it.
- start's three outcomes and verify's health row print every URL with its
user information masked.
- With the server's verdict false, the refusal names a reason only where
the value shows one the server gives too: a user name or password, a
query or a fragment, or an @ in an authority after //.
- Without a verdict, start takes only a plain http(s) URL, a rule of its
own and narrower than the server's, instead of modelling how
java.net.URI reads user information: http:/@host, a backslash, a tab,
an underscore or a space no longer reach the Harness restart, and an @
in a path still does.
- The design record and the deployment guide say so.
* fix(managed-agent): correct upgrade URL guards and plan output
* fix(managed-agent): keep refusing an @ in --server-url, and spell plan words
|
||
|
|
5faa620af9 |
fix(core): keep the user's instruction last in the model request (#16274)
* fix(core): keep the user's instruction last in the model request
A fresh turn's user entry is assembled from independent sources: per-turn
reminders ahead of the prompt, then hook/Mod context and the memory catalog
behind it. A long multi-part instruction therefore lands mid-entry with tens
of kilobytes of injected context on both sides, and a flash-class model
answered that it had received no task while the instruction sat there.
Reorder the request copy at the single point where every main-turn request is
assembled, moving the injected run that trails the caller's own text ahead of
it. Stored history keeps its shape: the transcript display projection and the
resume receipt both read the hook block as the final part, so reordering the
stored entry would break the projection and re-deliver hook context every turn.
Leading tool results keep their position, a non-leading tool result leaves the
entry untouched, and the volatile reattach tail stays last so the provider
cache breakpoint keeps its anchor. A reminder the caller placed between two of
its own parts bounds the movable run and stays put.
Refs #16259
Co-authored-by: Qwen-Coder <[email protected]>
* docs(core): correct the prompt-context placement prose
The Constraints bullet and the isInjectedPromptContext docstring both claimed a
caller-authored part shaped like a reminder keeps its own order. The rebuild
classifies by shape across the whole entry, so that promise does not hold for
the caller's last part: a trailing part in either envelope is treated as
injected and moves ahead of the caller's text. Describe the mechanism instead,
in the docstring and both design docs.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw1upqc7cl
* fix(core): reorder injected context on every user entry
The send boundary reordered only the pending entry, so request N sent
[reminder, hook, catalog, ...prompt] while request N+1 rebuilt the same
entry from stored history as [reminder, ...prompt, hook, catalog]. The
shared provider prefix then ended at the leading reminder and every
injected block was re-prefilled at full input price on every turn - the
failure mode REATTACH_BOUNDARY_METADATA exists to prevent (#11627).
Applying the same rule to every user entry in the request copy keeps an
entry's wire shape position-independent, so request N+1 extends request
N's prefix exactly. Stored history is still untouched, which keeps the
transcript projection and the resume receipt intact.
Pin it with a two-turn test that asserts the first turn's entry is
re-sent byte-identically, and compare each request with the previous one
instead of comparing the request prefix with stored history.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw25fj8od0
* docs(core): state the reorder's every-entry scope and name the third updated assertion
The every-entry pass landed in
|
||
|
|
f2e9a3fc66 |
feat(channels): add GitHub repository event filter with allowlist and blocklist (#16493)
* feat(channels): add GitHub repository event filter with allowlist and blocklist The GitHub channel processed notifications from every repository the authenticated account can see. The only repository-level control was the group access allowlist, which cannot express "everything except these repositories" and hides a plain event filter behind access-policy machinery. Add repoFilterMode (off / allowlist / blocklist) and repoFilter to the GitHub plugin. The filter runs at notification intake, before the reason filter, journals capped repo_filter skip rows plus one collapsed count row, and never holds the poll cursor. One shared validator rejects an active mode with an empty or malformed list at save time and at connect time; while the mode is off a stored list is ignored, so it can be parked without blocking unrelated saves. Notifications about bound PRs are filtered too; only their PR state watch events bypass the filter. The Web Shell editor gains a Repository Filter select and a Filtered Repositories list shown only while a mode is active, with the visibility and validation gated on the same predicate. Pasted repository or PR URLs are shortened to owner/repo on save; non-repository GitHub URLs are rejected instead of silently shortened. A parity table binds the editor mirror to the plugin's own validator, and the visuals suite captures the new editor in both themes. * fix(channels): close review findings on the repository event filter Address the verify-round findings and the triage review notes on #16493: - F1 (Medium): the reserved-path guard missed sibling GitHub areas, so pasted URLs like github.com/apps/renovate or /features/copilot were still silently shortened to nonexistent repositories that both validators accepted — fatal under an allowlist. Extend the guard with the measured missing roots plus the other known two-segment areas, export the set, and replace the sampled fixture with an enumerated test over every member plus explicit regression pins for the reported shapes. Removing a listed root now fails the pins; the denylist's best-effort nature is documented instead of overclaimed. - F2 (Low): pin the documented ordering — a notification excluded by both gates is journaled repo_filter, not reason_filter. The verify round's M7 mutation (swapping the two blocks) survived the old suite and now fails this test. - Triage boundary note: pin that a bound PR's state watch events bypass the repository filter, so a future refactor of the watch call site cannot silently silence bound sessions. - Docs: soften the URL-rejection claim to "known non-repository areas" in the user guide and both design docs, and state the residual denylist risk. * fix(channels): close the ten inline review findings on the repository filter - R1-1: close the arbitrary-host class structurally. The paste-shortener now only shortens URLs on the channel's own GitHub host (github.com plus the GHES host derived from baseUrl, api. prefix stripped); api.github.com, gist.github.com, gitlab.com and any other host pass through for validation to reject. Add the missing advisories/copilot areas, a snapshot assertion over the whole reserved set (removing any member now fails, which the enumeration alone could not witness), and foreign-host fixtures. - R1-2: seed paginate before connect() in the rejection helper so a successful connect no longer prints an unasserted poll-error backoff line. - R1-3: dialog case opening on a stored active mode — the list shows the stored text and a re-save keeps both keys. - R1-4: pin the notification side of case-insensitive matching (mixed-case subject URL vs lowercase entry) and the normalizer's trim/empty handling. - R1-5: pin the complement of the watch bypass — a notification about a bound PR in a blocklisted repository is filtered, not delivered. - R1-6: capitalized reserved-root variant pins the guard's own lowercasing. - R1-7: vocabulary test binding the repoFilterMode descriptor options and default to the exported REPO_FILTER_MODES, mirroring prWatchEvents. - R1-9: two-poll journal budget reset test (42 rows, two counted rows). - R1-10: extend the Off-retirement case with the discriminating switch back to Allowlist, where a stale error would remount. - R1-11: typing a valid entry (or correcting a malformed one) now retires the shown instruction through refreshErrors, with two no-save dialog cases; Submit remains the only error population site. Mutation witnesses on this head: dropping chatId lowercasing, the host gate, the list-typing retirement branch, and the refreshErrors guard each fail exactly their new test; all restored byte-identical. * fix(channels): exclude the channel's own API host and paths from URL shortening Round-3 review findings on #16493, all closed in this commit: - R1-1 (Critical, fix-induced): the host gate re-admitted the channel's own API host — with baseUrl set, a pasted REST URL shortened to `repos/octo` (api.github.com) or `api/v3` (GHES /api/v3), phantom repositories both validators accepted. Only the WEB host now qualifies: an api.-prefixed baseUrl host contributes only its stripped form, and paths under the baseUrl's own API prefix are excluded. Genuine GHES web URLs still shorten. Pinned with baseUrl actually passed through validation, for both REST shapes; re-admitting the raw API host reddens the test. - R2-1: the baseUrl argument now has a call-site witness on both paths — a GHES draft validates clean and saves ['octo/repo'] through buildChannelUpsertRequest, mirrored in the parity table against the daemon authority. Dropping either argument reddens its half. - Deferred notes (all three closed, none deferred onward): trailing sentence punctuation is stripped before matching (a pasted `…/octo/repo.` no longer becomes the phantom `octo/repo.`); `enterprises` joins the reserved set (snapshot updated); and a dialog case witnesses the parked-list round trip — hidden while Off, restored with its text on re-enable. Mutation witnesses: re-admitting the raw API host, dropping the validator baseUrl argument, and dropping the save-path baseUrl argument each fail exactly their new test; all restored byte-identical. |
||
|
|
752e399b3b |
feat(web-shell): surface task Q&A activity above the composer (#16037)
* feat(web-shell): add a waiting minigame to the composer Show a dismissible horizontal-Tetris strip above the main composer while a prompt is streaming. The game closes when the turn ends, ignores editor keyboard targets, and stays out of split panes and coarse-pointer devices. Add engine and component tests, EN/ZH copy, design docs with page screenshots, and a Chromium E2E scenario. Refs #16036 * docs(web-shell): point the waiting minigame design at the committed E2E spec * fix(web-shell): move the waiting minigame trigger into the status line Replace the full-width waiting strip with a compact gamepad button at the end of the streaming status row. The main composer now owns the open state, and the board mounts above the status row only while open. Keep the game's window keydown listener stable through a ref so rerenders cannot drop the Escape that collapses the board, and update the component tests, E2E scenario, screenshots, and both design docs. * fix(web-shell): keep the waiting minigame scoped and recoverable Pause and resume correctly after the tab is hidden, scope game keys to the focused card so Escape no longer arms stream cancellation, let focused buttons keep Space, restore the sound toggle, and keep the board grid out of the aria-hidden wrapper that also held the overlays. * fix(web-shell): keep the waiting minigame recoverable and scoped Resume gravity after the tab becomes visible again, create the audio context when sound is enabled, let the card's own buttons keep Space/Enter, leave Escape to open dialogs, and keep the restart button out of the aria-hidden board. The main composer now lets the open game own Escape so the first press collapses it without arming stream cancel. * feat(web-shell): add a waiting card with activity, usage and games Open a waiting card from the streaming status button. The card starts on the task's 44-bucket Q&A activity heatmap, switches to token usage instead of a credits balance, and places a Qwen mark that opens a Tetris/Snake picker. Games share the 44x7 grid and offer back-to-activity and switch-game actions. Add the activity and Snake engines, component and E2E coverage, a local harness with sample data, and design docs with page screenshots. Refs #16036 * feat(web-shell): refine waiting card activity and game intros Add hover details and click-to-locate to the activity heatmap, start a random game from a delayed cold-cell Qwen entry, and play a QWEN CODE intro that flies the blocks into the Tetris start cluster. Snake keeps its intro static, eats an apple emoji, and starts at a slower speed. * feat(web-shell): show task activity in a daily calendar Replace the task-span buckets with a fixed 44-week day calendar, including month labels, per-day hover details, click-to-locate and a stable card frame across activity, usage, intro and game views. Gate the Qwen game entry behind the waiting games configuration so the activity card stays the default experience. * fix(web-shell): show a gamepad icon on the configured game entry Replace the Qwen brand mark in the calendar's game entry with a gamepad icon, remove the now-unused Qwen logo component, and let the local harness read the waiting games config so ?waitingGames=1 opens the preview with the entry visible. Game screenshots and docs now describe the gamepad mark. * fix(web-shell): read cached tokens from the connection fallback The usage card falls back to the connection's token usage before any assistant usage frame arrives, but the fallback read the message-level cachedTokens field while the connection object carries cachedReadTokens. Accept both names and pin the connection shape in the unit test. * fix(web-shell): keep the game entry hit area inside its cell Give the calendar game entry a positioning context so its forgiving hit area stays bounded when prefers-reduced-motion disables the intro animation and removes the transform that used to establish it. Cover the reduced-motion path with an E2E that hovers and clicks a day while the entry is visible. Also reuse one Intl formatter pair per locale, format task token totals above 1M as M, and drop the unused no-activity copy. --------- Co-authored-by: yungsen.hys <[email protected]> |
||
|
|
f387c3a75a |
fix(ci): recreate git metadata before the publish-triage checkout (#16494)
* docs(ci): state the real publish-triage safety argument The publish-triage header comment claimed a 'clean runner', but the job routes to the persistent ECS pool like publish-tmux and runs without a container. The guarantee that actually holds is trusted-code-only: a sparse checkout pinned to github.sha, never agent-provided code, and a publisher that imports only node builtins plus one relative helper, so a reused workspace offers no bare-specifier node_modules to hijack. Reword the comment to say that, per the non-blocking note in the #16468 re-review. Comment-only change; no job, timeout, or behavior delta. Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): recreate git metadata before the publish-triage checkout The patrol closeout on the comment rewrite checked the claims against the file and found two gaps. First, publish-triage was the only job reaching actions/checkout on the shared pool without recreating the workspace .git: an earlier agent run owned that workspace and could have planted hooks/config that checkout's git commands would execute in the one job holding the write PAT. Add the same Recreate runner Git metadata step the triage/tmux/verify jobs run before their checkouts, and add publish-triage to the qwen-runner-checkout-workflow matrix that behaviorally executes it (removal witness: deleting the step turns the new entry red). Second, narrow the header: drop the 'it is safe there' certification in favor of naming the controls, and replace 'sole writer' — the triage job's reaction and lifecycle host steps also write with the PAT — with 'the only path from the agent's drafts to GitHub'. Design pair updated in both languages. Co-authored-by: Qwen-Coder <[email protected]> * docs(ci): keep the new step comment off the PAT-holder claim The publish-triage step comment and the design pair said the checkout runs in "the job that holds the write PAT", which is the same overstatement this PR removes: the triage job's reaction, status comment, and finalize step also write with that PAT. Name the publisher as what it is instead, so the header's new parenthetical and the design prose agree. Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): pin the publish-triage safety claims and derive the checkout guard Addresses the remaining review findings in this PR. R1-2: publish-tmux's banner still claimed a clean GitHub-hosted runner while its runs-on selects the ECS pool; reword it to the controls that actually hold (checks out nothing, runs no repository code), routing untouched. R1-4: the publisher's bare-specifier-free import closure is load-bearing for the pool argument but nothing pinned it; add a meta-test that scans the closure and matches it against the sparse-checkout list (witness: adding import 'yaml' reddens exactly that test). R2-2: the runner-checkout guard's hand-typed job list is what missed publish-triage; derive the qwen-triage entries from pool-routed jobs that check out, so the next such job reddens with no test edit (witnesses: a new pool checkout job without the reset, and deleting publish-triage's reset, both redden). R2-3: pin publish-triage's ownership-restore fallback chain with assertUnconditional (witness: deleting the sudo fallback reddens). R2-1: the design pair's Files-affected list still said the runner-checkout test needs no change; correct both languages and narrow the pinned-by parenthetical to what each suite actually pins. Co-authored-by: Qwen-Coder <[email protected]> * test(ci): make the new pins layout-independent, floored, and complete Round 3 found three holes in the pins added by the previous commit, all in the guards themselves. R3-1: specifiersOf used a single-line pattern, so Prettier-wrapped imports were invisible in both directions — a wrapped bare specifier kept the closure test green while the pool job would die ERR_MODULE_NOT_FOUND, and a wrapped relative one dropped a needed entry from the expected sparse list. Span newlines, hoist the scanner to module scope, and fixture-pin it (wrapped relative, wrapped bare, and the publisher's own multi-line node:fs import); those three fixtures were verified red against the old pattern and green after, and the sparse comparison stays exact equality. R3-2: the derived job set had no floor, so rewriting one pool job's runs-on into a vars lookup silently dropped its row; add an arrayContaining floor that names the lost job while still admitting new pool jobs with no test edit. R3-3: the ownership test's name promised the full fallback chain but assertUnconditional matches chmod unanchored, so deleting the sudo chmod arm stayed green; pin that arm in this test rather than the shared helper, which has five other call sites. Witnesses: wrapped bare import reddens the closure test, a vars-lookup runs-on reddens the floor naming publish-triage, and deleting the sudo chmod arm reddens the ownership pin — each with byte-level restore. Co-authored-by: Qwen-Coder <[email protected]> * test(ci): prove the sparse checkout by loading it, not by scanning syntax The APPROVED review deferred three findings against the closure guard, all instances of one root cause: the guard modelled JS module syntax with a regex. Proven on the shipped code — a 1164-character wrapped bare import left it green (the 400-char bound), and an out-of-tree helper turned the byte-offset slice into the garbage entry /.github/scripts/elper.mjs. Replace the scanner with the condition the pool actually creates: materialize exactly the sparse-checkout list into a temp tree, assert no node_modules exists above it, and import the publisher there. Sufficiency is the load succeeding; necessity is each .mjs entry omitted making it fail; the owner policy is tied by resolving the publisher's own new URL literal and requiring that exact path in the list, instead of an unanchored substring plus a hard-coded entry. This removes the scanner and its layout fixtures rather than patching them again — wrapped imports of any length, prose that looks like an import, and out-of-tree helpers are all covered by the load itself. Witnesses: the long wrapped bare import that was previously green now fails, as do a short bare import, an out-of-tree helper, a helper dropped from the list, a stale extra entry, and a moved owner-policy path; each with byte-level restore. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
e99286813e |
fix(ci): preserve Maven suite results across cache write-back failures (#15274)
* fix(ci): scope the sdk-java test lock to the cache write-back (#15262) The Managed Agent fast tier can fail without reporting a test result after waiting out the per-host SDK Java lock. #15128 scoped the MariaDB lane's lock to its cache write-back, but the test job still held the same lock across the SDK and Managed Agent Maven suites, so a long full-tier suite could starve the fast tier on that host. Seed the test job's Maven repository from the host cache, run both suites unlocked from that job-local copy, and take the ticketed host lock only for the cache write-back. The write-back children close fd 9 so a cancelled job's survivor cannot retain the lock. Pin the new order and the write-back fd closures in the workflow tests, and update the fair-lock design docs and the workflow size baseline. Fixes #15262 Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): keep sdk-java cache write-back best-effort (#15262) * fix(ci): keep the sdk-java gate result across the cache write-back (#15262) The write-back runs after the suite, so the step's exit code must come from the suite: both lanes now track the Maven status, demote only a lock-wait timeout with a step-summary record, and redden when the queue itself is unusable. The wait drops to 1800 s because it guards only the bounded copy; the seed stops hiding cp's diagnosis while staying quiet on a fresh host; the design docs record the head-of-queue stall the shorter hold does not bound. * fix(ci): revert the sdk-java lock helper to the PR footprint (#15262) The previous round changed .github/scripts/sdk-java-lock.sh, which this PR never touched; the deterministic gate rejects that expansion. Restore the helper and keep both call sites on the pre-round `if !` shape, retaining the 1800 s wait and the step-summary record. Drop the status-2 pins and tests that only existed for the reverted helper, and record the unusable-queue gap as a follow-up in the design docs. The timeout-returns test stays. Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): de-alias every resolver tracking file and guard an unusable lock queue (#15262) * fix(ci): de-alias Maven install metadata in the sdk-java seed (#15262) Co-authored-by: Qwen-Coder <[email protected]> * test(ci): restore the per-job Maven seed binding (#15262) * fix(ci): keep sdk-java resolution metadata out of the host cache (#15262) The seed drops maven-metadata-*.xml hardlinks, but the write-back still published job-local metadata with cp -n, where the first publisher's copy could never be refreshed. Exclude it at both write-back sites and pin the seed and write-back name sets to each other. Also witness each suite-status exit with an exit-1 case, restore the lock test's combined-stream assertion, and bring the design docs' acceptance criteria and fault arms up to date. * test(ci): carry the deferred-status helper into the sdk-java slices The tolerated-skip arms now call mvn_report_failed_suite, which the step defines above the lock block. Every slice that starts at lock_setup_fault() or at the write-back therefore ran bash -e without the definition and died with 127, which read as a code failure rather than a missing prelude. * docs(ci): clarify remaining sdk-java cache hardening scope * fix(ci): bound failed MySQL cache waits and exclude resolver residue Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): clear inherited resolver state in the Maven cache seed prune The seed de-alias prunes carried -links +1, which restricted deletion to the hardlinks cp -aln created in this job. RUNNER_TEMP survives across jobs on the pool, so a previous job's mutable resolver state (nlink 1 *.lastUpdated, _remote.repositories, resolver-status.properties) survived in the job-local repository and could redden an otherwise green Maven lane. Drop -links +1 from all four seed prunes so inherited state is cleared again; the write-back's -links 1 delta filter is unchanged and the seed stays best-effort (2>/dev/null || true). Move the deAlias test pin with the fix and add a regression witness that plants single-link markers in the job-local repository before the seed body runs and asserts they are gone (red with -links +1 restored, exit 1; green with the fix, on macOS and Linux). Re-record the sdk-java.yml .size-baseline entry (153970 bytes) and synchronize both design-doc languages, including the nine-coordinate set the main merge brought in. --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Qwen Autofix <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: yiliang114 <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
3f99e022b7 |
feat(cli): have an Ink TUI turn send the round its tool batch continues with (#16428)
With experimental.unifiedAgentLoop on, the shared tool batch's completion step hands the next round to the turn, which takes the input queued for the tool boundary and sends it. The batch keeps the scheduler and the TUI's activity until then; a turn that does not wait leaves the round to the batch. The Ink parity suite adds retry and queued-input scenarios. Part of #15327. |
||
|
|
11c83aa21e |
feat(managed-agent): allow concurrent child questions (#16484)
Release local execution membership while questions wait, recover the original runtime owner, and isolate root-page action selection and drafts. Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
acea5b1335 |
docs(design): correct the daemon PR state transitions design (#16487)
Fold the unposted findings of #16246's final docs-only review, and what the audits of those fixes turned up, into both language versions: - v0.26.8 (2026-11-05) ships the watch:* keys of #16258, so they follow the same deprecation window as prWatchEvents. - The view identity check normalises both URLs as pullRequestKey does (any path after the number and the port ignored) and compares host and number only. - Latency is stated as nominal (one interval, or ceil(n/30) for a view set of n), with the cases that make it longer. - The shared template contract moves key and template validation, chain resolution (event vocabulary and comment lane as parameters) and rendering from variable values into channel-base; %sender% is GitHub, %repo%/%url% keep the winner's bound URL spelling. - Every watch-file change reads and writes within one hold of the per-PR lock; a daemon claims the watch only when its refresh timer runs and its uid owns the watch directory and files, so writes stay temp-and-rename; a non-owner writes nothing. - The alias guarantees, the untrusted-primary exposure and the validation plan are updated to match; the Chinese twin carries the technical templates verbatim. |
||
|
|
301b61ddcc |
fix(channels): deliver plain conversation comments to PR-bound sessions (#16416)
* fix(channels): deliver plain conversation comments to PR-bound sessions With deliverToBoundSessions on, a plain comment on a bound PR still had to pass the repository's mention requirement before it reached the session. On threads GitHub lists as a mention, an assignment or a review request, the comment lane dropped it whatever the setting. On subscribed and similar threads the bound path's preflight rejected it, and the workaround, turning requireMention off, also made the channel answer plain comments on unbound PRs and issues there in public. Threads the bot account opened or commented on were not affected. The mention requirement keeps ambient chatter out of public replies; it is no authorization boundary. The private path now waives it through a preflight option that admits a message whose only group rejection is the mention requirement. The waiver covers a comment, not the PR's own first-contact description, from a sender the repository's allowlist names, in a repository the group gate admits, unless the sender is in operators: a bound session usually acts on GitHub as an operator's account, and its own comments would come back to it as turns. Sender, repository, pairing and message-route checks still run, and repositories that admit any sender keep the requirement. On the mention-type threads the lane admits such a comment only when the private path would deliver it: the routes match and a target resolves for it as a comment event, so templates keyed by the thread's reason do not apply to it. Its task is marked private-only: it never takes the channel path or posts a public error comment, and it is retired when no bound session is left. Comments only the waiver admits are dispatched after the rest of the lane and the first-contact body, so their outcome, a deferral or a gone session evicted from the binding map, cannot change how those are handled. Two failure paths that predate this change no longer post a public error comment for a bound event when the task store fails: the gone-session deferral under a partial scan and the retirement at the deferral cap. * test(channels): pin the default route, audit refusal and window hold of waived comments - Admit an unprefixed plain comment through `defaultMessageRoute`. - Keep a waived comment's key out of the cursor when an unreadable publication audit refuses its dispatch, and deliver it once the audit reads again. - Feed the first-contact body on the re-read when a failed waived delivery holds the notification window. |
||
|
|
dd4515ca27 |
feat(ci): isolate triage agent credentials and run all triage on ECS (#16468)
* feat(ci): isolate triage agent credentials and run all triage on ECS The triage agent carried a write PAT, so comment reruns and external fork-PR triage had to stay on ephemeral hosted runners while hosted capacity is tight and the ECS pool has headroom. Wire the token-isolation follow-up the workflow already documented: the agent reads GitHub with the job's read-only GITHUB_TOKEN and expresses every write as a draft; a separate publish job validates and executes drafts with the bot PAT, using one publisher script shared with local /triage runs. Contain the agent job, launch the CLI directly behind the hardened loopback model proxy, and move the deterministic maintainer resolver into the publisher. With no write credential in the agent environment, route every triage trigger to ECS behind the existing kill switch, and retire author_can_write. Markers, bot identity, and the finalize workflow's approve-on-green protocol are unchanged. Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): bump triage CI budget and address triage review findings The new publish-triage job crossed the .ci-budget ratchet (7->8 jobs, 280->290 declared timeout-minutes), aborting Lint & Static before any lint or helper test ran. Bump the budget entry, mirror the artifact-name fallback expression on the upload side so an early-death run's drafts stay addressable, and enforce SKILL.md's process-label rule at the publisher chokepoint (welcome-pr, maintainer, help wanted, good first issue) with fail-closed tests, aligning the issue-workflow instruction that previously contradicted the rule. Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): harden the triage draft pipeline after an adversarial audit Six findings from a multi-round pre-push audit, each with a mutation witness: kill lingering agent-user processes before the drafts strip/upload so a detached child cannot re-plant symlinks in the race window (upload-artifact follows them and RUNNER_TEMP is host-shared; unkillable survivors drop the draft set deny-by-default); lstat the draft containers themselves in the publisher preflight, since a symlinked bodies/ redirects the realpath containment base; reject cross-kind actions (review/close-pr on issues, close-issue on PRs); install util-linux for runuser as the sibling lanes do; make the mktemp'd proxy-wrapper directory traversable by the agent user so the gh/git wrappers do not silently fall out of its PATH; pin the artifact-name symmetry, the kill-before-strip ordering, and the toolchain install line. Design pair updated in both languages. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
1ea05364a6 |
perf(managed-agent): take tool execution admission off tenant placement (#16478)
* docs(managed-agent): design tool execution admission without tenant placement Admission still takes tenant placement on every tool call. It orders admission's plain read of the harness drain fence against requestHarnessDrain, the only fence writer that holds no row admission takes. Lifecycle phases already hold the Session root, and the storage fence is backstopped by migration's retirement check. The design has the drain take the Session root after placement, and lets an ordinary session-class admission with an existing root skip placement, taking the root as its first statement so a REPEATABLE READ snapshot cannot predate the drain. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(managed-agent): record the storage alias order blocker for placement-free admission Co-Authored-By: Claude Opus 5.5 <[email protected]> * perf(managed-agent): take tool execution admission off tenant placement Every tool call still took tenant placement in admitExecution. Placement ordered admission's plain read of the Harness drain fence against requestHarnessDrain, the only fence writer holding no row admission takes. requestHarnessDrain now locks the Session root after placement. An ordinary session-class admission with an existing root, no lifecycle authority and no storage or LOCAL storage skips placement and locks that root as its first statement, so its REPEATABLE READ snapshot cannot predate a committed drain. Other admissions take placement first, as before. completeStoppedSessionRelease locked the storage alias and owner rows before the binding, the reverse of admission and createSession. It now locks the binding first, so a placement-free admission cannot close a cycle with it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(managed-agent): format the admission design with the pinned Prettier Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
06a0cb3250 |
perf(managed-agent): take runtime binding control off the tenant guards (#16480)
Binding control took tenant placement and the publication guard for every session-class binding, so each periodic lease renewal queued on both tenant-wide rows. The publication guard only tells whether a CSI profile original's retention root exists; any other binding is answered null or refused whatever it says, so lockBindingControl now takes it for profile bindings only. Claim, renew and release change only the operation lease and move no binding out of its state. Like dispatch, they now decide the CSI pin under the Session root alone when it exists. compareAndSet keeps placement, which it takes next and which must precede the root. Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
45cde86998 |
feat(cli): align managed agent basic tools with daemon (#16445)
Expose glob in ordinary file sessions, durable todos in Shell sessions, and PDF page selection in Hosted read_file. Restore saved todo state on session reload while keeping exact current-schema validation and restricted resident/CSI tools. Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
7cb46d9fed |
feat(hooks): add FileChanged notifications for local sessions (#13969)
* feat(hooks): add FileChanged notifications for local sessions * fix(ci): preserve shell gate coverage and trim transcript data * fix(ci): flush debug prompts and isolate git fixtures * fix(hooks): respect ACP watcher ownership and skill registration * test: stabilize hosted recovery and failed spawn fixtures * fix(ci): budget the complete hosted integration fork * fix(runtime-broker): accept MariaDB CSI guard metadata * test: align schema baseline and hook dispatch waits * fix(ci): allow hosted shared fork to finish all classes * fix(hooks): preserve file watching across errors and cwd changes * fix(ci): isolate reference archive extraction per run * test(ci): keep reference archive checks dependency-free * fix(hooks): constrain recursive watches and repair CI * test(web-shell): synchronize verified connection return * fix(ci): correct runtime broker callback indentation * test(web-shell): match the managed workspace registration label Keep the remote workspace registration smoke checks in sync with the service label added by unified workspace navigation. Preserve exact text matching and all registration and replay assertions. * fix(ci): reduce standalone runtime fixture overhead * fix(hooks): ignore implicit atomic-write temporary files * test(managed-agent): recover initial node port conflicts * fix(hooks): observe parents before discovery anchors disappear * test(managed-agent): await stream cleanup before recovery teardown * test(managed-agent): align hosted shell fixture capabilities * test(managed-agent): let OS choose initial harness port * fix(ci): restore runner-local Java cache permissions * test(runtime-broker): use native execution profile paths * test(managed-agent): bind valid workspace upload probes * test(managed-agent): reuse the upload probe digest fixture * test(managed-agent): bind hook fault listeners to available ports * test(managed-agent): align the native profile path fixture * test(managed-agent): Wait for projection refusal retries * ci: Prepare daemon Java cache before Node preflight * test(managed-agent): check channel model calls after delivery * fix(hooks): observe explicitly registered symlink targets * test(managed-agent): separate consumption from relay snapshots * test(managed-agent): target tamper injection by resource key * test(managed-agent): exhaust attestation EOF retry in fault gates * fix(ci): bound RocketMQ release downloads * test(tui): await the restored assistant before capturing history * fix(ci): use a checksum-pinned RocketMQ mirror * fix(ci): provision missing browser test fonts * fix(ci): quote the font probe environment * fix(ci): prepare browser fonts without elevation * test(core): compare complete prune reports without ordering * fix(ci): repair only runner-local Java tool caches * test(managed-agent): qualify fixture JDBC templates * fix: isolate reconnect hooks and cancellation test fixtures Skip chat initialization so MCP reconnect cannot re-enable file hooks, and keep cancellation fixtures isolated from the background worktree scanner. * fix(ci): tolerate a missing Java cache helper on resumed releases Resuming an existing release checks out a tag that may predate the helper, which failed all five Java jobs before setup-java ran. * fix: finish the main merge for harness deadlines and workflow size Use the renamed settle-timeout accessor in the merged hosted-harness tests, and record sdk-java.yml's new size after the resumed-release cache guard. * style(managed-agent): restore import grouping in merged verification tests * test(managed-agent): require consumed child result mirrors * test(web-shell): center repository settings capture --------- Co-authored-by: yiliang114 <[email protected]> Co-authored-by: yungsen.hys <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
1853b1505f |
fix(subscription): isolate activation and restore sign-in recovery (#16453)
* fix(web-shell): wait for session before checking subscription commands * fix(cli): keep signed-out subscription startup in the composer * fix(subscription): preserve model selection across sign-in * fix(subscription): align sign-in with model selection scope * fix(subscription): apply reasoning defaults across models * fix(subscription): tag Qwen web links by client * fix(subscription): prefix Qwen web client tags * fix(subscription): attribute Qwen web links by paywall scene * fix(subscription): recover historical sessions with saved API models * fix(cli): remove the signed-out subscription startup notice * fix(subscription): support standalone desktop activation and expose errors * fix(subscription): isolate activation and session model selection * fix(subscription): enforce auth policy and restore home login * test(cli): preserve the default initializer mock on restore --------- Co-authored-by: luoyun <[email protected]> Co-authored-by: 易良 <[email protected]> |
||
|
|
1b1f4455d9 |
ci: build the PR tree once and shorten the required path (#16450)
* ci: build the PR tree once and shorten the required path The no-AK integration gate, the four web-shell smoke shards and Lint & Static each rebuilt the tree test_build already uploads, and the build cache rarely helped: most runs logged "29 of 29 workspaces changed" because the arch-keyed cache was split across the mixed x64/ARM64 pool and a prefix restore returned the newest entry instead of the base commit's. - test_build restores the build its base commit saved (ci-build-cache.mjs key --at), drops runner.arch from the key, and runs the serve fast-path closure and core subpath checks against the built tree. - The no-AK integration gate unpacks that tree and runs as three shards; integration_no_ak_result keeps the required check name. - The web-shell smoke unpacks the same tree instead of building; consumers recopy the opentui native assets for their host (host-assets). - The standalone runtime regression moves from unit shard 1 to shard 4. - A java_only profile skips the TypeScript lanes for changes confined to the Java packages; Lint & Static runs the scripts/tests suites that name them. - Drafts skip the integration gate and the browser smoke; ready_for_review starts the full run. * ci: check the settings schema in test_build, which has the core dist Lint & Static only had core's dist because the serve fast-path check built it; with that check moved, Generate settings schema failed to import it. * ci: keep the standalone regression on unit shard 1 Moving it did not shorten shard 1: the regression runs in the side lane, in parallel with the CLI and core suites, and shard 1's time is its CLI slice (576 s against 147 s on shard 4 for the same 181 files). That imbalance is left to duration-based sharding. |
||
|
|
e777fc7502 |
feat(managed-agent): enable automation by default (#16448)
* feat(managed-agent): enable automation by default Co-authored-by: Qwen-Coder <[email protected]> * style(docs): match pinned Prettier formatting Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
ff775787f5 |
feat(channels): journal private bound-session delivery and scan health (#16237)
* feat(channels): journal private bound-session delivery and scan health The event timeline could not tell the private delivery lane's story. A prompt injected into a PR-bound session ended on the same `running` word as an event still in flight, no task row named the session it reached, and the three private watch retires plus the dead-letter left only stderr behind — so a silent consumption looked like a quiet repository. A bound event rejected by the channel's own gates was worse than invisible: its skip row was followed by running rows, reading as a delivery. The private delivery method returned a boolean meaning "settled", which conflated an injected prompt, a private retire and the public fallback. It now returns a verdict, and the task runner derives the journal's terminal word from it, so an event that was never delivered can no longer be recorded as delivered. Routing, fallback, retry-budget and cursor decisions are unchanged. The binding scan is a new hard dependency: while it has never succeeded the cold-start gate stalls every notification, yet the poll reports ok. Degraded rows now surface that beside the stderr lines the adapter already throttles, and on the edge of the existing watch-reconcile flag, so a permanent outage cannot flood the journal. Attaching this to the poll detail was rejected: both cold-start gates return before the detail is set, which is exactly when it would matter. No new event type and no protocol change — an outcome is already an open string. The dead-letter's message body and its failure-path payload dump stay out of the journal. Also labels `failed_ambiguous`, which was already written but rendered as a raw token. * fix(channels): make the bound-delivery journal claims true Review round 1 found sixteen places where the journal, its labels or its documentation promised something the code did not do. All sixteen are addressed here; none is deferred. Three were ordering or attribution defects in the new rows. A watch retire was journaled before the removal it describes, so a throwing removal left a terminal claim for an event the next poll re-runs and retires again under a different reason. Rows written while a delivery is cleared lost the session they were about, which is the one fact the private lane's attribution adds. And the pre-claim watch retire sliced the loaded state before saving with no rollback, so a failed save lost the event with no trace at all — the comment claimed the opposite. Two were counts that measured something other than their label: the partial scan published the routing map's size, which counts bound PRs, as a session count; and the streak advances per binding scan, not per poll, because a gone-session retry refreshes again inside the same poll. The session count now comes from the scan's own rows, and the streak is labelled as scans in both languages and in the stderr lines that share the counter. Re-cadencing the counter itself would change the throttle timing the previous PR shipped, so the label moved instead. Two were missing surfaces: the pending-queue-overflow retire wrote only stderr and the dead-letter file, and an unreadable watch state was reported as a pending backlog whose stated remedy cannot clear it. The overflow now journals a body-free skip, and the watch guard classifies a read failure separately while its gate stays fail-closed. The recovery signal the prose promised did not exist: a poll/ok row is written after every poll that does not throw, including all of an outage, so it cannot mark the end of one. The watch guard now journals its clearing edge, and the prose says why ok rows cannot serve. The rest were documentation and test-coverage corrections: the protocol reference promised detail.streak on every degraded row when only the binding-scan rows carry it; the design file map certified the protocol reference as unchanged while this branch edits it; the override's stated consequence was backwards; and five guards had no witness or only half of one. Each now has a test, and the suite grew from 438 to 444 in the GitHub package. Verified by 37 mutations, all killed. Two initially survived and were investigated rather than accepted: a loose sessionId count that three unattributed-row-free rows still satisfied, and a filter that matched no test after a rename. Both were tightened until the mutation died. * docs(channels): align the journal ledger with the round-2 verification The validation section still cited the round-1 mutation count after the review fixes added fifteen more, and the Chinese half had lost the count altogether during the rewrite. Both now state 37 applied and 37 killed. Also records why two mutations initially survived and what was done about them, since a sweep that cannot distinguish "no test ran" from "the guard held" is not evidence: a loose sessionId count that three correctly attributed rows still satisfied, and a filter left matching nothing by a test rename. The Web Shell line now mentions the absence coverage too. * fix(channels): keep the lost bound session named on every journal row The second review round of the bound-session journal found fifteen gaps. Three were consequential, and two of those were introduced by this PR's own previous round: The gone-session retry never updated the local delivery, so with two followers both gone the terminal row named the first one — a session evicted a step earlier — while the row above it named the last. An operator reading "which session lost this event" would open the wrong workspace. Remembering the target only for the transition that clears the delivery is not enough: once the clear is persisted, every later poll re-enters the absence gate with no delivery to read, and the dead-letter at the deferral horizon reads a record that has none either. A driven run of that sequence left 239 of 242 rows unattributed, with the terminal row naming nobody — the one fact this lane's attribution exists to supply. The lost target is now persisted as attribution only; the delivery is deliberately not restored, since that would re-enter bound delivery and change routing. The overflow retire was journaled before its queue shift was durable, with no rollback, so a failed save left a skip row for an event a restart would later deliver. It now saves before it journals and restores the shift when the save throws, matching the sibling pre-claim path. Also: the partial-scan session count now includes the sessions retention keeps routing (a draining workspace lists none while still delivering to three), and both pre-claim retires carry the frozen target so the timeline and the dead-letter agree about the same event. Four coverage gaps whose mutants left the whole suite green are closed: the guard keeping the partial-scan row off the cold-start path, the session on the no-candidate retire, a second healthy poll so the recovery row's edge trigger is distinguishable from an unconditional write, and both a shape test and a Chinese render test for the interpolating labels. The docs said more or less than the code: detail.sessions is carried by one row, not by every partial scan; the normative sessionId definition read a retire chip as a delivery; the emit-point inventory missed the overflow site; and bindings_partial was described as recovering implicitly when a successful multi-page scan is its producer. Auditing that last correction found the stderr census was still incomplete — the lane has five recurring health and capacity sites, not three — so it now names all five, ranks them by consequence and states its own scope. * docs(channels): define what the partial-scan session count actually counts The count became listed-plus-retained when retention was included, but three surfaces still described the old value. The protocol reference called it "the sessions the map still routes to", which is wider than the code: the map also skips channel-owned sessions and unparseable binding URLs. Both design halves still said it was "the scan's own row count", which is narrower than the code and was the exact wording the review asked to correct. Define it as a session-row count — the rows this scan listed plus the sessions retention keeps routing, bounded by the retention cap — and say explicitly that it is not the number of entries surviving the map's own filters. Also record that "listed" in the row label and in the stderr line is shorthand for listed-or-retained and that both print the same number, so the wording cannot quietly drift apart from the value again. Documentation only: the adapter and its tests are byte-identical to the previous commit, so every runtime result reported for this branch still applies. * fix(channels): close the review gaps in the private-lane journal Address the eight open suggestions and the five deferred findings from the round-3 review, each with a mutation-verified guard (10 new mutations, 10 killed; one cap-count fixture initially survived because the first empty partial already capped lastBindings, and was rebuilt so retention is above the cap exactly at the emitting scan). - R1-15: the overflow dead-letter append and the sidecar save now fail independently. A failed save rolls the shift back, names the watch-state path instead of blaming the dead-letter file, drops the payload dump (the body is already durable), and remembers the head so the next detection skips the re-append. The save-throws test now drives a second detection and asserts exactly one dead-letter line. - R2-6: the pre-claim skip comment and both design languages now say only the overflow retire writes a dead-letter record; watch_no_candidate and no_role_matched leave the skip row as the event's only trace. - R2-3: bindings_cold_empty no longer claims implicit recovery — an empty partial is a successful scan, the streak resets only on a complete or non-empty snapshot, and past the escape the silence is the throttle maxing out, not recovery. - R3-1: a 1100-session fixture pins detail.sessions to the 1024 retention cap at the emitting scan; swapping retainedCapped.length for retained.length reds it while the two sibling count tests stay green. - R3-5/R3-6/R3-15: the design's outcome cell lists failed_ambiguous and degraded, the watch-guard bullet admits the read-failure error field, and the sessionId carve-out covers every row written on a clear plus the pre-claim skip; the chip sentence says reached-or-lost. Both languages. - R3-7: the three private-retire watch tests assert the public path stayed empty; leaking the envelope through handleInbound reds them. - Deferred: the gone-horizon attributes the in-memory task before retiring and the dead-letter record names the lost session; the watch-guard row re-journals when the blocking cause changes; the subscriptions-off recovery edge has a test; the recovery row renders Recovered/已恢复 instead of Degraded (wire outcome unchanged); a gone-retry fixture pins the scan-scoped streak at 10 inside poll 4. Verified: channels/github 595 passed, web-shell channels 343 passed, eslint and prettier clean; design mutation ledger updated to 47/47. * fix(channels): rework the overflow retire ordering and close round-4 review Critical R4-1: the overflow branch saved the sidecar mid-pass, committing the advanced baseline before the pass's later events were queued; a crash then never re-derived them. Remove the mid-pass save entirely — the head is sliced in memory only, and the baseline and full queue commit together in the single end-of-pass save, with the skip rows journalled after it. The dead-letter record is now the retire's commit point. A lazily loaded, file-backed key set (restart-durable, replacing the process-local map) skips the re-append (R4-16), and both drains recognize a dead-lettered head and complete its retire instead of delivering it, so no event is both delivered and filed as retired undelivered (R1-15 follow-up). Also addressed: - R4-12/13/14: terminal, transition lost-session and pre-claim rows carry sessionWorkspaceCwd so Open-session reaches a session in another workspace. - R4-11: the no_role_matched comment states the real ordering — the dedupe cursor is in memory until the base saves it, so the skip row is at-least-once. - R4-15: already_delivered_other_role is documented as a defensive, unreachable guard and dropped from the published vocabulary. - R4-2/3, R3-5: §1 outcome cell adds suppressed/posting, reason covers task and poll rows, sessionId carve-out adds cancelled and distinguishes live running rows. Both languages. - R4-4/5/6: the pre-claim census acknowledges the role-aware drain's pre-existing fourth retire; the retire-carries-session rationale notes the preflight exception; the dead-letter write-failure note names the non-recoverable public-suppression exception. Both languages. - R2-3/R4-7: cold-empty reset is keyed on the routing map and trust flag, and the escape's lane-open claim is qualified for the refresh-failure producer. - R4-8/9: the recurring-site census adds the credential/rate-limit pass abort and corrects the no-existing-flag claim for the stale-map refresh failure. - R4-10: watch-guard error wording covers reconciliation failure too. - R4-17/18/19: openTarget joins the interpolation guard with a ZH render assertion, the EN streak label says consecutive, and ZH degraded/retired/ recovered labels get render assertions. Verified: channels/github 601 passed, web-shell channels 331 passed, eslint and prettier clean; 4 new mutations killed (mid-pass save, file-dedupe, drain guard, workspace lookup); design ledger updated to 51/51. --------- Co-authored-by: verify-round <verify-round@local> Co-authored-by: verify-pr <verify@local> |
||
|
|
a1d713e747 |
feat(cli): report interactive program status with OSC 7501 (#16407)
* feat(cli): report interactive program status with OSC 7501 Report a single root OSC 7501 record from the interactive UI so terminals and agent inboxes can show whether Qwen Code is working, waiting on the user, or finished, without parsing the screen. - idle / working / blocked (permission, question, auth) / done / error, with app=qwen-code on every report - done and error latch until the next turn; a cancelled turn returns to idle; done and error are kept on exit, everything else is cleared - msg names the tool at most, never its arguments or model text - TTY only, deduplicated, retried after a failed write; never wrapped for tmux passthrough - cleared while an external editor owns the terminal, rewritten on return and on SIGCONT Closes #13870 * fix(cli): latch the OSC 7501 turn outcome when the turn ends The outcome was recomputed from a history index on every history change, so an error added while idle (a failed slash command) flipped `done` to `error`, and memory compaction reset it to `idle` or hid an error from the turn. Decide it once when streaming state returns to Idle, bound the turn by item id, and reset only when history is cleared. Also count a blocked prompt and a stop-hook loop as failed turns, and pin the msg-only report, release/restore and editor call sites with tests. * fix(cli): reset the OSC 7501 outcome on /clear and cap msg by decoded size /clear only masked the latched outcome: the tracker kept `done` or `error`, so the next item added while idle brought it back, and exit could keep a stale badge. Reset the tracker when history empties. The msg limit checked only the encoded length, which lets a 2049-byte message through as 2732 base64 characters. Cap the decoded text at 2048 bytes, which also bounds the encoded form. Correct the design doc: a slash command runs as a turn and decides its own outcome; it is not an idle history change. * test(cli): assert the decoded msg is present before measuring it decodeMsg returns string | null, which tsc --build rejects as a Buffer.byteLength argument. * fix(cli): ignore errors already pending when an OSC 7501 turn starts A terminal API error stays in the pending area until the next submit commits it, and a Goal submission skips that commit. The next Goal turn then ended with the old error still pending and latched `error`, which exit kept. Record the failure items pending at turn start and count only new ones. Also restore the program status before the raw-mode restore in useLaunchEditor, which can throw; say control characters become spaces in the msg encoder; and note in the design that the OpenTUI renderer does not report status yet. |
||
|
|
32d22f950c |
feat(managed-agent): support durable structured user questions (#16433)
Connect Hosted question actions, structured answers, durable continuation, and the WebShell question form. Cover canonical response replay and pending-question recovery across Harness and Java context restarts. Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
1a20792cfa |
ci: route the no-AK integration gate to ECS for every pull request (#16132)
* ci: route the no-AK integration gate to ECS for every pull request
Same trust class as lint_and_static (2026-11-02 extension of the
2026-10-31 decision): the integration step executes PR code, but the
lane carries zero secrets by construction — every model key is emptied
at the step and the job's only token is the read-only checkout grant.
The gate's step-level runner.temp HOME becomes run-unique and is
cleared before use (the R2-1 lesson: RUNNER_TEMP survives across jobs
on this pool). test and integration_cli keep the association-gated
ubuntu_runner. Mutation-verified: runs-on revert, fixed HOME, and a
real key in the PR-code step each red the routing battery.
* fix(ci): isolate no-AK HOME from the first step; value-pinned zero-secrets
Round-2 review findings: (1) Critical — the gate-scoped HOME missed the
lane's actual fork-code start: checkout's local action and the install
(lifecycle scripts plus scripts/prepare.js, un-skipped) ran under the
runner's real shared home with a shared COREPACK_HOME — the exact
cross-job poisoning channel the lint acceptance closed. Mirror lint:
first-step GITHUB_ENV run-unique HOME + COREPACK_HOME inside it +
shared verified XDG cache + always() reclamation. (2) Critical — the
zero-secrets guard was name-based and missed odd-named secrets; enforce
by VALUE (every non-path env key empty, no secrets.* anywhere in the
gate). (3) runs-on literal pin tightened (timeout line also mentions
lint_runner). Docs: EN single-lane phrasing, stale merge-queue claim,
2026-11-02 paragraph rewritten for the first-step shape. Six mutations
red (first-step removal, shared corepack, odd-name secret, no cleanup,
runs-on revert, fixed HOME).
* docs(ci): correct the enforcement-order claim and stale trust-basis note
Round-2 review: the doc claimed the routing battery runs before
dependency install — false (HELPER_TESTS imports yaml, runs after
install, and not at all on github_ci_only PRs' own runs); and the
ci.yml trust-basis comment still described the round-1 gate-scoped
HOME shape. State the actual timing and detection latency, and the
shipped first-step shape.
* fix(ci): scrub ANTHROPIC_AUTH_TOKEN at the no-AK gate; honest cache naming
Round-3 review: the Anthropic SDK's auth resolver back-fills
ANTHROPIC_AUTH_TOKEN from env, so a runner-account residue would survive
into fork PR code on the pool — add it to the gate's scrub list (pinned
by both suites). The 'Configure persistent npm cache' step name promised
persistence the per-run HOME removes; rename it and record the accepted
cold-install cost (shared pnpm store stays a follow-up).
* fix(ci): scrub ANTHROPIC_AUTH_TOKEN at the no-AK gate; honest cache naming
Round-3 review: the Anthropic SDK's auth resolver back-fills
ANTHROPIC_AUTH_TOKEN from env, so a runner-account residue would survive
into fork PR code on the pool — add it to the gate's scrub list. The
value whitelist only iterates present keys, so the deletion mutation
escaped it; pin the key's presence-and-empty in the battery and the
28-key literal list. The 'Configure persistent npm cache' step name
promised persistence the per-run HOME removes; rename and record the
accepted cold-install cost (shared pnpm store stays a follow-up).
* test(ci): pin the no-AK grant, HOME template and reclamation path
Three assertions in the no-AK routing battery proved less than they read:
- the job's `permissions` was never inspected, so the read-only-checkout
grant that justifies routing untrusted fork code onto the pool could be
widened without a red test;
- `create.includes('HOME=')` is satisfied by `COREPACK_HOME=` and
`XDG_CACHE_HOME=`, so deleting the real HOME/USERPROFILE publication kept
the test green;
- the cleanup only had to contain the `qwen-no-ak-home-` prefix, so a path
that dropped `-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}` would still pass
while removing a directory nothing used.
Pin the grant by value, the publication by its printf template, and the
reclamation by the exact run-unique path the create step builds.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw48g5gafx
* test(ci): pin the realized HOME values in the shared ungated lane loop
R2-1: assert the create `home=` line, the gate QWEN_HOME and the cleanup
command by value, and require the exact set of `$GITHUB_ENV` writers. An
`echo` stand-in for the cleanup, a `-v2` create path and an appended second
HOME writer all kept the battery green under containment checks.
R1-2 / R1-1: hoist those pins into the shared `ungated` loop so
`lint_and_static` gets them too. Its create step was pinned by
`includes('HOME=')` (satisfied by `COREPACK_HOME=`) and its cleanup by
`includes('qwen-')`, so a dropped `HOME=%s` or a retargeted `rm -rf` survived.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4h0sl6g9
* test(ci): red on later republishes of the isolated HOME keys
R3-1: the GITHUB_ENV equality guard read step 0 only, so a later step that
republished HOME reverted the run-unique home by last-write-wins while every
assertion stayed green — the isolation would silently stop applying to the
steps that run PR code. Scan the remaining steps for isolated-key
$GITHUB_ENV writers and for step-level HOME/USERPROFILE/COREPACK_HOME/
XDG_CACHE_HOME overrides, leaving the npm cache step's own key a legitimate
later write.
Mutation-verified on this head: appending printf 'HOME=%s\n' ... to
integration_no_ak's 'Configure per-run npm cache (self-hosted)' step and
adding a step-level `env: HOME` each turn the suite red (103 pass -> 102
pass / 1 fail); reverting restores 103/103.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4ng9wvgi
---------
Co-authored-by: Qwen-Coder <[email protected]>
|
||
|
|
738c4bbf7e |
fix(serve): bound command snapshot replay memory (#16413)
* fix(serve): bound command snapshot replay memory Suppress unchanged command snapshots and retain only the latest full skill details so periodic refreshes cannot grow daemon heap indefinitely. Bound replay rings by serialized bytes while preserving ACP live delivery and reconnect IDs, and expose retention diagnostics. * fix(serve): avoid redundant replay seed serialization Size only retained complete command snapshots when seeding replay history, and forward the replay byte limit through the development daemon launcher. Pin snapshot accounting and lifetime eviction counters with independent regression assertions, and document the count/byte retention bounds and conditional command notifications. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
255d5cd2ae |
feat(web-shell): polish composer action menu presentation (#16181)
* feat(web-shell): polish composer action menu presentation * style(web-shell): use regular composer menu labels * fix(web-shell): address composer menu review feedback --------- Co-authored-by: 钉萁 <[email protected]> |
||
|
|
3663209c3d |
chore(ci): shorten stale PR window to 30 + 14 days (#16435)
* chore(ci): shorten stale PR window to 30 + 14 days The open PR backlog keeps growing, and the 60 + 30 day window only reaches PRs inactive for two months, leaving the 30-60 day band untouched. Lower the stale threshold to 30 days and the close window to 14 days, and raise operations-per-run to 300 so the first sweep can label the backlog in one or two runs. The stale/close messages and the fleet-shepherd design docs are updated to match. Co-authored-by: Qwen-Coder <[email protected]> * docs: keep the stale-window reference in the design current Avoid pinning the note to a merge-date guess: name the policy change instead, and reflow the paragraph to the file width. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
6fb741642f |
perf(managed-agent): take tool execution control off the tenant guards (#16415)
Every tool call admits an execution and authorizes its dispatch. Both went through lockBindingControl, which for a session-class binding (the default isolation class) took tenant placement, the publication guard, every slot and binding row of the Session and then its root, only to prove the Session is not CSI-pinned. lockExecutionControl now decides that under the Session root alone: the pin lives on the root, and profile slots and bindings require a committed pin. lockManagedSession and requireProvisionPin lock the root before slot and binding rows, so every placement owner shares the root-then-binding order that guard-free dispatch uses. Without an existing root, a locking read takes only a gap lock and orders nothing; dispatch then keeps the guarded path (a MySQL stress test reproduced the deadlock between the binding's unique-index and primary-key lookups otherwise). Admission still takes placement for the harness drain fence. |
||
|
|
17ad9cdb04 | feat(skills): add file-backed archive installation kernel (#16421) | ||
|
|
9b1ce46d77 |
feat(serve): add atomic user Skill activation (#16186)
* feat(serve): add atomic user Skill activation * test(serve): cover conditional user Skill activation capability * docs(serve): index user Skill activation endpoint * fix(serve): preserve User Skill refresh recovery |
||
|
|
4cea3a792f |
feat(cli): have an Ink TUI turn await its shared tool batch (#16385)
With experimental.unifiedAgentLoop on, the stream processor hands a shared round's tool batch to the round's submitQuery, which awaits it, its completion step and the round that step sends, once its own stream and activity ended. While another stream is active, which may be the stream that sent the round (a response of provider replays alone), the batch completes on its own as before. - The Ink executor takes a scheduler hold: the scheduler's completion callback returns once the completion step stopped the turn or is about to send the next round, as the current path holds it, up to the round whose calls need the scheduler. - A batch whose executed calls history all answered no longer sends its replay answers alone. The design doc describes PR 3b-2b. |
||
|
|
ecc64a735a |
feat(qqbot): deliver proactive loop messages as active group messages (#15635)
* feat(qqbot): deliver proactive loop messages to the group target
* fix(qqbot): fail blocked proactive deliveries and post loop output once
- pushProactive now raises ChannelProactiveDeliveryError when
sendMessageWithReplyContext reports a blocked outgoing route: the
blocked send resolves instead of throwing, so a loop delivery that
never reached the wire was being recorded as delivered. The block's
disposition is kept so the scheduler can distinguish a permanent
route failure from a retryable one; ACTIVE_MSG_DISABLED still throws
unwrapped from the send path.
- QQ's streaming chunk sink is skipped for unattended turns (loop and
webhook task, identified by the new protected isProactivePromptTurn
predicate), so the full answer is posted once through pushProactive
instead of being streamed to the group first and posted again. Base
chunk bookkeeping, lifecycle events and attended streaming are
unchanged.
- Tests: permanent and transient route blocks reject the loop and never
send; streamed chunks of an unattended turn produce exactly one send
carrying the full response.
* fix(qqbot): keep permanent delivery errors permanent and document proactive scope
- pushProactive rethrows QQ's permanent DeliveryError codes
(RETRY_EXHAUSTED, ACTIVE_MSG_DISABLED, FALLBACK_FAILED) as
ChannelProactiveDeliveryError('permanent', ...) so the daemon boundary
classifies a group that cannot receive active messages as rejected
instead of retryable. RATE_LIMITED stays transient.
- Drop the threaded-target guard copied verbatim from the base override;
every entry point already gates on supportsProactiveTarget and QQ never
produces a thread id, so the copy was unreachable drift.
- Name all four callers in the pushProactive doc comment (loop, webhook
task, background/named-task response delivery, daemon deliverProactive)
and state that background responses intentionally go out unanchored now.
- Document QQ in the proactive-delivery capability matrix and in the QQ
channel guide: active messages, group quota, ACTIVE_MSG_DISABLED and
the 5-failure auto-disable, no threaded targets, learned chat type
required.
* feat(qqbot): retire loops for chats that can no longer receive proactive messages
A loop created before a group disabled active messages, or before the bot
was removed from the group, kept firing: each run rebuilt a session, ran
a full unattended agent turn, and could never deliver. Both chat-wide
state changes now retire the chat's loops.
- base: add an optional chat-scoped controller lookup (listForChat) and
protected disableLoopJobsForChat(chatId), which disables every enabled
loop in a chat regardless of the creator's exact target; the CLI
controller implements the lookup over the loop store.
- qqbot: handleGroupDelRobot folds the disabled count into its existing
cleanup log line; handleGroupMsgToggle disables the group's loops when
active messages are turned off (both handlers become async and are
dispatched with an explicit rejection log).
- qqbot: refuse proactive targets for a group whose active-message
toggle is known to be off (supportsProactiveTarget), so loop creation
is rejected up front and a firing refuses before burning an agent
turn; direct messages never consult the group toggle.
- tests: creation refusal via the loop tool, run refusal with the prompt
never called, removal and toggle-off teardown against the real
controller, DM exemption, mid-flight toggle classification, and the
base helper's contract.
* test(qqbot): keep the loop-store integration test honest
- Pin a rendered next-fire ISO timestamp instead of base's 'n/a'
fallback: the copied controller now delegates nextFireTime the same way
the production controller does, and the store clock is fixed so the
value is deterministic.
- Redirect QWEN_HOME at the test's tmpdir before the channel constructor
runs and assert the channels directory lands inside it, so the test no
longer creates ~/.qwen/channels on the machine running it.
- Resolve the session through the locally constructed SessionRouter
instead of reaching the protected ch.router, which was a TS2445 that
no gate in the repo catches (test files are excluded from tsc).
* fix(qqbot): close the round-2 proactive delivery findings
- Resolve the target chat type through chatTypeFor() instead of
chatTypeMap, and refuse an explicit group target whose type is unknown,
so config-declared groups and never-learned groups are gated on the
same type resolveRoute routes on instead of burning an agent turn that
cannot deliver.
- Route background/named-task responses through pushProactive
unconditionally: a toggle-off group must not silently receive them
through the anchored passive reply path.
- Extract one PERMANENT_DELIVERY_CODES/isPermanentDeliveryError predicate
and use it at the proactive seam and the five existing classification
sites, preserving each site's behaviour.
- Fix the two comments that named a scheduler disposition check that does
not exist (the daemon classifyChannelDeliveryError is the only reader)
and word the blocked-route error as 'outgoing route unresolved
(<disposition>)'.
- Include the group openid in group-event failure logs and pin the
websocket dispatch rejection guard with a corrupt-store test.
- Stop retiring a group's loops when its active-message toggle turns
off: the toggle is reversible, the pre-flight gate already prevents the
turn burn, and the scheduler's 5-failure auto-disable retires the job.
Retirement stays on bot removal, where the gate cannot see the change.
- Pin the permanent/transient arms with FAILED-fallback and RATE_LIMITED
tests.
* test(cli): pin the chat-scoped loop lookup
listForChat decides which loops chat-wide teardown retires, and the QQ
suites substitute hand-rolled controllers whose filter mirrors it. Test
the real controller over a real ChannelLoopStore seeded with two chats
and two channel names, so dropping the filter (or only its channelName
clause) reds instead of silently retiring unrelated users' loops.
* docs(qqbot): record the proactive delivery contract
- Add the design record in English and Chinese covering the capability
flip and the four base call sites it activates, the single-delivery
rule for unattended turns, permanent/transient classification, chat
retirement on bot removal, and the group active-message toggle
contract.
- Complete the opt-in adapter list (DingTalk Workspace, Email with its
proactiveRecipients qualifier) and state QQ's target restrictions and
the unanchored background-response behaviour.
- Document isProactivePromptTurn in the base hook table and the
adapter-author proactive delivery section.
---------
Co-authored-by: Shaojin Wen <[email protected]>
|
||
|
|
e9a20d6ed7 |
fix(core): keep session list titles when a transcript is rebuilt (#16299)
* fix(core): keep session list titles when a transcript is rebuilt The sidebar label came only from the transcript: a custom_title record or the first user prompt. A transcript recreated without its head (missing file recreated on the next append, crash truncation, external cleanup) lost the prompt, so displayName became empty and the Web Shell rendered the session id prefix. Persist the list label to <sessionId>.title.json next to the transcript: - the first user prompt (source "prompt"); - custom titles from ChatRecordingService and SessionService.renameSession (source "manual"/"auto"). Reads keep the transcript authoritative and fall back to the sidecar. Archive, unarchive, delete, and storage relocation move or remove the sidecar with the session. Web Shell rows now render "Untitled session" / "未命名会话" instead of an id prefix. Design: docs/design/session-title-sidecar.md * fix(core): keep the list-label sidecar best-effort Write it after the transcript append and swallow its errors, so a partial Config or an unwritable home can never drop a user message. Update the deletion-cleanup ownership assertion for the extra title-sidecar check. * fix(core): align the title sidecar with the list reader - Seed the sidecar when a transcript is resumed (custom title first, then the recorded first-prompt display text) so sessions created before the sidecar existed gain one without another prompt. - Write the sidecar when forkSession commits a titled fork. - Store the prompt's displayText when present and share the reader's 200-code-point-plus-ellipsis truncation instead of a UTF-16 slice. - Pin the intended behaviour with tests for a text-less first prompt, resume seeding, and a titled fork; update both design documents. * fix(core): close the review findings on the title sidecar - Seed a resumed session with its actual first prompt: track that text outside the 20-entry title window and fall back to the record's text part when no display projection exists. - Never replace a persisted label from a later prompt. This also lets an image-only first prompt leave the slot open for the next text prompt. - Truncate and validate the sidecar name by code point so an astral prompt cannot store a lone surrogate or a discarded ellipsis. - Reconcile the title sidecar in both archive/unarchive conflict-resolution branches, where the hand-rolled sidecar set previously left it orphaned. - Update the workflow-mode e2e locator for the untitled row label, and cover the primary and fanout rows, archive moves, and storage relocation. - Correct both design documents: the list reads the sidecar when either the title or the prompt is missing, and a wholesale chats-directory clear also removes the sidecar. * fix(core): keep sidecar-backed session names consistent Review round follow-ups for the session title sidecar: - wrap the archive/unarchive conflict reconcile so a sidecar IO error no longer reports an already-completed transcript move as failed; - also fall back to a manual/auto sidecar in findSessionsByTitle, the picker prefix scan, and the unarchive collision guard, so a name the sidebar shows can be found with --resume and cannot collide unnoticed; - converge the live write and the resume seed on one label policy that mirrors the list reader (explicitly empty projection, Mod prompts); - stop a projector-restored session from being labelled with its first post-resume prompt, which is not the session's first prompt; - copy the source sidecar into a fork made without an explicit title; - return no name instead of another project's prompt text when the transcript is refused for project membership; - keep the id in the fan-out tooltip and the read-only row's accessible name, where it is not the visible text. Tests cover each fix, including the archived half of the lifecycle and a writer/reader pair on one filesystem; design docs updated in both languages. --------- Co-authored-by: verify-round <verify-round@local> |
||
|
|
c4f9899eea |
perf(managed-agent): take native journal snapshots off the tenant guards (#16410)
lockSnapshotWriter took tenant placement and the publication guard on each of its three calls per snapshot, so every snapshot of a tenant queued behind binding transitions, provisioning, retention, maintenance, collectors and writer acquisition. It now uses the guard-free publication writer. The build transaction cannot lock the Session root first, because its nested journal reads take read leases on that root and head. When it reaches the root it holds only the snapshot's own new rows. Any transaction that could hold the root and wait for those rows already closes a cycle with it today through the guards, so dropping them adds no cycle. Root and head order the writer checks, and the read lease keeps the copied resources. A pinned CSI Session still takes both guards behind its root. Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
a32646e640 |
docs(design): propose PR state transitions from the daemon PR refresh (#16246)
* docs(design): propose PR state transitions from the daemon PR refresh The daemon's PR refresh already sweeps every bound PR with the host's gh credential; the GitHub channel polls the same PRs again to detect CI failures, merge conflicts, head changes and merged/closed transitions for bound sessions, with its own credential, schedule, CI definition and state machinery, and only where such a channel is configured. Propose observing head, CI and mergeability in the refresh, keeping the snapshot and pending transitions in the PR sidecar, delivering each transition into the bound session through the existing prompt admission and resume paths, and removing the channel watch in a second PR. * docs(design): keep per-head CI history and daemon-wide recipients for PR state events Two inconsistencies with the channel watch the proposal replaces: - A single ciFiredSha lost earlier heads, so a force-push back to a head whose CI failure had already fired would fire it again. The sidecar now keeps a bounded ciFired ring (16 heads per PR), the daemon's equivalent of the channel's durable fired ring, and the acceptance criteria pin the revisit case. - Recipient selection was per workspace, so two registered workspaces for one repository would each deliver to their own bound session. The sweep that detects a transition now selects one recipient across all trusted workspaces, dispatches through that session's runtime, and writes the snapshot and fired record to every sidecar bound to the PR so sibling sweeps observe no transition. * docs(design): give prWatchEvents a deprecation window now that v0.26.7 ships it The proposal justified rejecting prWatchEvents at connect() with "no tagged release contains the channel watch". That premise is stale: #16117 merged on 2026-11-03 and v0.26.7 (2026-11-04) ships and release-notes it. PR B now keeps accepting the field for one minor release (one warning naming sessionPrWatch, no watch pass), and the following minor release rejects it; acceptance criterion 3 and the files table follow. The refresh-interval note no longer claims a one-minute floor: sub-minute values fall back to the default and 0 disables the sweep. * docs(design): reconcile sibling PR snapshots before detection Propagating a snapshot to every sidecar bound to a PR is a sequence of separate file writes, so a crash between the recipient's write and the sibling writes left a sibling one observation behind; its next sweep re-detected the same transition under a fresh key and the recipient was admitted twice. Detection now takes the newest sibling snapshot (by a new observedAt field) as its baseline, so a lagging sibling catches up silently and the original pending key is the only one for the episode. The durability section lists this fourth crash window, and the validation plan and acceptance criterion 5 pin it. * docs(design): version slim-listing state observations for baseline recovery The newest-sibling baseline only worked for observations made by gh pr view: merged and closed states come from the slim listing and left observedAt unchanged, so after a crash between the recipient's write and the sibling writes the two sidecars carried the same version and recovery depended on enumeration order. Every observation that changes the watch block now stamps observedAt, the block carries the state seen at that version, and the baseline is the whole block of one observation. The validation plan covers the terminal-state crash case. * docs(design): PR-scoped pending, provenance-bound admission and sweep budgets Folds in the findings of the docs-only review on |
||
|
|
ad487ffedb |
feat(autofix-pr): give helper refusals a code and a next step (#16238)
* feat(autofix-pr): give helper refusals a code and a next step
A helper that refuses now exits 1 with one JSON line last on stderr —
{"refused":"<code>","next":"<step>","message":"…"} — instead of throwing
to the CLI's critical-error handler, which printed the error's stack. Each
yargs handler catches the error and prints the line, as review's
extract-step does. Anything that is not a coded refusal is `failed`.
The step depends on the helper as well as the code: `watched_elsewhere` is
`take_over` from `prepare` and `end_task` from a round's helper,
`worktree_dirty` is `discard` from `sync` and `needs_human` from `push`,
and a `reply` that failed is `retry`, as its second run skips what the
first one posted.
The skill acts on `next` through a new "Refusals" section instead of
matching the wording of messages, and the autofix_pr tool returns the coded
line first, verbatim, then what the helper printed before it. Design
section 6.16 lists the codes.
A clone with no directory for the watch's lock (its watch files removed)
is refused as `not_prepared` instead of with ENOENT.
* fix(autofix-pr): code the refusals whose step the caller needs
- A reply plan that is not JSON or not shaped as a plan is `plan_invalid`
(`fix_plan`), not `failed` (`retry`): retrying it unchanged fails again.
- A switch back that `sync --discard` asked for and git refused is
`failed`: as `worktree_dirty` it answered `discard`, which asks again.
- The skill's Resume step keys on the code `pr_closed`, since `prepare`
gives it `report`; `retry` holds wherever a step says a refusal ends it;
refusals with no coded line are the calling step's to handle.
- The tool takes the last coded line of stderr, not the last line, and a
refusal's message keeps at most 8,000 characters, its head and tail.
- Each helper's code is pinned through the command in an empty clone.
* fix(autofix-pr): stop a watch git cannot switch back, and fit the coded line
- `sync --discard` that git cannot switch back to the watch's branch
answers `synced: false`, as a worktree still dirty after discarding
does, so the round stops instead of reporting the same refusal at every
fire.
- A reply plan that cannot be read is `plan_invalid` as well.
- A refusal's message drops terminal controls and is cut to 8,000
characters as JSON, not as text, so the line fits the stderr the tool
keeps.
- The tool reads a coded line only from a helper that exited 1, and only
when nothing but Node's warnings follows it.
- The skill's `discard` step tells Round step 1, where it clears what an
earlier round left, from the push of step 6, where the round's own fix
left the worktree unclean.
* fix(autofix-pr): answer the first review of the refusal codes
- A switch back git refuses because the branch is checked out in another
worktree names that worktree and how to free it: `cleanup` cannot
delete the branch either while it is held.
- A message drops escape sequences whole and the daemon's log-unsafe
class (C1 controls, bidi overrides, invisible formatters), keeping tabs
and newlines.
- A defect, not a refusal, prints its stack frames before the coded line.
- The coded line survives the frames `--trace-warnings` adds after it.
- `fix-start` refuses a fix in flight with the message that says how it
ends.
- The tool's description states the result's prefix, and `partial` is
described by the deadline that raises it.
- Tests: the whole step matrix as a specification, `cleanup` and `status`
through the command, the order of the watch checks, prepare's `failed`
arm, the line within the stderr tail the tool keeps, and the skill's
steps read from the helpers' source with what each does.
* test(autofix-pr): pin a C1 control a refusal's message drops
* fix(autofix-pr): say how to free a branch a stale or locked worktree holds
When `sync --discard` cannot switch back because another worktree has the
watch's branch, the reason now fits what holds it:
- a worktree whose directory or `.git` was deleted by hand: drop its
record with `git worktree prune`, since `git worktree remove` refuses
one whose `.git` is gone;
- a locked worktree: unlock it first, since neither `remove` nor `prune`
touches a locked one.
The test runs each case's git commands as the reason gives them and
checks that the next round gets through.
The every-helper refusal test now joins its fake `gh` to PATH with
`path.delimiter`, and its `prepare` row refuses before calling gh. The
fake records its calls and the test asserts there are none, because on
Windows `execFile` would never run the script.
* fix(autofix-pr): quote the holder path in the recovery commands
The `git worktree remove` and `git worktree unlock` commands that a
switch-back refusal prints interpolated the holder's path bare. A path
with a space split into two arguments (git exits 129), and a quote broke
the shell, so the branch stayed held. Both commands now quote the path
with `shellQuotePath`, the form review's printed repair commands use.
The holder test now runs every case at a plain path and at one with
spaces and both quotes. It runs the printed commands through `sh -c`
instead of splitting them on spaces.
* fix(autofix-pr): answer the second review of the refusal codes
- The tool runs its helpers with NODE_NO_WARNINGS=1. Node flushes a
warning raised while a helper refuses after the coded line, and a
warning's text can run over several lines, which hid the line from
`codedRefusal`, so the code and its `next` were lost. A new test runs a
helper that raises such a warning: run directly, the helper prints the
warning after the line; run through the tool, its line is read.
- The switch-back reason's git commands carry `-C` with the watch's
worktree, so they run as printed from any directory. A branch held by
the clone's own checkout gets only "switch that checkout", because git
never removes the main worktree.
- `worktreeHolding` reads `git worktree list --porcelain -z` (git 2.36+)
and falls back to the line form on older gits, so a path with a
newline in it is no longer cut short.
- New tests: the holder matrix also runs at a path with a newline and
runs its commands from outside the clone; the main-checkout holder;
and the no-holder arm (an index.lock stops the switch), whose `cleanup
--force` advice nothing reached before.
* fix(autofix-pr): keep a watch whose branch another checkout has
When `sync --discard` cannot switch back because another checkout has
the watch's branch, it answers `synced: false`, and the skill stops the
watch with `cleanup`. `cleanup` removed the worktree first and then ran
`git branch -D`, which git refuses while that checkout has the branch.
The watch was left with its state file and branch and no worktree, and
every later `cleanup` and `prepare` failed at the same delete.
`cleanup` now checks `git worktree list` before it removes anything,
`--force` or not. When another checkout has the branch, it keeps all
three and ends the watch, as it does for unsaved work. It answers
`removed: false` with `heldBy`, and a reason that says how to free the
branch and to run `cleanup` again. The holder lookup is shared with
`sync` (`branchHolder`) and anchors its commands at the clone root.
The skill's Stop step and both design docs say what `heldBy` means.
|
||
|
|
11f6163f0f |
perf(managed-agent): narrow SSH settlement and worktree locking (#16386)
Settle SSH registrations using their operation and exact physical owner. Allow ordinary Runtime members and uploads during main worktree preparation while retaining maintenance ownership and cleanup exclusion. Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
5d45b42940 |
feat(verify-pr): give the sandboxed round the PR's review threads (#16300)
* feat(verify-pr): give the sandboxed round the PR's review threads The sandboxed /verify round had no token and no review context. Its only inputs were the metadata snapshot and, on a follow-up round, the previous report. On #13652, Critical R1-26 was confirmed and then marked fixed three days before a round that published merge-ready without mentioning it. Eight hours later the review bot, at the head that round had certified, still could not rule out its residual. The verify job's trusted resolve step now writes review-threads.json beside pr.json. It holds every review thread, resolved ones included, since resolving is the author's claim, together with each reviewer's standing CHANGES_REQUESTED review. It comes from one paginated GraphQL query reshaped with jq. The snapshot is best-effort: on failure no file is written and the step warns. verify-pr-prepare.mjs sends the same query and writes the same shape for the local path. It removes a previous copy first, so a failed fetch cannot leave a stale file. A test runs the workflow's block verbatim and prepare's transform over the same pages and requires equal output. The skill adds report item 4, Existing review findings. Each term the verdict turns on is computed from snapshot fields: - a thread is blocking when its first comment is tagged Critical, Blocker, P0 or P1 by an owner, member or collaborator; - a blocking thread is open when it is unresolved, or when that comment belongs to a standing change request, resolved or not. That is the rule GitHub's review decision applies. An open blocking thread left unmeasured rules out merge-ready, and with nothing failed the verdict is inconclusive. Findings at fail 0 would publish as "passed with non-blocking findings". Other blocking threads are re-measured as budget allows, and the verdict line counts those left unmeasured. Change request bodies are listed but do not hold the verdict, because their severities are prose. The duty is also a line in the scope budget. A missing file reads as unknown, never as "nothing open". The file adds 2396 bytes, so the size baseline records the exact 378242. * test(verify-pr): pin the review snapshot's jq guard, copy order and odd answers - The CI/local parity test runs the step's real jq program. Like the file's four other jq cases, it now skips when jq is missing, instead of failing one assertion later with a message that names neither jq nor the missing tool. - The local recipe must clear the cached review-threads.json before it copies prepare's file. The test pinned the three lines but not their order. With rm after cp, every local round would read the review state as unknown. - prepare's two other failure arms are now driven through the CLI: an answer that is not JSON, and pages without a pull request. Each must leave no file and say why. An empty state there would read as "nothing open". * test(verify-pr): drive the review snapshot through the shared gh harness The review-snapshot case had its own copy of the executed-step harness: a temp dir, a stub gh on PATH and GitHub's bash flags. makeGhHarness exists so that fidelity contract lives in one place. The factory now records every call's argv, returns its temp dir, and has an empty-stdout arm for paginated reads, GH_STUB_EMPTY_LIST. The snapshot case runs through it. Its failure arms are the factory's GH_STUB_FAIL_LIST and GH_STUB_EMPTY_LIST. Deleting the shared stub's fail arm, its empty-stdout arm or its argv capture turns the case red, and so does recording only the first argument. |
||
|
|
ef6914c1d9 |
fix(channels): keep GitHub replies hit by a secondary rate limit (#16334)
* fix(channels): keep GitHub replies hit by a secondary rate limit A final reply that GitHub rejected with a secondary rate limit was classified as an ambiguous delivery failure: it was not retried, not kept as a pending delivery, and no error comment was posted, so the answer was lost while the notification was marked read. GitHub sends secondary limits with quota left (and not always with retry-after), so only the exhausted-budget shape counted as a definite no-write. Treat a 403/429 that carries GitHub's rate-limit headers and names a secondary limit as a definite no-write too. The reply is retried in process and, if the limit persists, kept as a pending delivery for the next channel start. Responses without GitHub's rate-limit headers (for example a 429 from a proxy) and permission 403s stay single-shot. * fix(channels): wait out GitHub secondary limits before retrying A secondary rate limit without retry-after fell through to the generic 1 s / 2 s backoff, so the final reply's three attempts landed within about three seconds while GitHub was still throttling the account; GitHub asks for at least a minute in that case, longer on each repeat, and warns that continued requests can get an integration banned. Wait 60 s and then 120 s for such a limit; retry-after and the budget reset still take precedence. Note in the classifier why a retry-after header alone is not proof of a rejected write there. * fix(channels): stop the pending GitHub retry pass on a rate limit Now that a secondary limit counts as a definite no-write, the retry pass on channel start spent three attempts on every pending reply while the same account-wide limit answered each of them; stop the pass at the first rate-limited record and keep the rest for the next start. Pin the retry count and waits per secondary-limit shape, the older abuse-detection wording, and that a 502 carrying an exhausted-budget header stays single-shot. Reword the classifier comment: the header shows a rate limiter answered, and GitHub's wording tells which one. * fix(channels): end GitHub rate-limit waits on stop, keep cleaning pending replies - disconnect() used to cut a rate-limit wait short and let the remaining attempts go out back to back into the same limit. A stopped wait now gives up with GitHub's last answer, so a final reply stays a definite no-write and is kept as a pending delivery. - The pending pass stops sending at the first record GitHub still rate-limits (either limit) but keeps dropping later records the audit already shows as posted, which needs no request. - The PR watch pass treats a 403 carrying GitHub's secondary-limit wording as shared, like the publication and notification paths. - A retry-after that is not a positive number of seconds (an HTTP date, "0") no longer skips the secondary-limit minimum. * fix(channels): treat retry-after on a GitHub-answered 403/429 as a rate limit The final reply's definite no-write required an exhausted budget or GitHub's secondary-limit wording, so a rate-limit rejection that carried GitHub's x-ratelimit-remaining and retry-after but worded its body differently was audited ambiguous and the reply was dropped. retry-after is GitHub's own documented signal for that limit; with the x-ratelimit-remaining header still required, a proxy's bare 429 stays single-shot. A 403/429 whose retry-after is unusable now waits the secondary-limit minute as well. Also pin the shape where GitHub rejects the reply once and accepts the retry. * fix(channels): stop GitHub retries by the error's shape, not the wait branch - The stop guard keyed on which wait was chosen, so an exhausted budget without a reset header took the generic 1 s / 2 s branch and, after disconnect(), still fired its remaining attempts into the spent budget. Rate-limit answers are now recognised by their shape (403/429 with an exhausted budget, retry-after or the secondary wording) for the wait, the stop guard and the definite no-write check alike; such an answer without a usable retry-after or reset waits the one-minute minimum. - x-ratelimit-remaining must be a count: an empty value no longer reads as an exhausted budget and turns a permission 403 into a stored retry. - The pending pass starts no further record once a stop is requested, even while another write in flight defers its abort. - Tests pin the stop guard for every wait, reconnecting, a kept record between the stop and a posted one, and an infinite retry-after. * fix(channels): let an answer that lands after a stop decide the pending reply - The start-up pass returned on an aborted signal before looking at the error. A stop defers the abort until the in-flight write ends, so a 502 or network failure from that write (GitHub may have posted the comment) left the record and its reply_pending task for the next start, which posted the reply again. The pass now classifies first; only an attempt that was never sent leaves its record, and githubApi marks that abort as not sent. - A rate limit without GitHub's header (a 429 with only retry-after) is still dropped as ambiguous, but it now stops the pass like a definite one instead of spending every later record against the same limit. - x-ratelimit-remaining as a number must be a non-negative integer, and the watch pass judges a 403 with the same rate-limit rule as the reply. - A call that starts after the stop gives up before logging or sleeping. * fix(channels): treat a past GitHub budget reset as clock skew GitHub answering with an exhausted budget means its window has not reset, so a reset time that already looks past says the local clock is ahead; the wait fell to a 1 s retry and spent all three attempts in about two seconds. It now waits the one-minute rate-limit minimum instead. Tests give the secondary limit GitHub's usual reset header and pin the start-up pass keeping its records when a stop, deferred by another write, lets the wait run out. * test(channels): pin the ambiguous audit, the watch 429 abort and the quiet stop Lock three behaviours the suite let drift: a header-less rate limit on the final reply is audited failed_ambiguous (the key stays consumed), a bare 429 aborts the whole PR watch pass, and a reply that starts after the stop logs no retry it will not make. * docs(channels): name the header a GitHub no-write answer needs The no-write rule needs a numeric x-ratelimit-remaining on the 403 or 429, so a rate-limit answer without it (a 429 with only retry-after, say) is ambiguous and its reply is not kept; the guide now says so. The publication test helper's comment also said the opposite of what stopping the poll loop does to retry waits. * docs(channels): bring the GitHub publication contract up to the retry rules The contract still said a definite no-write goes straight to the pending file. It now names what counts as one, the two in-process retries and their waits, what an ambiguous answer to a retry does, the stop that keeps the reply pending, and how the start-up pass stops at the first reply GitHub still rate-limits. The first bullet now speaks of one final response rather than one delivery attempt. * docs(channels): add the Chinese version of the GitHub publication contract docs/design/README.md asks a change to a single-language design to add the missing version. The publication contract gains its zh-CN twin, reciprocal links under both titles, and a Verification line for the retry tests. |
||
|
|
6611b8aeb2 |
fix(cli): land a Web Shell update on the current latest release (#16374)
* fix(cli): land a Web Shell update on the current latest release The daemon pins its update target when it prepares. Once the state is `ready` the check returns the cached status even for `?refresh=true`, so polling never re-downloads — and Web Shell prepares automatically on mount. For a long-lived daemon the prepared release is therefore whatever was latest shortly after it started. Clicking Update restarted into that stale target, and every release published in between needed another prepare-and-restart round, which reads as advancing one version per click. The CLI resolves and installs at each process start, so it always reached the current latest in one step. Re-resolve the release once at activation, after the restart has been acknowledged, and stage the newer one when it is higher and one-click covers the installation. Both the standalone and the managed npm paths share one resolver, so eligibility cannot drift between the check and the click. Readiness stays pinned and background polling still never re-downloads; only the explicit click re-resolves. A failed re-resolve or staging applies the prepared release instead, so a registry failure cannot strand an explicit update request on the running version. The activation guard still preserves an equal or newer active installation, and a failed activation of the newer release cleans both payloads. Staging runs after the acknowledgement and can take minutes, during which the daemon keeps answering. The browser now measures its give-up deadline from continuous unreachability rather than from the click: a reachable daemon is still working, and the old click-time budget would have reported a failure while the update was in fact succeeding. The protocol reference and both language versions of the update design doc state the pinned-readiness contract and the new deadline semantics. * fix(cli): keep an update restart observable and its payload committed An audit of the activation-time re-resolve found three defects in it. Discarding the superseded payload ran before the newer one was committed, so a failed discard fell back to a payload that might already be half removed and orphaned the one just staged, losing the update entirely. Commit to the newer payload first and discard the old one best-effort: a failed discard now leaks disk instead of losing the update. Dropping the browser's click-time deadline had also removed its only overall bound. A daemon whose process replacement does not land on the new build comes back reachable and settled on the same version, and the control then waited forever for a version change that would never arrive. Restarting is sticky until the process is replaced, so an acknowledged restart followed by any other state with an unchanged running version now terminates the wait and reports a failure. That path publishes the settled state itself, because the poll immediately after it can drop while the daemon is still coming back, which would otherwise leave a stale restarting status rendering no control at all. The rule is gated on the restart having been acknowledged: polling starts on the click, before the request resolves, and can still observe the pre-click state. Browser fixtures now model the daemon's contract — restarting from the acknowledged restart until the process is replaced — because a fixture that keeps reporting a prepared state after the click cannot exercise that distinction. Assertions are unchanged; only the stimulus became faithful. Also pin the edges the re-resolve depends on: a prerelease below the prepared release, a registry rollback, build metadata that does not change precedence and a malformed registry value must none of them restage, and an in-flight restart must survive the operator disabling automatic updates, concurrent polls and retries, and ordinary shutdown. Every mechanism added here is mutation-witnessed, including the two halves of the deadline change and the acknowledgement gate. * fix(cli): sample the restart acknowledgement when the poll starts The settled-state termination read the acknowledgement ref after awaiting the status request, which does not establish that the response was sampled after the acknowledgement. The status and restart requests are independent, so the first restart poll can be issued while the restart is still pending and deliver its pre-restart snapshot after the 202 arrives. The ref is then true, the stale snapshot looks like a daemon that settled without updating, and the control reports a false failure and clears the restart — after which a genuine version change is handled by the idle branch, so the page never reloads and the captured task URL is never restored. Sample the flag when the poll is initiated instead. A response delivered after the acknowledgement by a poll started before it is now ignored, and the next poll — initiated afterwards — is the first one allowed to conclude the replacement did not land. The regression test holds the status request open, acknowledges the restart, then delivers the old snapshot, and requires no error plus a successful reload once the new version is reported. Also restore an absolute backstop, which the move to a continuous- unreachability deadline had removed. A daemon that stays reachable but never leaves restarting — a hung activation or process replacement — would otherwise leave the control disabled forever with no error, and a page reload hides it entirely. The cap sits above the daemon's own staging ceilings (five-minute archive download, ten-minute managed npm child) so a slow but progressing update never reaches it, and a test pins both halves: the wait survives fourteen minutes of a reachable restarting daemon and ends at the cap. Both the protocol reference and the two language versions of the design doc state the sampling order and the backstop. * fix(cli): log why a click fell back, and keep the entry after giving up Three review findings on the update restart path. A registry outage at click time left no trace. The release check resolves an error result rather than throwing, so the fallback returned silently and "the registry said nothing newer" was indistinguishable from "the registry was unreachable" — both install the older prepared release. Log the classified failure before falling back. The catch below only ever sees detection or staging throw, so it now names restaging instead of sending the reader to the registry. The test that covered this used a rejected check promise, a shape the real function does not produce for an outage; it now resolves the real error result and asserts the log, and a second test pins that a staging failure is not reported as a registry failure. Giving up took the update entry with it. While the wait runs, each successful poll publishes the daemon's state, so a wedged daemon leaves it at restarting; the give-up branch cleared the restart without publishing anything, and the render gate does not display restarting, so the entry unmounted and could not return while the daemon kept reporting it. The user got one notification and then no affordance at all. Every give-up path — both deadlines and a daemon-reported error — now keeps a retryable entry on screen. The design sentence claiming the control is never hidden was untrue of these paths; it is now true of all of them. The wedged-daemon test asserted only that the spinner was gone, which a vanished control also satisfies, so it now asserts a visible enabled entry. The absolute cap was sized against a ceiling the code does not have. The standalone archive download tries OSS and then GitHub sequentially with a five-minute budget each, and the checksum and signature fetches add up to two more minutes, so one restage can spend about twelve minutes downloading and verifying before activation — not the five the docs and the constant's comment claimed. That left the fifteen-minute cap about three minutes of headroom while the text promised a slow update would never reach it. Raise the cap to twenty minutes and state the real chain in the protocol reference, both design docs and the comment. |
||
|
|
edb094fea3 |
test(managed-agent): gate lifecycle-v2 close with a stopped worker (#16123)
Co-authored-by: Qwen-Coder <[email protected]> |