mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-09-11 19:46:21 +00:00
9608 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1424ea0f9e | fix(web-shell): preserve continuation outcomes across replay | ||
|
|
53e6196c6b
|
fix(web-shell): persist cancelled responses and elapsed time (#11608)
* fix(web-shell): preserve cancelled responses and elapsed time * fix(core): retain cancelled text during reactive compression --------- Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> |
||
|
|
d66ad3e3a2
|
feat(review): focus automatic static navigation reviews (#11456)
* feat(review): focus automatic static navigation reviews * fix(review): address round-1 review of the docs-nav profile (#11456) - QWEN_REVIEW_AUTOMATIC is operator-only: added to PROJECT_ENV_HARDCODED_EXCLUSIONS and read through automaticReviewRequested(), which refuses file-sourced values - SKILL.md: the docs-nav profile passes the cumulative findings file to compose-review as findingsPath, so an unruled candidate caps at Comment instead of posting as a verified blocker - the classifier treats top-level key order as structure (a reorder keeps the full review); the design doc records the decision - assessResume reports a moved head as head-moved even on a profiled report; both profile guards read the shared DOCS_NAV_PROFILE constant - the mechanism-health note no longer reads the profile's by-design anchor withhold as a stopped chain - the coverage gap subject names the omitted full review and reverse audit instead of the review that ran - buildRoleBrief welds no context pointer for a malformed plan prNumber * fix(review): address round-2 review of the docs-nav profile (#11456) * fix(review): address round-3 review of the docs-nav profile (#11456) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(review): close focused navigation review gaps * fix(review): strengthen focused navigation contracts --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
0376c241fe
|
fix(serve): preserve child error detail on explicit-close and kill paths (#11190)
The session/close notification-failure log shared by closeSession and killSession serialized the caught rejection with String(), which collapses a wire-shaped JSON-RPC error record (the child's close refusal, not an Error instance on the daemon side) to [object Object]. Route it through the existing extractErrorMessage helper, and apply the same fix to the branchSession live-state cleanup close that calls the same wire method. Refs #11123 Co-authored-by: mac <bishopapril850965@yahoo.com> |
||
|
|
f649d65d1f
|
feat(live): add visual input, proactive assistance and memory (#11369)
* feat(live): add visual input, proactive memory and desktop controls * fix(live): preserve upstream checks and finalize migration validation * feat(live): open active configuration from host settings * fix(live): address lifecycle and proactive review findings * feat(live): add subagent controls and diagnostics with review fixes --------- Co-authored-by: 菲兹 <feizi.wx@alibaba-inc.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
c6c276b05c
|
feat(web-shell): improve composer and context usage presentation (#11509)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
pnpm Worktree Smoke / Install (macos-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (ubuntu-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (windows-latest) (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(web-shell): improve composer and context usage presentation * fix(web-shell): address context presentation review feedback * test(web-shell): align brand fixtures with startup and layout * fix(web-shell): improve context readability and availability |
||
|
|
2f426a64f4
|
feat(web-shell): enrich browser notifications and open target sessions (#11447)
* feat(web-shell): enrich browser notifications and open target sessions Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(web-shell): include current prompt in browser notifications Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(web-shell): configure browser notification branding and defaults Allow hosts to configure notification names, icons, and the initial preference. Enable notifications by default in the built-in page while preserving saved user choices and browser permission requirements. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(web-shell): address browser notification review feedback Preserve code excerpts, bound pending notification text, and keep notification configuration changes from restarting session streams. Reconcile branding documentation and add regression coverage. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(web-shell): address notification follow-up review cases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
ec743fd450
|
feat(web-shell): route scheduled runs by model and group (#11396)
* feat(web-shell): route scheduled runs by model and group * fix(serve): harden scheduled task rollback * test(serve): align deletion generation expectation * fix(serve): scope the fail-closed sub-session model gate to scheduled-task runs * test(serve): pin the scheduled-task model-gate conjuncts The fail-closed model gate in create-sub-session and its standalone sibling had three conjuncts no test discriminated: widening the source predicate to sourceType === 'default' or dropping modelApplied === false left both suites green. Add a positive scheduled-task case with modelApplied: true, and give both non-scheduled negative cases real non-scheduled source coordinates so each conjunct has a witness. Also correct the updateGate.fail comment in cronScheduler.test.ts: the mock returns the input array, so updateCronTasks skips the tasks write before the injected error is thrown — it does not write through. * fix(serve): address scheduled-task routing review feedback (#11396) A dispatch-failed one-shot stayed inert when re-enabled: the PATCH re-seat pointed a date-pinned cron at a next occurrence up to a year out, and the missed-detection disk guard suppressed the retry signal forever. Re-enable of such a task now keeps its consumed anchors so the missed-one-shot pass delivers the retry (confirm-first), and the dead guard plus the restoredPerRunOneShots set it fed are removed. Also: PATCH answers 404 for a missing task id even when the patch names a missing group; the routing-id cap is shared with session creation at 256 and the create_sub_session tool validates it up front; an unreadable .deletions sidecar no longer vetoes the tasks write (observations skip, deletion writes rebuild); stray routing fields on a non-per-run task are normalized on read instead of failing the file; the rollback watchdog strictly outlasts the agent close timeout and force-kills plus cleans the transcript on the timeout path; and the workspace-unavailable form error is localized. --------- Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|
|
7556821e65
|
fix(ci): retry transient E2E build artifact upload failures once (#11364) (#11375)
* fix(ci): retry transient E2E build artifact upload failures once (#11364) * test(ci): harden the e2e-build upload retry contract pins (#11364) Address review findings on the retry helper suite: scope the two-attempt assertions to the archive instead of every upload-artifact step and bind the attempts by step name, pin the pack-to-upload handoff (archive name and step order), pin the build job's absent job-level continue-on-error, pin the retry gate by whole expression so a failure() conjunct cannot silently disable it, compare the two with: blocks wholesale (minus the intentional overwrite asymmetry) plus the action pin, and compare downloaded names against every workflow upload instead of restating the four known legs. Also add the suite to the yamlSuites roster in ci-platform-lanes.test.js so it can never join the dependency-free fast lane undetected. * fix(ci): correct the upload-retry rationale and name-key its contract (#11364) Real-runner probes on the PR showed the comment's two reasons for the retry's overwrite do not reproduce: a stalled upload aborts before finalize and reserves nothing, and name uniqueness is scoped per run attempt so a manual re-run does not 409 either. Reword both comments to the window overwrite actually covers — a first attempt that finalized e2e-build and only then reported failure. The contract test picked "the archive" by position (uploads[0]), contradicting its own comment: an unrelated upload step placed before the archive upload reddened the suite with misleading messages. Key on the literal e2e-build name instead — already the pinned contract in scripts/tests/e2e-workflow.test.js — in both the filter and the download-leg assertion. Mutation probes: a decoy upload step before the archive now stays green, while decoy+retry-deleted, retry-deleted, retry-renamed, and overwrite-dropped all still fail. * test(ci): pin the pack-to-upload directory in the e2e retry contract (#11364) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(ci): pin the exact upload path in the e2e retry contract (#11364) Round 3's handoff pin closed the pack side but left the upload side pinned to a directory prefix, so moving only the two upload with.path values into a subdirectory of runner.temp kept the suite green while the runtime upload found no file and redded the build job. Compare first.with.path to the exact '${{ runner.temp }}/' + archive literal, reusing the derived archive binding rather than restating the name. Mutation probes: the upload-side drift now fails on the new assertion (16/1, expected-vs-actual), the workspace-escape and pack-side drift mutations stay red on their existing assertions, and the same upload-side mutation under the old prefix assertion stays green — the exact pin is the load-bearing one. Also reword the overwrite rationale in both the workflow comment and the test comment to the form the shipped action source supports: the record is created before any bytes move, so a failed first attempt can leave the e2e-build name taken, and overwrite is delete-then-upload and a no-op when nothing exists. The previous wording asserted a stall reserves nothing, which the create-then-upload lifecycle contradicts. And name the retry contract's actual ungated siblings (run-e2e-tests.sh's sandbox-image build retry, build-and-publish-image.yml's image build retry) instead of the budget-gated sandbox:none shard retry the comment over-claimed parity with. * fix(ci): pin the unpack archive name and restore the overwrite rationale (#11364) Round 4's handoff pin derives the archive name from the upload with.path, so renaming the tarball at the pack step and both upload paths while the four legs' unpack arguments stay put kept the suite green while every leg would fail closed on main (review R1-3). Pin each 'Unpack build artifact' run's trailing argument against the derived name; the legs download into runner.temp/e2e-build/, so the comparison is a suffix match, not the upload's full path. Mutation probe: a producer-only rename now fails the contract with 'a leg unpacks a different archive than the build job uploads' (16 pass / 1 fail), the unmutated workflow stays green (17/17), and the same mutation was green before this pin (17/17). Also restore the overwrite rationale that |
||
|
|
6a43348f7d
|
test(integration): assert compress telemetry success (#11259) | ||
|
|
38688f891f
|
fix(core): free an exited web terminal's PTY resources at exit time (#11572)
WebTerminalRegistry released PTY resources only in release(). handleExit set `exited`/`exitCode` and notified the exit listeners, touching nothing else, and the browser route deliberately keeps an exited session alive for scrollback replay (finishExited leaves releaseAfterReplay at its false default). The client will not release earlier either: a live exit closes the socket with 4000, which is non-retryable, so only a tab close, a workspace drain, dispose() or the 15-minute idle reclaim ever freed the PTY. So every exited web terminal held node-pty's conout worker - and, upstream, its conhost.exe - for up to IDLE_RECLAIM_MS. Exited sessions are also excluded from the admission cap on purpose, so accumulation inside that window was unbounded. Extract the PTY-resource half of release() into releasePtyResources() and call it from handleExit, deferred one setImmediate so the trailing onData callbacks node-pty may still have queued reach the buffer first - the same race shellExecutionService drains before finalizing. The helper keeps the session's map entry and its buffer, so readSnapshot() replay and the route's releaseAfterReplay path are unaffected, and it never signals the pid: the shell is gone and its pid may be recycled, which is why #11313 added releaseHost instead of reusing kill(). A per-session flag keeps a later release() from disposing anything twice. release() now routes both arms through the helper. killPtyTree stays on the live arm only and still runs first, so releaseHost keeps seeing the close the wrapper noted. Everything between the old detach site and the new one is synchronous (spawnSync, process.kill, pty.kill), so no onData callback can interleave and the live path's observable behaviour is unchanged. Adds three tests: resources freed at exit time rather than at the reclaim, scrollback still replayable afterwards, and no double free when release() follows. Verified at the mechanism level on Linux with os.platform() mocked to win32; actual conout-worker and conhost.exe reclamation on a real Windows ConPTY is not verified here, and #11352's upstream close defect is untouched. Fixes #11353 Patrol-Run: qwen-issue-patrol/jmtvhcstvvk Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
8eb33a860a
|
perf(core): drop the redundant history clone in session recovery planning (#11595)
Classifying a resumed session deep-cloned the rebuilt API history twice: once to keep a pristine copy for interruption detection, and once for the orphaned-tool-use repair to mutate in place. Detection only ever reads, so the pristine copy can alias the array the caller passed in — every caller already hands over a snapshot it owns, and the repair still runs on its own clone so that snapshot is never modified. On a synthetic 50k-record transcript this removes ~85 ms from a resume with no compression and ~15 ms from one that compressed, against a total resume cost dominated by the transcript parse. |
||
|
|
5a9c9b3406
|
feat(goal): stop a Goal at a turn or an active-time budget (#11457)
* feat(goal): stop a Goal at a turn or an active-time budget The token budget is a runaway-spend guard, sized for that job: 30,000,000 tokens by default, which a healthy long run reaches late. It is not what a user reaches for to keep a Goal short -- people say "at most twenty turns" or "at most half an hour". Neither had any effect, and the objective template invites the first phrasing while the user doc has to explain that writing it configures nothing. Add two ceilings beside the token one, both off by default: `model.goalMaxTurns` counts finished Goal turns and `model.goalMaxActiveMinutes` counts the wall time the Goal spends active. The meters were already on the record; only the ceilings and the stop were missing. A Goal that reaches either takes the path a spent token budget already takes: one wind-down turn to hand off, then `usage_limited` with a new `limitKind` (`turn_budget` or `time_budget`) and a reason naming the budget. A resume authorizes another window on top of what was used and moves only the ceiling that ran out. Ceilings are read at the continuation boundary, so the turn that crosses one still finishes; a turn crossing several reports one reason, token first. - `spentBudget` replaces the runtime's direct token check, so the continuation gate, the settle, and the no-progress bound cannot disagree about whether a Goal is out of allowance. The no-progress pause yields to every spent budget, not just the token one, so a cadence stop still gets its hand-off. - The reducer's budget-resume branch keyed off the literal `token_budget`; it now keys off `isGoalBudgetLimitKind`, or a turn-stopped Goal would resume still rendering "ran its turn budget" as the reason it is active. - The wind-down line stops naming the token budget, since the hosts carry a plain `windDown` flag and cannot say which ceiling was reached. It points at the budget line above it, which grew to report turns against their ceiling and, when one is armed, active minutes against theirs. - Both new `limitKind` values reach the two whitelists outside core: the SDK's hand-copied union and the Web Shell's `getGoalState`, which rebuilds the record field by field and would otherwise drop them on the live path. Design: docs/design/2026-09-09-goal-turn-and-time-budget.md and its .zh-CN.md counterpart. * fix(goal): repair the assertions and mocks the budget change broke Six CLI assertions pinned the continuation prompt verbatim, and renaming the budget line's prefix and rewording the wind-down line moved that text out from under them. Two more CLI suites could not collect at all: they build their `@qwen-code/qwen-code-core` mock by enumerating exports, and `settingsSchema.ts` now reads two new caps at module scope. Neither was reachable from the verification steps this change shipped with, which ran only `runBudget.test.ts` and `config.test.ts` in the CLI package. `cd packages/cli && npx vitest run` reaches all eight. Also, while in these files: - `get_goal`'s `lastGoal` summary already reported `tokenBudget`, so a Goal stopped by a cadence ceiling could be inspected and show only the allowance that did not run out. It now reports all three, and the tool description says so. - Two validator doc comments said "-1 for no ceiling, or a positive integer"; the predicates accept `0` as the same opt-out, the way the token budget's sibling already documents it. - `settingsSchema.test.ts` now pins both new entries: integer, no default, bounded by their caps, no restart. * fix(config): reject signed zero for the Goal cadence settings, and say what the ceilings actually do The `excludedValues` guard added last round compared with `Object.is`, which tells `+0` from `-0`. `Number()` maps `-0`, `-0.0`, `-.0` and `-0e0` to `-0`, `Number.isInteger(-0)` is true and `-0 < -1` is false, so `/config model.goalMaxTurns=-0` walked past the guard, reported `Set ... = 0` because `String(-0)` is `"0"`, and `JSON.stringify` persisted the excluded value. Every later start in that scope then aborted on `must be > 0`, with `/config` unreachable to repair it -- the same lockout the guard was added to prevent. Compare numbers with `===` so signed zero matches, keeping identity for the string half of the field's type. The documentation of both ceilings overclaimed in three ways, each fixed wherever the claim is published -- the two schema descriptions and their generated mirror, the settings table, the user guide, both locales of the Web Shell placeholder, three places in the goal-draft skill, and the `propose_goal` objective example: - `-1` was described as running Goals with no ceiling. It only removes a ceiling a Goal has already spent, on the resume or edit that follows; a Goal still under its ceiling keeps it. - Neither ceiling is retrofitted onto a Goal already on the record, so the only way to bound one is to replace it. Nothing said so. - The minutes row read as a wall-clock cap. There is no timer: the ceiling is read between turns, so a turn already running is never interrupted and the window can be exceeded by that turn plus the hand-off. The design doc's "no behavioural change when off" bullet and its first acceptance criterion were also too strong, in both languages. Restoring an `active` Goal rebases `updatedAt` unconditionally, so the elapsed figure a restored Goal reports drops offline time for everyone, settings or not -- and drops with it any active time in an interrupted turn that no journal write had committed. Both are now stated rather than implied, with the reason the record cannot separate the two. Two promises the tests did not hold: - Three surfaces say user turns are still admitted at the ceiling, and the only case covering it admitted the turn one step below the limit. A case now reserves a user turn with the ceiling already spent and the hand-off in flight; adding a `spentBudget` gate to `beginTurn` reds it. - The active-time ceiling is the only one whose spent state can change between queueing a continuation and delivering it. A case now queues under the ceiling with no host and binds past it, asserting the hand-off; making `bindHost` flush directly reds it. * fix(goal): name what replacing a Goal costs, keep ceiling advice out of objective text Round-three review, four findings. Replacing a Goal is the only way to bound one already on the record, and the docs offered it without the cost. `/goal set` goes through `createGoal`: a new Goal at revision 1, turn, token and active-time meters at zero, and an evidence cursor at the replacement, so everything the old Goal recorded drops out of the citable catalog. Said so on every surface that offers the remedy -- the user guide, both settings rows and both schema descriptions, regenerated. The ceiling settings were pointed at from two opposite places: the goal-draft template slot said "outside the objective", while its exemplar, the user guide's Budget row and strong example, the `propose_goal` objective example and the Web Shell placeholder all wrote the pointer into objective text -- text re-sent every turn to a model that cannot act on it, advising a setting that by its own wording never applies to the Goal carrying it. One placement now: never in objective text. The rules-of-thumb bullet names the settings, the user guide's Budget row says to set them in settings, and the Web Shell pointer moves from the placeholder into the hint rendered under the same field. `SKILL.test.ts` pins both halves -- the settings are named in the skill body and absent from the template and the exemplar -- so they cannot drift apart. The strong example also gains the `On block:` clause the template asks for. The signed-zero guard is one `includes` call: SameValueZero already matches `-0` against a `[0]` exclusion, so the typeof-split ternary carried an arm no setting can reach. The `-0` assertion from last round still pins it. The settings table rows are trimmed to fit the widest existing row, so the table is not re-padded, and one shortening that would have misdescribed which values are rejected is reverted to the accurate list. * fix(goal): stop the ceiling docs promising more than the runtime does Round-four review, five findings, all documentation precision. Each one is fixed by narrowing a claim rather than qualifying it further, so the surface the next round can find fault with gets smaller. - Bounding a Goal already on the record: "replacing it with /goal set or clearing it, which starts a new Goal" said clearing starts a Goal. Clearing leaves none. Only the replacement starts a new Goal at revision 1; clearing is followed by starting again. Both settings rows and both schema descriptions now say so, matching the user guide. - "Time while the process is not running does not count" overclaimed. The only exclusion is the rebase a restart performs; a suspended process, such as a sleeping laptop, is charged. The claim now says exactly that. - The quantified overshoot, "that turn plus the hand-off turn", holds only when a turn is running as the window runs out. A window that runs out while no turn runs is not noticed until the next turn ends. The bound is dropped in favour of saying the Goal can run well past the window. - The Web Shell hint sent readers to settings for two keys that client's settings surface cannot reach. It now names `/config` in the chat, which runs in daemon sessions. - The second weak-to-strong exemplar still wrote a bare turn count into its Budget. It carries the advisory marker like the first, and the skill test now checks every exemplar row instead of one. --------- Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> |
||
|
|
af4dece3a7
|
feat(sdk): add a peer endpoint so a program outside Qwen Code can join cross-session messaging (#11560)
* feat(sdk): add a peer endpoint so a program outside Qwen Code can join cross-session messaging
`@qwen-code/sdk/peer` is an implementation of the cross-session protocol
for a program that is not itself a Qwen Code session — a voice
front-end, a relay, a build watcher. `PeerEndpoint.start({ name })`
binds an inbox and publishes a session record, so the program is listed
by `qwen sessions ps` and by every session's `list_agents`; `list()` and
`send()` address sessions by name; `onMessage` receives what they send;
receipts are applied as state transitions and can be awaited.
It is written from the protocol page with Node's own modules alone and
shares no code with Qwen Code. That makes it the page's first
independent implementation, and a conformance suite runs it against
Qwen Code's own registry, inbox, directory and send path in both
directions: listing and reaching each other, frames parsed and answered
each way, a folded drop receipt settling a burst, a user-minted
controller token recognised and a forged one not, identical verdicts on
hostile lines, record files and names, and every published limit pinned
to Qwen Code's constants.
The endpoint takes the shared `<pid>.json` when it is free and a minted
`<pid>-<8 hex>.json` when it is held, never replacing a record it cannot
prove dead. Its inbox accepts only its own token, it never deletes
records or sockets other processes wrote, and trust comes only from a
controller token. On Linux it refuses to publish a record without a
start token and PID namespace.
Implementing the page found two rules it left implicit, now stated in
"Writing your own record": `procStart` and `pidNs` are required on
Linux, and a held `<pid>.json` means writing the minted name instead.
Separate Node-only subpath, kept out of the browser-bundled entries;
the build checks its size and that it carries no runtime dependency.
* fix(sdk): present the controller token only on sends marked controller
Review follow-ups on the peer endpoint.
The controller token was presented on every send, so any process that
published a record under a name the program then addressed read the
plaintext of a credential Qwen Code only ever stores hashed. A send now
presents it only when marked `controller: true`; unmarked sends — from an
endpoint holding a token too — present the recipient's own token. On a
marked send, two records for one session id and name are ambiguous rather
than collapsed to the newer, so a copied record cannot take the grant. A
malformed `controllerToken` (empty, no `qpc_` prefix) is rejected at start
with `invalid-controller-token`, as is a marked send without one.
Other fixes:
- Two `start()` calls in one process raced for `<pid>.json` and one
record was lost; registrations inside a process now run one at a time.
- `send()` checks `closed` again after the directory read, so a send
overlapping `close()` rejects instead of writing a dead reply address.
- `ambiguous.matches` holds only addresses that select one session.
- Receipt `reason` is flattened and bounded like every other label.
- The local concurrent-send ceiling is marked `local` and described as
this program's limit, not as a busy peer.
- An explicit `socketPath` no longer re-permissions a directory the call
did not create.
- `probePeerSocket` is renamed `probePeerSocketVerdict`, matching core;
the status list is the single source of the status type.
- `flattenPeerLabel` matches control and format characters by Unicode
category and caps in code points, in core as well, so the two
implementations still agree.
- The build checks every emitted peer declaration, from a module with
tests of its own; the surface test is compiled by the typecheck and
matches imports as path segments.
Docs: `list_agents` and `send_message` need `agents.crossSessionMessaging`
on the receiving session; delivery without review also follows a matching
`fromMode`, and `agents.crossSessionInbound` outranks a controller grant;
a controller send to another endpoint is dropped unread; the endpoint's
inbox applies none of the protocol page's §6; the `socketPath` sibling
fallback; and the protocol page gives separate reasons for `pidNs` and
`procStart`. The design doc no longer says the default entry ships to
browsers.
Tests pin each of these, plus the paths the first round left unexercised:
ledger and answered-id eviction past their bounds, start rollback, the
Linux identity guard, registry directory tightening and read errors, the
over-long line with the default deadline, the symlinked and over-long
socket paths, prompt close, Windows, and a live PID with a mismatched
start token.
|
||
|
|
30a1624752
|
Merge branch 'main' into codex/web-shell-session-continue | ||
|
|
c46cb85cf2
|
fix(vscode-ide-companion): resolve diff paths once in DiffManager (#10494)
* fix(vscode-ide-companion): resolve workspace-relative paths in closeDiff * fix(vscode-ide-companion): resolve diff paths once in DiffManager The MCP openDiff/closeDiff pair stored and matched raw paths, so a diff opened with one form of a path could not be closed with the other. Resolving only on the close side, as this branch first did, made that worse: openDiff kept storing the relative key while closeDiff looked up the absolute one, so no close ever matched. Resolve once inside DiffManager instead, and split the two jobs the stored path was doing. originalFilePath stays exactly as the caller supplied it and is what goes back out in the notifications, because the CLI keys its pending openDiff promise by the string it sent. The new resolvedFilePath is what everything matches on: closing, deduping, focusing, and the active-editor check that compares against an always-absolute editor fsPath. This also fixes the open side for relative paths, which read old content and built the diff URIs from a path relative to the extension host's cwd. Drops the duplicated resolution blocks in showDiffCommand and FileMessageHandler in favour of the shared helper. * fix(vscode-ide-companion): stop closeDiff from cross-matching entries and mangling the echoed path Two separate bugs review found in the resolve-once consolidation: closeDiff echoed originalFilePath after it had been normalized, so any caller path that wasn't already normalize-stable (src/./foo.ts, a //, Windows forward slashes) came back as a different string than the CLI keyed its pending promise by. The promise never settled, disconnect() never released diffMutex, and every later openDiff hung forever. originalFilePath is now stored verbatim from the caller. closeDiff also matched purely on resolvedFilePath and took the first insertion-order hit. DiffManager is a per-window singleton shared by every session, so two entries can share a resolvedFilePath with different content (a webview permission preview and a second CLI session's proposed edit). Closing one could return the other session's content, which the scheduler then writes out as a "user edit" on the wrong file. closeDiff now prefers the entry whose originalFilePath matches the caller's form, falling back to the first resolvedFilePath match only when nothing matches exactly. Both are pinned with red-then-green tests. Claude-Session: https://claude.ai/code/session_01DE7wVFkA8f4EKdePsHm3ee --------- Co-authored-by: wenshao <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
92f575906a
|
fix(serve): load persisted MCP config after ACP preheat at startup (#11145)
* fix(serve): load persisted MCP config after ACP preheat at startup Reconcile workspace MCP configuration once the primary runtime bridge finishes preheating so persisted mcpServers settings are available without a manual reload. Fixes QwenLM/qwen-code#7771 * fix(serve): start workspace MCP discovery after ACP preheat Use the non-throwing primary runtime lookup and coordinator.ensure() so startup discovery runs on the workspace Config without re-enabling bootstrap MCP spawn. * fix(serve): skip keep-alive preheat after startup ACP warm-up Boot discovery now prepares the already-live runtime without a second client-route keep-alive preheat, so an idle daemon is not forced to a 10-minute channel window. Warm-start failures are debug-logged. * fix(serve): make ensure skip-keep-alive semantics explicit Document EnsureOptions.skipKeepAlivePreheat and the ensure overload so object-form live-runtime skip is no longer implicit. Log the two boot early returns, and use skip-path wording when a live snapshot dies before status(). * test(serve): restore registry after boot skip-path debug coverage Avoid polling daemon status after clearing the primary generation so the skip-no-runtime debug assertion does not race a broken status read. * fix(serve): hold the ACP child through boot MCP discovery Boot ensure({}) skipped keep-alive, so default channelIdleTimeoutMs=0 reaped the warm child before initializeWorkspaceMcp. Arm the 10-minute window and load createDebugLogger from the debugLogger subpath so the serve fast-path bundle stays closed. * fix(serve): address round-3 review suggestions for boot MCP discovery Extend the preheat success guard to skip discovery when runtime startup already failed, route boot diagnostics through daemonLog (including cause on failure), and add tests that go red if discovery runs on preheat failure, targets non-primary workspaces, or races shutdown/startup failure. * fix(serve): address round-4 review on boot runtime ensure Annotate the channel-worker snapshot mock for typecheck, split shutdown vs startup-failure skip guards, unwrap ensure failure causes for daemon logs, skip warn on WorkspaceDrainingError, rename logs to workspace runtime ensure, and add tests for cause-less failures and the runtimeStartupError guard arm. * fix(serve): drop bridgeErrors import and harden boot ensure guards Remove the forbidden bridgeErrors static import and classify ensure .catch failures via shuttingDown/runtimeStartupError. Check startup failure before shutdown when skipping post-preheat ensure, gate primary runtime on active state, and add tests for deepestErrorMessage, inactive primary, and the runtimeStartupError guard. --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
07b1cd033e
|
refactor(core): move the web_search DashScope client behind a backend interface (#11490)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
pnpm Worktree Smoke / Install (macos-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (ubuntu-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (windows-latest) (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* refactor(core): split the web_search DashScope client into a backend and surface page titles
The tool description tells the model to cite sources as [Title](URL), but the
result it receives carries bare URLs: DashScope's search items expose only
{type, url}, so the model either invents titles or pastes raw links. Ask the
search side model to open its reply with a "Sources:" block of "title — url"
lines, attach those titles to URLs the search actually returned, drop anything
it did not, strip the block from the narration, and render a source as a
markdown link when a title is known and as a bare URL otherwise — so a result
without titles is byte-identical to today's. The block is requested first
because a search that exhausts the request budget is salvaged from whatever
streamed, and anything asked for last is what a slow search loses.
Move the Responses client, stream parsing, retry and salvage logic out of the
tool invocation into a WebSearchBackend implementation behind a small
interface, leaving the tool with permissions, the result envelope, truncation
and the citation policy. No error path, timeout, retry or display line
changes, so another provider's server-side search can plug in beside this one.
* fix(core): stop the web_search Sources parser from editing narration or trusting page text
- Require a bullet for plain source entries and drop a matched block
all-or-nothing, so narration that merely ends in a URL survives and no
orphaned bullet tail is left behind; gate entry parsing on line length
to bound the regex backtracking.
- Mine titles only from narrated text: salvaged extractor page content
must not author its own citation title.
- Run response-declared titles through cleanTitle and strip URLs smuggled
into relayed titles; accept parenthesized markdown destinations and the
bullet glyphs models actually emit.
- De-duplicate the evidence tiers by normalized URL, drop the unread
instance-level backend kind member, and narrow the docs, interface
contracts, and the web_fetch bullet to the guarantees the code keeps.
* fix(core): escape backslashes in web_search link text
A title ending in a backslash consumed the escape added for the closing
bracket, so `[C:\path\](url)` reached the model as literal text and the
citation it was meant to protect was lost. Escape backslashes along with the
brackets. Also drop the now-redundant export on SIDE_REQUEST_INSTRUCTIONS —
nothing outside the module reads it, and the instructions are pinned through
the request the backend sends.
* fix(core): keep web_search citations reachable in the CLI and stop capped pages vanishing
Three findings from the review, all in how a result reaches the reader.
Percent-encode markdown destinations instead of wrapping them in <...>. The
wrap was meant to protect parenthesized URLs, but the CLI only turns a link
into an OSC 8 hyperlink when it can read a bare scheme, so the wrapped form
lost the hyperlink and showed literal angle brackets for exactly those URLs;
its own link pattern already handles one level of balanced parens. Encoding
keeps the scheme in front and also covers what wrapping never fixed: nested
or unbalanced parens, and `<`/`>` corrupting the OSC 8 target. Stop escaping
`)` in link text — it needs no escape there, and nothing unescapes what the
CLI renders, so it only showed the user a stray backslash.
Give the block stripper the set of pages the formatter will actually render.
Both evidence tiers are capped at 25, so a page past the cap had its Sources
line deleted from the narration and was never listed either — it left the
result entirely while the prose still credited it. Those lines now stay, with
their heading.
Fall back to extracted page text after the strip when the side model's whole
reply was the Sources block, so a successful result cannot arrive with no
findings at all. The fallback runs on the stripped value, never before it, so
raw page text is still never mined for titles.
* refactor(core): take page-title extraction back out of the web_search backend split
Two review rounds on the title feature grew from 16 findings to 29, seven of
them induced by the previous round's fixes, and nearly all of them land on one
mechanism: parsing a free-form Sources block out of the side model's narration
and then editing that narration. That design needs deciding on its own rather
than being patched one finding at a time, so this change returns the PR to
what it is underneath — the DashScope client moved behind a backend interface,
with no behavior change — and the title work moves to a separate issue with
its implementation preserved on its own branch.
Output is identical to the merge base. Across 28 mocked scenarios covering
success, truncation, the per-tier caps, stream salvage, every error mapping,
the no-search retry, request parameters and progress updates, the content the
model receives, the display line, the error, the request body and the client
options all match byte for byte. The merge-base test suite passes unchanged
apart from the new `kind` discriminator on the resolved backend config.
Also drops the re-export of WebSearchBackendConfig from web-search.ts, which
nothing imported.
---------
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
|
||
|
|
0bb84a4153
|
chore(release): sdk-typescript v0.1.12 (#11589)
* chore(release): v0.23.3 * chore(release): sdk-typescript v0.1.12 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
ed2791e6a2 | fix(web-shell): preserve continuation across reconnects | ||
|
|
4815941476
|
feat(goal): start approved Web Shell proposals after their owning turn (#11360)
* feat(goal): start approved Web Shell proposals after their owning turn * fix(goal): close proposal lifecycle gaps * fix(goal): address settlement review gaps * fix(goal): block proposal settlement when a guard continuation yields to a queued prompt (#11360) #runStopContinuation's skipped-send funnel returned a plain end_turn for the queued-prompt and unreliable-drain stops without marking the turn's settlementBlocked flag, so a parked proposal approved earlier in the turn was applied even though the turn ceded to the queued user prompt. Set the flag at those three stops, mirroring the loop-level exits, and pin each with a regression that fails without it. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): deflake the ab-drive run-dir sweep counts against foreign temp dirs (#11360) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(goal): revoke proposal approval when entering Plan mode * fix(goal): give ACP proposal settlement one owner --------- Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
c3d9bed85b
|
fix(serve): restore configured channels after restart (#11472)
* fix(serve): restore configured channels after restart * fix(serve): make configured channel restore fail soft * fix(serve): identify configured channel source * fix(serve): harden configured channel recovery * fix(serve): bound configured channel startup restoration * fix(serve): preserve fail-soft channel startup --------- Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> |
||
|
|
cbd2cbadec
|
fix(acp): Preserve submitted prompt provenance for auto recall (#11455)
* fix(acp): Preserve submitted prompt provenance for auto recall Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(acp): Omit submitted_prompt on channel-classified turns (#11455) Channel turns are machine-relayed deliveries (loop jobs, webhook tasks, adapter-synthesized events), so nothing on them crossed a submission boundary. Since submitted_prompt presence alone gates Auto Recall's outbound provider search, emitting it there ran retrieval on unattended turns and could publish composed wrapper text as user provenance. Gate emission on a positive user-submission condition instead. Also reconcile the producer-set contract this PR changed: hooks.md, the Auto Recall READMEs, the in-product hook description with its locale mirrors, and the recording payload comment now state the actual rule, and the new design doc records the channel exclusion plus the strict-decoder rollout warning. * fix(acp): Require explicit submission provenance for auto recall Carry original Web Shell composer text separately through admission and queues; keep background dispatches and undeclared requests ineligible. Reconcile hook and integration contracts and add regression coverage. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
67fe20a838
|
fix(autofix): charge regressions to the brake and gate test weakening (#10188)
* fix(autofix): charge regressions to the brake and gate test weakening
Two holes let an autofix round ship a new problem for free.
A round that pushed a fix and turned CI red counted as a SUCCESS: the
consecutive-failure brake counts "rounds that pushed nothing", so the red
it created reset the counter, came back as the next round's input, and was
paid for out of the round budget. A PR could alternate regress/repair
indefinitely while every brake read it as converging.
Every acted round now stamps what it pushed and whether the head it pushed
onto was fully green; the next round charges a regression only when the
live head is exactly that sha, the marker says green, the window key
matches, and the head is red now. The observing round records it in
whichever report it posts, and the brake stops resetting on a regressing
round. Attribution is conservative on every axis: a moved head, a re-arm,
a pending check at push time, a cancelled run, or the loop's own lanes all
drop the charge.
The second hole: relaxing an existing assertion was invisible to the whole
gate. Build/typecheck/lint never read assertions, the package tests run the
weakened file, and the bite check reads only the tests a round ADDS. The
SKILL already required content evidence for deleting or weakening a test;
nothing enforced it. The gate now measures deleted test files, net
assertion removal, and added skip/todo markers, and rejects the round
unless each file is recorded in test-weakening.json with a reason. It
judges that the claim exists, not that it is right — the reasons ride into
the round report for a maintainer to read against the diff.
Deletions get their own merge-freight discriminator: content equality reads
"absent on both sides" as identical, so a round deleting a test the PR
itself added looked like freight and escaped. The merge base separates the
two cases.
* fix(autofix): attribute test weakening to round commits, close marker escapes
* fix(autofix): close weakening-gate merge/typechange escapes, own-lane charge leaks (#10188)
* fix(autofix): close weakening-gate comment, attribute, and name-framing escapes (#10188)
* fix(autofix): close weakening-gate masking escapes and delete/restore false charge (#10188)
* fix(autofix): close weakening-gate lexer, marker-shape, and verdict escapes (#10188)
- WEAKEN_ASSERT_RE: left-boundary class (console.assert( never fails a
test) and a matcher requirement on added expect( lines (matcher-less
expect passes for anything); the del side stays bare so a
multi-line-formatted assertion removal still charges
- skip markers: concurrent/sequential/shuffle chains, tagged-template
each tails, computed accessors, and newline-split chains measured on
a joined view; sd counts comment-stripped del lines
- weaken_strip_comments gains string/template state (an in-string /*
can no longer poison the strip); weaken_strip_code gains
regex-literal and template-nesting state
- modify/delete merge resolutions that KEEP the file apply no freight
- the net-range deletion arm enumerates the explicit pre-round->tip
pair and is judged even when per-commit measurement is UNAVAILABLE
* fix(autofix): close weakening-gate member-assertion, marker-spelling, and lexer escapes (#10188)
- WEAKEN_ASSERT_RE/ADD_RE: the member arm requires the CALL (an
exported assert alias executes no assertion), and supertest's
throwing member form .expect( counts on every arm
- WEAKEN_SKIP_RE: backtick-quoted computed accessors, the ?.
optional-chaining spellings (it?.skip( and it.skip?.(), and the
reversed chain with an execution modifier behind the marker
- weaken_strip_code: regex-vs-division is keyed on the last
significant TOKEN (an operator including /, a regex-head keyword
preserved across whitespace, or a control-head close paren tracked
on a paren-kind stack), not the last output character
- weaken_strip_code/weaken_strip_comments: a ${} hole closes on the
brace matching its own ${ (per-hole brace depth), not the first }
inside the hole (an object literal or arrow body)
- the skip-marker net gains the whole-blob backstop twin of the
assertion arm, so a genuine addition cannot net against an
in-comment marker deletion or hide behind a block-comment span
opened above the hunk
- twelve witnessed fixtures (each guard flips its own test red under
mutation); fixtureWrite emits single-quoted shell words so
backtick/${ fixture content survives
* fix(autofix): close weakening-gate merge coverage, freight phantom, and lexer escapes (#10188)
* fix(autofix): close weakening-gate cross-commit, merge-discard, and skip-surface escapes (#10188)
* fix(autofix): close weakening-gate matcher-credit, each-skip, and merge-recount escapes (#10188)
* fix(autofix): close weakening-gate tail-drop, skip-netting, fast-forward, and JSX escapes (#10188)
* fix(autofix): stop the weakening gate charging honest test edits (#10188)
Three of the four measured defects were false rejects the gate published
as machine-measured fact, and the fourth was test surface it never saw.
The matcher-tail census counted every member call in a stripped blob, so
deleting a spy and its restore read as three removed assertion lines;
scope it to continuation-shaped lines, the only shape that arm exists to
see. A fast-forwarded main recorded a recount base only for files it
added, so main's own delta was charged to a round that synced and then
renamed a symbol; record bases for modified files too and read the ledger
instead of set membership. The body-call skip arm charged vitest's
condition-valued dynamic skip, which is the runtime spelling of the
.skipIf environment guard the doctrine already exempts. And `*_tests.rs`
matched no pathspec arm, leaving two tracked Rust test files invisible.
Each fix carries a witness that a mutation probe confirms goes red
without it; the pinned guards for all four arms stay green.
* fix(autofix): measure test weakening with the parser, not a text census (#10188)
Replace the awk lexer, the four extended regexes, the matcher-tail census,
the per-commit accumulators, the verdict-time recount and the netting skip
with one instrument: count-test-surface.mjs parses the whole file with the
TypeScript compiler and reports its declared test surface (statement-level
assertion chains, test/describe registrations with their enabled state,
bare early returns ahead of assertions). Each file's round delta is
tip - pre-round - main's contribution, where main's contribution across a
merge is measured on git's own auto-merge of main's side (git merge-file
--ours) and across a fast-forwarded main commit on the commit itself, so
the same weakening measures the same before, during or after a merge, and
main's delta neither charges nor shields.
Narrow the pathspec to test-NAMED files (a README under integration-tests/
is not a test deletion), read the enumeration producers' status instead of
swallowing it, re-apply the reason bar when rendering, and state the
declared non-goals in the gate header: reachability, condition-valued
guards and by-reference options are runtime facts the runner-backed
instruments judge.
Bind the af-155 regression classifier to the commit its rollup describes
(headRefOid read with the rollup; a moved head classifies none), exclude
the loop's own dispatch-pending StatusContext by its exact context value,
classify verdict-less checks (EXPECTED, STALE, STARTUP_FAILURE) as pending,
stamp pre=none after a salvage-merged push, drop the unread CHECK_STATE
from the failure report's env, and render the regression disclosure on
every report shape.
Drop the hand-rolled CommonMark fence model from workflow-size.test.js: the
existing anchor/pointer/contents pins already prove navigation, and a text
model of a renderer is the class of instrument this change removes.
* chore: record the merged qwen-autofix.yml size
* fix(autofix): stage the surface counter with the pre-merge absence guard (#10188)
The stage step copies count-test-surface.mjs from the trusted base, but
the script is new in this PR: pre-merge the base lacks it, and a bare cp
under the step's -e kills every PR-event round whose workflow resolves
from the PR's own ref — the deferral the reviewer kept re-reporting.
Give the cp the heartbeat guard: rm -rf first so a planted leftover
cannot survive a failed copy and be executed as trusted (af-149), then
|| true so absence skips; the gate already fails open on a missing
counter (WEAKEN_MEASURED=false). Pin the pair and their order beside
the heartbeat pins.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(autofix): close the weakening gate's fail-open paths and make green an allowlist
Answers rounds 21-26 by retiring mechanisms rather than adding entrances.
Head-state classifier (af-155): GREEN is now an allowlist, never the
fall-through. A charge-enabling verdict requires every counted check to
hold a state the classifier recognises as passing, so CANCELLED, NEUTRAL,
SKIPPED and any state GitHub adds later read as pending instead of minting
the premise. (R22-3)
Test-surface counter: a run that exits 0 without printing a readable
verdict is a measurement FAILURE, not a verdict. weaken_measure validates
the shape before returning it, so the file takes the existing fail-closed
arm ("test surface could not be measured") instead of being silently
uncharged while the round still reports itself measured. (R26-2)
Merge attribution: git merge-file's failure test is its exit status, not
its output size. --ours resolves conflicts and exits 0, so a legitimately
empty result (main emptying a test file) was never conflated with failure
-- the line is replaced anyway so the intent no longer has to be
re-derived from a size test, and pinned by a fixture that makes main's
side zero bytes across a real merge commit. (R21-4)
Report-post fallback: gated on the af-155 state the round AUTHORED, not on
its outcome. A no-op round's observation about the PRIOR round was
unrecoverable once its report failed to post, because the marker it read
is superseded by the next push and the workflow's handoff comment excludes
exactly the fixed/noop outcomes. (R22-1, R26-3)
Instrument staging keeps the pre-merge absence guard it was given here and
moves to its own step after npm ci, so the parser is a .cjs copy of the
typescript the TRUSTED lockfile installed; both digests travel in
expression context and are verified before the counter executes. A
verbatim absence probe runs the step against a script-less tree with
leftovers planted in both shapes.
Boundaries stated where they can be read: --ours is the attribution model
and its error deliberately lands on the charging side; the base axis is a
documented residual with the same bound as the flake carve-out; the SKILL
no longer promises assertion counting for the Python and Rust shapes,
which are watched for deletion alone.
508/508 in scripts/tests; five mutants (green default, verdict validation,
emptiness-as-failure, outcome-gated fallback, no-op event overwriting the
baseline) all killed.
* test(autofix): execute the paths the weakening gate's own guards protect
A reverse audit of the previous commit found two guards that no test
reached, both load-bearing in the fail-open direction, plus one hazard in
the counter's CLI dispatch.
git merge-file refuses a file carrying a NUL byte (exit 255, no output),
and the branch's own side has to stand in for the auto-merge then. Nothing
executed that arm: making the substitution unreachable left the whole suite
green. Without it the refusal's empty output reads as main having emptied
the file, which credits the round with the entire baseline surface and
masks a removal of up to that size. New fixture `binary-merge` -- main
appends a comment, the round drops an assertion, and the NUL makes every
side of the merge binary -- charges `net 1 assertion(s) removed` and fails
when the substitution is removed. The round rewrites the file rather than
filtering it, because a line filter reads a NUL-bearing file as binary and
would empty it instead.
The staging step's present-counter branch -- the one that runs in
production once this lands -- was pinned only by text. Swapping `mkdir -p`
and `cp` inside it left the suite green while killing every round, because
the step is unconditional and runs under `-e`. The probe now runs the step
body the way GitHub runs a `run:` block (`bash --noprofile --norc -e -o
pipefail`) in both shapes: the pre-merge tree that lacks the script, and a
tree that has it with a parser planted where npm ci puts one, asserting the
staged bytes and both digests.
The counter's CLI dispatch ran at module scope, so importing it executed it
against the importer's argv -- a positional the importing program owns
lands in the unknown-mode arm and takes that process down with exit 2. It
now runs only when the file is the program, compared as REAL paths: the
gate runs it from RUNNER_TEMP, which on macOS is reached through
/var -> /private/var, and a literal comparison silently disables the CLI
and reports every file unmeasurable. Both halves are pinned, including a
run through a symlink.
Also: the head-state classifier keeps the status axis it had, so a check
still in flight is pending even when it carries a conclusion from an
earlier attempt; and the dialect table is exercised for every extension the
pathspec selects, not only `.ts` -- deleting the JS entries was invisible
to 52 tests while every tracked `*.test.js` file is measured by the gate.
509/509 in scripts/tests. Mutants killed: the merge-file substitution made
unreachable, the staging order swapped, the main-module guard compared
literally, the JS dialects deleted, plus the five from the previous commit.
* fix(autofix): follow git's trivial-merge rule when merge-file refuses
A reverse audit of the previous commit found that its new binary-merge
fixture pinned THAT a substitution happens, not WHICH side it takes -- and
the side it took was wrong half the time.
When `git merge-file` refuses (a NUL byte makes the file binary, exit 255,
no output), the previous commit substituted the branch's side
unconditionally. That is right only when main's side is unchanged from the
base. When MAIN is the side that moved, it reads main's own weakening as
the round's and rejects a round whose only act was `git merge origin/main`
-- the exact property the neighbouring main-empties fixture already guards,
re-broken by a second trigger into the same substitution.
The fallback now follows git's own trivial-merge rule: a side that did not
move from the base contributes nothing, so the other side IS the result,
and only a genuine two-sided change resolves for the branch the way
`--ours` would have. Three fixtures pin the three shapes -- the round
weakens (charged), main weakens (accepted), both move (charged for the
round's own removal only) -- and both fixed-side choices now fail.
The head-state classifier's status axis becomes an allowlist too. The
denylist it had enumerated four in-flight states and missed REQUESTED, a
real CheckStatusState member that four other sites in this same workflow
already list; the axis is now `status == "COMPLETED"`, with an absent field
reading as finished because a commit status carries no status field at all.
Both axes are allowlists for the same reason a denylist forgets the member
it was not told about -- which is how CANCELLED entered the green arm in
the first place.
The classifier's rationale moves to qwen-autofix.md#af-155, leaving the
workflow 1024 bytes below the hard gate.
509/509 in scripts/tests. Mutants killed: always take the branch's side,
always take main's side, the four-member status denylist restored, the
status axis dropped entirely.
* fix(autofix): bound main's contribution by what the merge actually took
A third reverse audit found a false ACCEPT in the attribution model, in
both lanes: the auto-merge is computed from the two sides and never
consults the merge commit's own tree, so a merge that DISCARDED main's side
still books main's delta as a contribution.
A round can therefore merge main with `-s ours` and delete an assertion in
the same breath: the credit for main's discarded weakening absorbs the
round's own removal exactly, and the round ships uncharged. The previous
commit widened the reach of this by extending the trivial-merge rule to the
binary lane, where the same shape had been rejected before.
Main's contribution is now bounded by what the tip took. When the merge
result is the branch's side byte for byte, main contributed nothing to that
file, whatever an auto-merge would have produced, and nothing is
subtracted. A resolution that took main's version, or produced something
new, still goes through the model unchanged -- including `merge-conflict-
drop`, whose resolution commit differs from both sides.
Two fixtures pin the shape in both lanes: `strategy-ours-hole` and
`strategy-ours-hole-binary`, each a round that merges `-s ours` and removes
an assertion. Deleting the bound leaves both accepted.
509/509 in scripts/tests.
* fix(autofix): clamp main's credit by what the merge actually landed
The previous commit's bound was byte-exact: it fired only when the merge
result equalled the branch's side to the byte. A fourth reverse audit
showed one added comment line inside the merge reinstates the whole hole,
and the blast radius is main's ENTIRE modelled delta -- a round can strip
every assertion from a pre-existing test file and ship it uncharged.
Main's contribution is now the part the MODEL and the LANDED blob agree
on, and only where they agree in sign. Equal, and the clamp is the
identity: the ordinary merge measures as it always did. They diverge
exactly when a resolution took neither side whole, and then the clamp cuts
both ways -- a resolution that discarded main's side credits nothing, and
a resolution that weakened the file itself is not main's contribution
either.
`weaken_auto_blob` emits three lines now, always: the modelled auto-merge,
an optional override for the side to compare against, and the blob the
merge commit actually holds. Presence is deliberately NOT clamped: the
baseline is what the round had available to weaken, and letting the merge
result define it would let a round discard a test main added and answer
for nothing -- the one thing the deletion signal exists to catch.
What the clamp cannot see is identity. Main removing one assertion while
the resolution puts it back and drops a different one nets to zero, the
same way an assertion moved within a file always has; that boundary is now
stated in the header beside the model.
Fixtures: `ours-hole-plus-comment` is the evasion the byte-exact bound
missed, and `binary-resolution-takes-main` pins the binary fallback's
branch-side arm -- which the clamp had quietly un-covered -- at its
declared over-charging direction.
509/509 in scripts/tests. Mutants killed: the clamp returning the model,
the clamp returning the landed delta, the landed blob never emitted, the
binary fallback's else arm taking main's side. One survivor recorded: that
arm taking the merge base, which the clamp makes equivalent in every shape
a fixture can reach.
* fix(autofix): clamp only main's removals, never its additions
The clamp the previous commit added was sign-symmetric, and its two signs
have opposite consequences. Capping a REMOVAL cuts the shield, which is
what it was written for. Capping an ADDITION cuts the charge -- so a round
could discard whatever main added to a test file mid-round and pay
nothing. A fifth reverse audit demonstrated it through the real gate in
three spellings, none of them needing a merge strategy flag: an ordinary
merge resolution that drops the assertion main just landed, a
delete/modify resolution that drops the test main appended, and the same
resolution keeping that test registered `.skip`.
The commit that added the clamp already left PRESENCE unclamped for
exactly this reason, and said so; the reasoning was never carried into the
deltas. It is now: main's additions raise the baseline whatever the merge
kept, and only its removals are credited as far as they landed. It also
contradicted a promise the gate header makes -- the same weakening
arriving by fast-forward was charged while the one arriving through a
merge was not.
The event skip goes back to testing the model alone. Its second half was
added to let a landed-only divergence through, but the clamp returns zero
for every such event, so the only thing processing it could still do was
latch `baselinePresent` off a side main never held: a file the round
authored itself, touched while resolving a merge and dropped afterwards,
was charged as a deleted pre-existing test.
Six fixtures, all through the real gate: the three discard spellings
(charged), the round's own file edited in a merge and dropped (accepted),
main removing two where the resolution keeps one and the round then
removes another (charged for its own), and main disabling a test the
resolution keeps enabled before the round disables it (charged).
509/509 in scripts/tests. The four mutants that survived the previous
commit are now killed: both clamp branches, and `clampBag` returning
either bag unclamped. Presence clamped by the landed blob, and the skip
processing every event, are killed too.
* fix(autofix): measure main's contribution where main made it
A sixth reverse audit found two more defects, and both trace to the same
mechanism: main's contribution was read off a three-way SPLICE of the two
sides (`git merge-file --ours`), which mixes the round's own edits into
what is supposed to be main's delta.
The splice can invent a surface neither side ever had. Witness: the round
un-skips a suite, main adds a test inside that suite, and the resolution
takes main's file verbatim -- so the tip is byte-identical to main's and
nothing was weakened. The splice combined the round's un-skip with main's
addition into an ENABLED registration that existed in no tree, and the
round was charged for disabling it.
The splice also decided PRESENCE, through an endpoint that is the branch's
side in two arms. A path both sides added during the round, resolved to
main's file and then deleted by the round, read as "the baseline never
held it" and shipped uncharged -- while the gate's own
`weaken_baseline_holds`, the definition its fail-closed arm uses, said the
opposite.
So the splice is gone. Main's contribution is main's OWN delta, measured
on main's own side against the merge base, and presence follows main's own
side directly. That makes the two arms one rule: a modify/delete resolved
for main was already measured from the merge base. `git merge-file` leaves
the gate entirely, and with it the binary-refusal fallback, its
trivial-merge rule and the over-charge they carried -- a resolution that
takes main's version now charges the round for nothing, where the model
used to bill it for main's removal too.
What the merge actually DID with main's contribution still clamps it, as
before: removals credited only as far as they landed, additions never
clamped.
509/509 in scripts/tests, with the gate section 60 lines shorter. Four new
fixtures: the splice artifact (accepted), the add/add path dropped after
the merge (charged), a resolution that removes more than main did (charged
for the difference), and one that moves opposite to main (charged for the
round's own removal only). Mutants killed: main's delta measured against
the branch side, presence forced either way, removals credited from the
landed blob, the landed guard dropped, additions clamped down.
* fix(autofix): decide the baseline by what main LANDED, not by what it holds
A seventh reverse audit found three defects in the new model. Two are
false rejections with high reachability, one is a false accept that a git
tie-break decides.
Presence flipped true whenever main held the file at a merge, even when
main's side was byte-identical to the merge base -- main landed nothing
there. So a test file an EARLIER round removed, or renamed, is charged
`test file deleted` again in every later round that merges main, until
main also loses the file. The gate's own header promises a rename costs
one ack entry naming the old path; it was costing one per round.
Presence now moves only when main's side and the merge base DISAGREE about
the file existing. Main adding it during the round puts it in the
baseline, and the round answers for dropping it afterwards. Main deleting
it takes it out only when the merge adopted that deletion. Main merely
still holding a file it has always held says nothing.
The "not an event at all" early return is gone with it. It dropped main's
deletion from the running total while the NEXT merge's base already
reflected that deletion, so main's re-add was credited from zero a second
time: add, delete-with-the-round-keeping-its-copy, re-add was charged
three assertions where one was removed. Every merge event is recorded now,
and the clamp handles the resolution that kept the round's copy -- its
landed delta is zero, so main's modelled deletion credits nothing.
A criss-cross history has more than one equally valid merge base and git
picks one without promising which. Since main's delta is measured against
that base, the pick decided the verdict -- and one of them credits main
with the ROUND's own removal, waiving a real weakening. The file is now
refused rather than measured: `test surface could not be measured`, which
one ack entry answers.
509/509 in scripts/tests. Three fixtures for the presence rule and the
event chain; both presence mutants (latching main's hold unconditionally,
and clearing on main's deletion whatever landed) are killed. The stale
auto-merge prose in the counter's header, its clamp comment, an orphaned
doc comment in the gate and two leftovers in the suite are corrected.
* fix(autofix): chain main's events, and let the merge decide what it adopted
An eighth reverse audit found that the previous commit's headline fix does
not work and its fixture is vacuous, plus two false accepts it introduced.
The removed early return was load-bearing. Its case -- main deleted the
file, the merge kept the round's own copy -- is now recorded as an event
whose `landed` blob IS the round's copy, so a copy weaker than the merge
base credits MAIN with the round's own removal. The round guts a test file,
main happens to delete it, the resolution keeps the gutted copy, and the
round ships uncharged. The early return is back.
What that removal was meant to fix is fixed properly: main's events CHAIN.
Each event now measures main's side against main's side at the PREVIOUS
event, not against a fresh merge base that already reflects it, so the
events telescope to main's own net for the round. Add, delete-with-the-
round-keeping-its-copy, re-add was charged three assertions where one was
removed -- and it still was after the previous commit, because recording
the deletion with a landed delta of zero is behaviourally identical to
dropping it. The fixture missed it by re-adding byte-identical content, so
the third merge was not an event at all; it re-adds a comment line now.
Presence follows what main CONTRIBUTED, not what it holds. `mainHolds`
alone exempted a whole path for the round whenever the pre-round ref
lacked it -- including when main GREW it and the round restored main's
file and gutted it. The rule is now: main holding the file and having
moved it puts it in the baseline, whatever the resolution then did; main
deleting it takes it out only when the merge adopted that deletion.
The criss-cross refusal was charging honest rounds. More than one merge
base only matters when the candidates DISAGREE about the file, which is
now the test; when they hold the same blob the pick cannot change the
verdict and the file is measured. Two fixtures pin both sides of that.
516/516 in scripts/tests. Six mutants killed, each the exact regression it
names: the early return removed, the chain removed, presence from
`mainHolds` alone, presence cleared without checking the base, the refusal
made unconditional, and the refusal dropped. The stale presence comment
the previous commit missed, and `weaken_baseline_holds`'s claim to be the
same definition the counter reports, are corrected.
* fix(autofix): drop the event whenever main holds no side and the file stays
A ninth reverse audit found that the reinstated early return was too
narrow, that presence read byte movement where it meant surface movement,
and that the criss-cross guard compared the wrong identity.
The early return required the merge base to hold the file. One merge
later it does not: after main deletes a file the round kept, the NEXT
merge sees no main side AND no base, the event is recorded, and the
round's own copy becomes the landed blob that sets the clamp -- crediting
main with exactly the round's removal, one merge after the guard that
exists to prevent it. The base is not part of the test any more: main
holding no side while the file stays in the round's hands is the same case
however the history reached it.
Presence asked whether main's side moved BYTES. Main appending a comment
to a file an earlier round deleted contributes no coverage, and reading it
as a contribution re-charged that deletion in every round main happened to
touch the file -- and made the verdict depend on how many times the round
merged main. It asks about the measured SURFACE now: the totals and both
registration multisets.
The criss-cross guard compared `git rev-parse <base>:<path>`, which
returns a TREE oid for a directory while the measurement treats a tree as
no blob at all. Two bases that agree for the measurement read as a
disagreement, and an honest round was charged unmeasurable on a tie-break
that could not have changed the verdict. It compares blob-or-absent
identity now, the same question `weaken_blob` asks.
One finding is answered with a stated precondition rather than code. The
model reads main's side as MAIN's, which holds while origin/main does not
contain the round's own commits -- true here, where pull requests land
squashed and main's first-parent history carries no merge of a PR branch.
A guard for it was written, found unreachable from every history these
fixtures can build, and removed in favour of saying so in the header.
516/516 in scripts/tests. Three fixtures, and with them the two mutants
the audit found surviving are killed: the caller's skip of a dropped event
(previously indistinguishable, because no fixture had a prior main event
for the file) and the early return's base condition. The clamp's
`landedHolds` condition is now redundant for manifests the gate produces,
and says so.
* fix(autofix): read movement from the bytes where the surface is unmeasurable
A tenth reverse audit found that the previous commit's surface test
disabled the deletion arm for every non-JavaScript test file -- 45 Rust
and 18 Python paths in this repository.
`count()` reads no surface from those shapes, so two present blobs always
compare equal and "main moved it" could never become true. Once the
pre-round ref lacked such a path, nothing main did during the round put it
back in the baseline, and the deletion arm -- the only arm those shapes
have, and one the gate header promises never fails open -- was waived.
Movement now falls back to the bytes when neither side is a dialect the
instrument reads, because there "the surface did not move" is unmeasurable
rather than false.
The surface test also compared main's DISABLED registrations, which reach
no signal: only the tip's do. Comparing them raised the baseline for a
change the gate can never charge -- main adding an `it.skip` re-charged a
deletion an earlier round had already answered for, which is the exact
failure the surface test was written to remove. It compares the chargeable
quantities now: the assertion totals and the enabled-registration
multiset, of which `enabled` is a derived count.
The criss-cross comparison goes through `weaken_blob`, the reader the
measurement itself uses, instead of a second identity built from
`rev-parse`. A tree, a gitlink and a missing path are "no blob" to all of
them alike, and a mode-only difference is the same blob, so the guard
refuses exactly the disagreements that could change a verdict.
Two fixtures for the deletion arm: a Python file main grows while the
round keeps its earlier deletion, and a file main adds whose measured
surface is indistinguishable from an absent one. Both are charged; both
fail if the fallback or the presence rule's existence test is removed.
The `main-absorbs-round-commit` fixture is deleted. The audit showed it
measured nothing -- the file is byte-identical on both sides of its merge,
so the event filter drops it before attribution runs -- and its comment
claimed a mechanism that was not the one making it pass. The precondition
it stood for is stated in the header instead, with its justification
corrected: a squash-merge of the round's own work into main mid-round is
the spelling that breaks the model, not a merge, which is self-correcting
because the merge base carries the same commit.
516/516 in scripts/tests.
* fix(autofix): decide the byte fallback by what was measured, not by the extension
An eleventh reverse audit found that the previous commit's fallback keyed
on the file's EXTENSION rather than on whether the instrument read
anything. A `.test.ts` that registers nothing measures exactly like a
`.py` -- and this repository has one, a type-level fixture under the CUA
driver -- so its deletion could still be waived after an earlier round
removed it. The test is the measured surface now: no registration of
either kind and no assertion, on both sides.
The audit also showed the previous commit's narrowing of the surface
comparison changed real verdicts with no coverage, in either direction.
Main moving only DISABLED registrations is no coverage and reaches no
signal, so it must not put a file an earlier round deleted back into the
baseline; re-adding that comparison now fails a fixture, as does dropping
any of the three fields that remain.
The criss-cross guard's presence arm was equally uncovered: both shipped
criss-cross fixtures disagree about the file's CONTENT, and the variable
that tells "no blob" from "the first candidate" was free to be removed. A
fixture whose candidate bases disagree about whether the file exists at
all pins it.
Three fixtures, three mutants killed: the fallback keyed on the extension,
the surface comparison widened back to disabled registrations, and the
criss-cross presence guard removed.
The header's three claims are corrected. It said movement falls back to
the bytes for "the Python and Rust shapes" (it is any file that measures
to nothing, in any dialect), that presence follows main having moved the
surface (main ADDING the path raises it with no movement at all), and that
comparing merge bases through `weaken_blob` buys agreement on trees,
gitlinks and mode-only changes (the identity it replaced already had all
three; what it buys is one reader instead of two that can drift apart).
The byte fallback's cost -- main touching such a file re-charges a
deletion an earlier round answered for -- is stated rather than left to be
discovered.
516/516 in scripts/tests.
* fix(autofix): let a skips-only file reach the bytes, and pin every field
A twelfth reverse audit proved the previous commit introduces no new
false accept -- structurally, and over forty thousand generated histories
-- and found one inconsistency and a coverage claim that was false.
`isEmptySurface` counted a disabled registration as content while
`sameSurface` cannot see one. A file that is NOTHING BUT disabled
registrations therefore took the comparison branch, where the comparator
is blind to the only thing in the file, so no rewrite of it could ever
read as main contributing. This repository has one such file. It reaches
the bytes now, like every other file the comparison cannot read, while
main moving disabled registrations on a file that has other content is
still not coverage arriving.
Four fixtures, four mutants killed, and each of the comparison's three
fields is load-bearing on its own for the first time: main planting a
guard (the live total falls, the declared total stands), main deleting an
assertion already behind one (the declared total falls, the live total
cannot), and main renaming a test (both totals stand, the multiset moves).
Those three survived every test in the file before.
The previous commit's message claimed dropping any of the three fields
fails a fixture. It did not; only the re-added disabled comparison was
pinned. The claim is not repeated here, and the comparison's own header
now says what it leaves out and where those files go instead.
The criss-cross presence assertion also gains the path check its sibling
already had: exiting 1 does not discriminate, because every guard mutant
still exits 1 -- charging main's own deletion to the round, which is the
hazard the guard exists for.
516/516 in scripts/tests.
* fix(autofix): close the escape hatches the review found in the weakening gate
- count-test-surface: see through type-only wrappers (`as`, `<T>x`,
`satisfies`, `!`) in constant folding, options objects and call
chains; fold comparison/equality/logical operators of two constants;
treat template-literal returns as non-thenable
- count-test-surface: an `if (true)` wrapper or a catch whose try holds
an assertion no longer shelters a body skip; a registration callback
handed by name resolves to its module-scope binding; a collector
factory binding (it.skipIf / it.each / test.extend) registers nothing
- af-155: charge a regression only when the run's window IS the live
re-arm key, and never stamp pre=green when the push carries a merge
past the head prepare classified
- tests: exercise all five own-lane names in the charge classifier, and
gate the verbatim staging witness on the host having sha256sum
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(autofix): arm-aware guard conditions, lane-list dedup, branch-side clamp (#10188)
Holes the round-27/28 review kept re-asserting on top of
|
||
|
|
38513b591b
|
chore(release): v0.23.3 (#11583)
* chore(release): v0.23.3 * docs(changelog): sync for v0.23.3 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
910e0020a3
|
test(review): pin the fold invariant the per-line escape rests on (#11533)
* test(review): pin the fold invariant the per-line escape rests on
`escapeTagOpeners` models CommonMark INLINE structure only. That is correct
exactly while every model-written channel folds its text to a line before it,
and the fold lives at five sites, not one — `ingestEntryList` for the entry
channels, the `\s+` normalisation for downgrade reasons, `collapseEntry` for
the `Not reviewed:` disclosures, `scriptLintGate`'s own push, and compose's
refusal of a line break in a ruling note's `by`. `scriptLintGate` is the proof
that a sixth channel can appear and be missed: it joins `bodyCriticals` after
`ingestEntryList` has run, and for a while it did not fold, so a fenced block
in a side file the review agent can rewrite reached the escape as several
lines and rendered a literal `&lt;` to a reviewer.
Nothing in the code enforced that. This adds the guard: one case drives every
channel with a multi-line entry — LF, CRLF and a lone CR — and asserts the
CONSEQUENCE, so a new channel that skips the fold reddens here instead of
mangling a review. It also pins the two channels that REFUSE rather than fold
(a quoted fence on either Critical list, and a ruling note's `by`), and the
one that renders as a code span, where an escape would show the reader a
literal `<` instead. Removing the fold from `scriptLintGate` or from the
disclosures reddens it.
Two boundaries that reverse audits keep rediscovering are recorded rather than
changed, both measured against cmark-gfm — the renderer GitHub runs — and an
HTML parser:
- A reference link's LABEL is ordinary inline text: a backtick inside
`[a`b][c]`, `[x][a`b]`, `[a`b]` or a link reference definition's own label
is a code-span delimiter like any other, and the tag it pairs over is code.
Two audits reasoned the other way and neither could build a counterexample;
the rows say so now.
- `rendersAsNothing` is fold-blind and position-blind on purpose. An arm that
deleted a `<details>`'s content would delete a quoted `` `<details>` `` too,
which is the shape a review of that file writes — it loses a real finding to
catch a readable one. And splitting the `<?` arm by position was tried and
REVERTED: on a prose-weighted corpus of 30000 bodies the split alone traded
121 -> 116 bodies wrongly called empty for 1426 -> 1730 wrongly called
visible, and teaching it the container prefixes then needs the container's
EXTENT as well — without that, 121 -> 3594. Being right there needs the
block structure the module deliberately does not model.
* test(review): give the fold guard teeth, and say only what it measures
Review found that the case added here, and the comments beside it, claimed
more than they delivered. Four reverse-audit rounds then found the same
class of defect four more times — each time in a sentence written as
MEASURED that had not been. Everything below is now checked by a mutation
that reddens it.
The helper had three assertions and two of them could not fail.
`body.split('\n')` does not split on a bare `\r`, and the filter keeps the
entry's first physical line, so the line count read 1 whatever happened to
the rest; and `&lt;` can only appear if a model wrote those characters
itself, since the escape emits `<` and nothing in the source emits
`&`. Both are replaced by the invariant the fold actually promises: the
entry's TAIL on the same line as its head, and no `\r` or `\n` left inside
it. That is what a surviving break costs the reader — the per-line escape
decides each half on its own and can post a tag the renderer holds live.
The case now drives eight rows, not five. It gained the downgrade reasons,
which need no fixture at all, and a Critical deferral's RELOCATION exit —
a second producer that joins `bodyCriticals` after `ingestEntryList` has
run, which a `Suggestion` row never reaches. Removing the fold at either
site reddens the case; before, both were silently uncovered.
The comments now name what the experiment shows:
- Three rows are held by more than one fold — the deferral list, the
cannot-tell leg, and the relocation exit, that last one three times over
— so they cannot redden on a single-site regression, and the other five
do. The earlier text said two and six, and credited the relocation exit
to two holders that can both be broken with the row still folded.
- The deferral LIST line never reaches `escapeTagOpeners`; it posts as
`- ${mdField(entry)}`. The relocation line does reach it and comes back
unchanged. Both are asserted inert as a code span, for different
reasons.
- `ingestEntryList`'s `\r\n?` normalisation folds nothing on its own;
`collapseEntry` is what carries the entry and disclosure legs.
- `rendersAsNothing` stays position-blind because `scanLines` discards
`token.map` and returns a block-quote-only depth — not because a parse
would cost too much. That path already runs one:
`canProjectFooterMarker` opens on a `<`, which every body these arms
judge carries by construction. The earlier text blamed cost and put the
figure at the parser's own inline-pass note, which is off by two orders
of magnitude.
The corpus numbers behind the reverted `<?` split keep their conclusion and
lose their false precision: the axis of the third figure is named, and both
they and the parse comparison are marked directional rather than
reproducible, because the absolute values swing more than twentyfold with
the body's shape.
No behaviour change: outside the tests, only comments move.
|
||
|
|
dfafbf240e
|
fix(ci): widen the replayed supersede-cede timeline margin to deflake Scripts (#11581)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
57e828a215
|
refactor(dingtalk): remove obsolete background response aggregation (#11570)
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> |
||
|
|
b3ebe1e84b
|
feat(channels)!: remove message prefix filtering (#11571)
BREAKING CHANGE: messagePrefix no longer filters or rewrites channel messages. Existing sender, group, mention and pairing policies still apply. Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> |
||
|
|
1961e9744a
|
fix(acp): pass shell execution config to tool invocations (#11102)
* fix(acp): pass the shell execution config through ACP tool dispatch
The ACP tool dispatch called `invocation.execute(signal, onToolProgress)`
with no third argument, so `ShellToolInvocation` fell back to
`shellExecutionConfig ?? {}`. The PTY was then sized 80x30 from
`shellExecutionService`'s own fallbacks — not even `Config`'s 80x24
default — and `pager`, `showColor` and `maxBufferedOutputBytes` were
dropped for every ACP tool call.
The TUI scheduler already passes `config.getShellExecutionConfig()`
(coreToolScheduler); do the same here.
This is how the process tree in #11303 was identified: the reporter's
orphans were all `conhost.exe --headless --width 80 --height 30`, which
is this fallback's signature and nothing else's. The leak itself is a
separate defect, fixed in #11313.
* fix(core): reap surviving hook process trees on Windows
`terminateSurvivingHookProcessGroup` was an explicit no-op on win32, so
when a parent-exit-surviving hook (MessageDisplay / StopFailure /
SessionDelete) was cancelled, nothing reaped the shell the supervisor had
started. Those hooks spawn a detached supervisor plus a shell child and
`unref()` it, so the parent's own tree kill on the supervisor is not a
reliable substitute: the supervisor may already have exited, which
reparents the shell out of its tree, or its taskkill may fail. The result
is the leftover `cmd.exe` processes reported in #11303.
Implement the branch with `taskkill /f /t`, extracted from
`terminateWindowsHookProcessTree` as a shared pid-only helper.
Guarded by a `process.kill(pid, 0)` liveness probe first. Windows has no
process group to signal, so an unguarded taskkill against a pid that has
already exited could land on a recycled pid belonging to an unrelated
application — the #6067 collateral-kill failure mode. The POSIX branch
gets that safety for free by signalling the negated pid.
* fix(vscode): shut the ACP CLI down gracefully instead of killing it
`AcpConnection.disconnect()` did a bare `this.child.kill()`. On Windows
that is `TerminateProcess`: the CLI's `process.on('exit')` handlers never
run, so every PTY, ConPTY host, sleep inhibitor and tracked child process
it was holding is orphaned. That is why the leaked processes in #11303
survived until the VS Code window itself was closed.
Close the child's stdin instead. Ending the ndjson stream is the CLI's
own shutdown path: `await connection.closed` returns, it fires SessionEnd
hooks, drains the MCP pool, disposes its sessions and exits normally, so
its exit cleanup actually runs.
Force-kill only if that does not land within 5s — `taskkill /f /t` on
Windows, since at that point the CLI is unresponsive and nothing else
will reap the shells underneath it, and SIGKILL elsewhere. The timer is
cleared as soon as the child exits.
A late write error on a pipe whose reader is gone arrives as an 'error'
event, and an unhandled one on an EventEmitter throws — in the extension
host, not here — so stdin gets a one-shot error listener before it is
ended.
* fix(vscode): bind the ACP exit handler to the child that owns it
`setupChildProcessHandlers` keyed its exit handler on `this.child`, not on
the child it was installed for. A superseded child exiting after
`connect()` installed its replacement therefore nulled out the *live*
connection and reported it as disconnected.
Latent before, because `disconnect()` force-killed the old child and it
was usually gone before the replacement was assigned. The graceful
shutdown in the previous commit widens that window to seconds, so bind
the handlers to their own child.
* fix(vscode): handle the ACP exit rejection before anything races it
`processExitPromise` is created in `setupChildProcessHandlers` but its
only consumer is the `Promise.race` in `initialize()`, which attaches
much later. A child that exits in between — a failed startup, or a
superseded child winding down after `disconnect()` — rejected it with no
handler attached: an unhandled rejection in the extension host.
Mark it handled at creation. The race still receives the original promise
and still sees the rejection, so nothing else changes.
Caught by CI on this branch: all 521 companion tests passed but vitest
reported the suite as failed on the unhandled error, which is the same
shape the extension host would have hit at runtime.
* test(vscode): cover the Windows escalation branch, which CI never reaches
`Test (windows-latest)` is skipped on PRs, so a test that branches on
`process.platform` only ever exercises its POSIX half in CI — and the
Windows half is the whole point here.
Pin the platform on both escalation tests: one asserts SIGKILL on POSIX,
the other asserts the `taskkill /f /t` tree kill on win32. `execFile` is
now mocked alongside `spawn` so the Windows branch can be asserted
without a real taskkill on the runner.
* fix(vscode): let the CLI finish its own shutdown before force-killing it
The 5s grace was shorter than the shutdown it was waiting for. On the
ide_close path the CLI budgets 8s for the MCP pool drain
(shutdownMcpPool(8_000), acpAgent.ts:2902) and 30s for the session drain
(SESSION_DRAIN_TIMEOUT_MS, acpAgent.ts:496), so a single slow MCP server
put the escalation in the middle of a wind-down that was progressing
correctly, skipping the process.on('exit') cleanup this teardown exists
to protect. Raise the grace to 40s to cover both stages, and pin the
boundary in the escalation tests: nothing may be signalled one tick
before the deadline, so a grace shorter than the CLI's own budget now
reds them.
No SIGTERM stage on POSIX: shutdownHandler is still attached during the
wind-down (process.off('SIGTERM', ...) sits in the finally at
acpAgent.ts:3119, after the awaits at 3112-3116), and `shuttingDown`
only blocks a second signal, so a SIGTERM would run a second concurrent
shutdown over the same pool and sessions.
Also split the escalation log by branch: only win32 performs a tree
kill, the POSIX branch signals the non-detached child alone, and the
shared message claimed a tree kill on both.
And move the two new constants above the class JSDoc, which they had
been inserted between, detaching the AcpConnection doc comment from the
class.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmts86p0gqr
* fix(core): fall back to a direct kill when the surviving hook's taskkill fails
terminateSurvivingHookProcessGroup discarded the boolean
taskkillProcessTree was extracted to return, so a taskkill that errored
or exceeded WINDOWS_TASKKILL_TIMEOUT_MS left the hook's cmd.exe tree
running with nothing else able to reap it and only a debugLogger.warn as
a trace. The sibling caller in terminateWindowsHookProcessTree already
honours it.
The fallback stays behind the isProcessAlive probe, so the #6067
recycled-pid guard is unchanged.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmts86p0gqr
* fix: close Windows process cleanup review gaps
* fix: reap CLI process group and gate superseded ACP callbacks
Address qwen-code-ci-bot review findings on #11102:
- Spawn the ACP child detached on POSIX so disconnect() can signal its
process group (reaping the PTYs, ConPTY hosts and MCP children the CLI
tracks) with a root-only fallback. Previously SIGKILL reached the CLI
root process alone and orphaned the tree.
- Gate the five SDK inbound callbacks on `this.child !== ownChild` so a
superseded connection stops dispatching into callbacks that read
`this.*` at call time.
- Re-probe liveness before the Windows pid-based SIGKILL fallback in
terminateSurvivingHookProcessGroup, so a pid taskkill already reported
dead is never signalled directly (collateral-kill risk).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtspbz90ri
* fix: guard retired ACP dispatch and pin teardown escalation tests
Two gaps remained after d67635b77:
- sendPrompt's onEndTurn tail ran unguarded after a stale prompt resolved
post-disconnect, clearing the replacement session's streaming state. Bail
out when the connection was retired (this.sdkConnection !== conn).
- The five SDK inbound callback guards compared this.child !== ownChild,
which is wrong across the shutdown grace window (this.child is nulled
before the grace timer and initialize() re-runs on the still-current
child). Compare against the wired connection instead.
Pin the positive halves the first fix left unpinned: the surviving hook pid
is still reaped after the supervisor exits, a live child exit clears the
connection and fires onDisconnected, and the force-kill escalation still
fires when stdin could not be ended.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtspbz90ri
* fix: cover runExitCleanup in the ACP shutdown grace
The ide_close path budgets 8s for the MCP pool drain and 30s for the
session drain, then runs runExitCleanup() (up to OVERALL_CLEANUP_TIMEOUT_MS
= 5s) in the finally wrapping runAcpAgent. The 40s grace fired 3s before
that 43s bounded wind-down finished, cutting the CLI off mid-cleanup and
dropping the recording flush and MCP disconnects. Bump to 45s to cover all
three bounded stages.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtspbz90ri
* fix(vscode-ide-companion): guard stale session writes after supersede
newSession and loadSession wrote this.sessionId after awaiting the
connection they captured, with no supersede guard. disconnect() now
leaves the retired CLI alive for up to SHUTDOWN_GRACE_MS, so a late
session/new or session/load response from that retired CLI can land
after a replacement connection is installed and stamp the dead
session's id back onto the field disconnect() just nulled.
Gate both post-await writes on `this.sdkConnection === conn` (the same
guard sendPrompt already carries), and correct the escalation comment
and its test comment to name exactly what the POSIX group kill does and
does not reach. Add tests pinning the detached-spawn premise and the
stale-session guard.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtsxwmdgrw
* fix(vscode-ide-companion): pin re-connect supersede gate and align spawn comment
The spawn-option comment claimed the POSIX group signal reaps PTYs,
ConPTY hosts and MCP children. The escalation comment (corrected by
faab984f29) says the group kill only reaches the CLI root and its
non-detached MCP stdio children — it does NOT reach descendants that
call setsid() (detached hook supervisors/monitors, node-pty sessions).
Align the spawn comment with that.
Every supersede test drove the gate through disconnect(), which nulls
this.child and this.sdkConnection together, so the captured-identity
predicate and a weaker !this.child always agree. Add a re-connect test
that installs a replacement child while the old connection is still
winding down, pinning the stronger predicate.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmttcwqrask
* fix(vscode-ide-companion): pin re-connect supersede gates for outbound session writes
The outbound guards (newSession/loadSession/sendPrompt) key on
`this.sdkConnection !== conn` (or `=== conn`) to detect a superseded
connection. Every test drove the gate through disconnect(), which nulls
this.child and this.sdkConnection together, so a weakened `!this.child`
still bails and the predicate is never discriminated. Add re-connect cases
that install a replacement child while the retired connection's promises
are still in flight: a guard weakened to `!this.child` would then stamp
the retired session id (newSession/loadSession) or fire onEndTurn
(sendPrompt), turning the test red.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmttf1wj0so
* fix(vscode-ide-companion): fail superseded newSession/loadSession instead of returning the retired CLI's payload
The supersede guards added at newSession and loadSession correctly skipped the
stale write to this.sessionId, but still returned the retired CLI's response.
QwenAgentManager applies that payload unconditionally: applySessionStateFromResult
(qwenAgentManager.ts:1283) and restoreBaselineSessionStateAfterLoad (:1066) write
the dead CLI's model and mode state into the live webview's baselines, and
createNewSession hands the same promise to every concurrent caller through
sessionCreateInFlight (:1226-1234), so they all adopt it.
Both now throw RequestError.internalError({ details: 'connection superseded' }),
matching the convention the inbound callback guards in this same file already use.
Callers already have error paths: createNewSession rethrows non-auth errors
(isAuthenticationRequiredError matches AUTH_REQUIRED -32000, not INTERNAL_ERROR
-32603) and its finally clears sessionCreateInFlight, so concurrent callers get
the rejection instead of stale state; loadSessionViaAcp rethrows as well.
loadSession's gate moves outside its catch, so a supersede is no longer logged as
a request failure, and ahead of the success log, so a discarded load prints no
unqualified success line.
sendPrompt's guard is deliberately left returning: its only caller
(qwenAgentManager.sendMessage:397) is Promise<void> and discards the response, so
no payload is certified to anyone, and the guard's real job -- skipping onEndTurn
so the replacement session's streaming state survives -- is already correct.
Tests: both supersede cases now assert the rejection (code + data.details) rather
than awaiting a resolution. Mutation-verified: reverting either guard to
`return response` reds exactly those two cases ("promise resolved
{ sessionId: 'stale-from-retired-cli' } instead of rejecting"), 31 passed.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtu0hicwtk
* fix(vscode-ide-companion): close the superseded session on session replace
The agent keeps every session alive until told otherwise, and a retained
session still fires autonomous model turns when its background tasks
complete — each turn can spawn shells, which is what grew conhost.exe
without bound in #11303 while the window stayed open. session/new and
session/load now close the superseded session once the replacement is
confirmed. Fire-and-forget: a refused or unsupported close (older CLI)
must not block the user's new session, and a later session/load of the
closed id simply re-reads the flushed transcript.
* fix(acp): narrow shell execution follow-up
Remove the risky hook-process and superseded-session cleanup changes, leaving only the ACP shell execution configuration passthrough.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
||
|
|
ac1edef974
|
fix(review): keep host-trusted state out of the container's writable surface (#9983)
* fix(review): keep host-trusted state out of the container's writable surface
Two instances of one root cause, both reachable on `main` today and neither
opened by the sandbox — before it, the reviewed repository's commands ran as
the host user with no filesystem restriction at all, so containment narrowed
this reach rather than creating it. What it did not do is close it, and a
`required` that claims containment should not leave host-trusted state in the
directory it hands the container read-write.
**Leases.** `cleanupReviewWorktreeLeases` matches a lease by session ids alone
and then force-removes whatever worktree and deletes whatever branch it names.
Those files lived in the review temp dir — the bind mount. Reviewed code that
edits one makes another session's cleanup destroy the wrong tree; one it
plants with a foreign session id is swept by nobody and wedges that PR on that
machine until someone deletes the file by hand. They now live one directory
over, in `.qwen/review-leases`, which nothing mounts. A lease left behind by
an older build is deleted when the same target is leased again.
**Gitfiles.** These cannot move — git requires `<tree>/.git` — so the check
moves instead. The existing identity gates cannot see the attack and never
could: the writer chooses both the gitfile's target and that target's
backpointer, so the round-trip agrees; `--show-toplevel` still names the tree;
no symlink is involved; and asking git for the common dir resolves THROUGH the
rewritten gitfile, so that answer comes from the same hand. Location is the
question that has an honest answer — a real admin entry lives under
`<repo>/.git/worktrees/`, outside the mount, and a planted one has to be
inside it. `restoreProbeTreeTracked` now refuses an entry that resolves inside
the review temp dir.
Reproduced end to end before fixing, and the reproduction is the reason the
gate is where it is: a rewritten gitfile pointing at a planted admin entry
whose own `commondir` names a planted repository, both under the mount,
passes all three existing gates — and `git checkout --force` then runs that
repository's `filter.evil.smudge` ON THE HOST. With the new gate the same
shape is refused while the entry it replaced still passes.
Three mutants, three reds: a gate that always admits, one that admits when the
entry cannot be resolved, and one that refuses when there is no mount at all —
the last would refuse every ordinary repository.
* fix(review): close the shapes the first cut of this gate left open
Six blockers from round 1, all verified before fixing.
**The predicate failed open at both boundaries of its own question.** An entry
whose realpath IS the mount root produced an empty relative path and was read
as outside — while the root is exactly as writable as anything under it — and
a child legitimately named `..evil-git` produced a relative path starting with
the characters an escape would. Both are places a planted entry can sit. The
hand-rolled arithmetic is gone; `isSubpath` already answers this correctly and
is already tested.
**Every gate sat behind `.git` being a file.** Reviewed code that replaces the
gitfile with a `.git` DIRECTORY of its own — `rm .git && git init .` inside
the mount — skipped the lot, and the restore then ran against the repository
it had just made. That shape is now refused before the branch, and only under
a mount: outside one, a `.git` directory is what every ordinary checkout has.
**The gate was on the second host-side write, not the first.** The probe
tree's own creation — `git worktree add` — resolves the repository through the
REVIEW worktree's gitfile, a second rewritable pointer in the same mount,
written after the build phase has already run the PR's code in the container.
`worktree add` checks files out, so it executes whatever filter that pointer
leads to, before anything inside the restore could fire. Both call sites now
ask the same question through one helper rather than two inline copies.
**The legacy-lease removal was a wedge.** `force` only swallows ENOENT, so a
DIRECTORY at the old lease path threw EISDIR out of acquisition — and
`mkdir .qwen/tmp/qwen-review-lease-pr-42.json` is one command inside the
mount, after which every review of that PR on that machine failed, with
nothing able to remove it: the rollback rethrows, the sweep skips the lease
shape, and `rm -f` cannot remove a directory.
**The move stranded the only automated orphan sweeper.** The review job's
`if: always()` heal step swept the old path only, so a cancelled or timed-out
run left a dead lease at the new one, on a runner whose workspace persists.
It now sweeps both, recursively.
Tests follow the same rule they were missing: the gate's production call sites
are driven, not just its predicate. Every previous fixture put its tree under a
bare `tmpdir()`, where `mountRootFor` answers null and the gate short-circuits
before its logic runs — which is how deleting the wiring shipped green. The new
fixtures sit under `.qwen/tmp` and plant a COHERENT admin entry, because an
incoherent one fails earlier for a reason that has nothing to do with the gate.
Seven mutants, seven reds — including passing the probe tree where the review
worktree belongs.
* fix(review): gate the third write, and stop breaking the lane that pins the sweep
**The workflow edit broke its own pinning test, and this PR's required check
with it.** `scripts/tests/review-worktree-cleanup-workflow.test.js` asserts the
sweep line literally, derived from `paths.ts` so a rename there fails the build
rather than silently no-op-ing the sweep on the runners. Adding `-r` broke the
substring. The test now pins BOTH locations — the new one where this job's own
runs leave leases, the old one where a persisted workspace can still hold one —
which is what the test existed to enforce and what the move should have updated
in the same breath. It went unnoticed because the commit was verified with
vitest alone; `npm run test:scripts` is a separate lane of the same required
job.
**The revert phase was a third ungated write, reached exactly when the others
refuse.** `git checkout base -- …` runs against the same mounted probe tree,
and its only guard was a symlink-only lstat walk that a rewritten gitfile
passes untouched. Worse, a restore refusal becomes `inconclusive` without
throwing and the mutation phase's catch continues on purpose "so the revert
probe below still runs" — so guarding the first two writes and not this one
left the route open precisely where the other two closed it.
**A claim in the gate's own rationale was false.** The comment called
`worktree add` "the FIRST host-side git write of this phase"; `discardWorktree`
above already runs `worktree remove --force` and `worktree unlock` with the
same cwd. They materialise nothing, so no filter runs — but that distinction
is the entire reason the gate can sit below them, and the looser sentence
invites the next maintainer to add a checkout above it.
Tests follow the same rule as the code. The ADMIT path had no coverage, so a
mutation mangling the parsed gitdir survived: every refusal fixture would still
refuse, because a mangled target fails to resolve and the location check fails
closed — while in production the same mangling refuses every healthy tree. It
is asserted now, relative spelling included, since git writes that pointer
relative when it can. And the revert gate is driven through the phase itself,
by a fake runner that rewrites `.git` from inside the probe tree — which is who
does it in production.
Nine mutants, nine reds.
* test(review): gate the new cases off the platform where containment cannot exist
The three tests added for this gate are deterministically red on Windows, and
the reason is this PR's own sibling: `mountRootFor` refuses every absolute
Windows path, because a drive letter is a colon and the `-v` grammar has one
separator. Containment is unavailable there by design, so the gates never
speak, `out.json` never carries the refusal, and the assertions fail for a
reason that has nothing to do with what they test. The Windows lane runs only
under `merge_group` and reports as skipped on the PR page, so green here was
no evidence — the first red would have landed in the queue and stalled the
batch.
Gated with the same named predicate the sibling suite uses, so the reason is
stated once rather than repeated three times.
Two more from the same review. The fake runner embedded the planted path in a
JS string literal by interpolation, which a Windows path's backslashes
self-mangle; it is generated through `JSON.stringify` now. And both new tests
had pasted the ~22-line fixture that `scaffoldModifiedPr` already provides,
leaving three copies to evolve in lock-step — the helper takes the worktree
location as a parameter instead, since the location is exactly what differs
and exactly why the gates fire.
The three gates were re-mutated after the refactor, one at a time: three
mutants, three reds.
* test(review): assert the property, not the sentence that explains it
Both gate tests asserted a refusal MESSAGE, and `worktreeCreateFailureDetail`
serializes that message identically whether the gate throws before the write
or after it. So the mutant that matters — moving `worktree add` above the
gate, which is exactly what the comment beside it warns a refactor might do —
checked files out through the planted pointer, ran its filters on the host,
and left both tests green.
Creation: the probe tree not existing is what says the spawn never ran.
Revert: the planted repository now carries a real `filter.evil.smudge`, and
the assertion is that its canary was never written. That is the property
itself rather than a proxy for it — with the gate removed the filter EXECUTES
on the host inside the suite, which is what the test now catches.
The fixture had to become coherent for this to mean anything: an empty planted
directory fails the checkout for reasons unrelated to the gate, so it copies
the real admin entry and points `commondir` at a planted common dir, the shape
the reproduction in the PR description uses.
* fix(review): ask git where the repository is, and gate the entrances that were left
**Stop re-implementing git's resolver.** The gate parsed `gitdir:` and resolved
it in Node while the write that followed resolved the same file in git, and the
two disagree in ways that are individually small and collectively unbounded:
JS `trim()` strips U+00A0 where git's `read_gitfile` trims only C-locale space,
so `gitdir: <NBSP><real entry>` resolved outside the mount here — admitted —
and to a planted entry inside it there; and `resolve()` is lexical where a
spawned git resolves relative targets against the tree's physical path after
chdir. Every divergence admits a pointer whose real referent only git sees, and
enumerating them has no last corner.
So the resolver that decides is now the resolver that acts: `rev-parse
--absolute-git-dir`, run in the tree, with hooks and fsmonitor inert. What
stays ours is the only judgment that was ever ours — WHERE the answer lives.
The fixtures had to become real repositories for this to mean anything, which
is an improvement on its own.
**Three more entrances, all probed to host execution.** Round 2 recorded that
per-call-site gating does not close the class, and I argued a shared layer
would be noise. That was wrong, and the review paid for the answer: base-tree's
rebuild had no defence at all; scratch-tree's screen matches
`filter.*.smudge|clean` and not `filter.*.process`, which also executes; and
`--resume` runs an index-refreshing `git status` through the same pointer, so a
planted `core.fsmonitor` fires inside the very command that collects the
ruling's evidence — the attack does not need the resume to succeed. All three
now ask the same question, and `status` carries an inert fsmonitor besides.
**Absence is the caller's question, not this one's.** The first cut refused a
tree that does not exist, which is the state `--resume` legitimately asks
about — 26 tests, measured. It returns null there now.
* fix(review): the entrances round 6 found, and two limits stated rather than papered over
Case, first: the verdict is a string comparison, and Node's JS `realpathSync`
hands back the spelling it was asked for whenever the lookup succeeded — so on
a case-insensitive filesystem (default macOS APFS, a host this pipeline
supports) a planted entry spelled with different case resolves INSIDE the
mount and compares OUTSIDE it. `realpathSync.native` asks the operating system
for the stored name, so both sides arrive spelled as the filesystem holds them.
Two more entrances. scratch-tree's REUSE path reaches a `checkout --force` of
its own through the scratch tree's pointer, and fetch-pr's FRESH path checks
out through the review worktree's. The reuse one is not a refusal, though: an
unusable leftover is exactly what the rebuild exists for, and refusing there
turned "rebuild over it" into "fail the command" — three tests said so.
And `--resume` refused to CRASH where this file's own contract, the option's
describe and the docs all promise refuse-to-FRESH. Throwing propagated out of
`runFetchPr`, so `cleanStale` — the thing that removes the planted tree — was
never reached. It returns a refusal reason now and the fresh path sweeps.
The two new refusal tests were red on Windows, for the third time in this PR
and for the same reason: `mountRootFor` refuses every absolute Windows path,
so containment is unavailable there by design and the gate never speaks. Both
gated off win32 like their siblings.
Two limits I could not close, stated where they are rather than left looking
proven. The `--resume` gate's wiring is pinned by no test: `fetch-pr.test.ts`
mocks `node:fs` without `existsSync`, so the tree never exists there and the
gate is inert — deleting it ships green. And the reuse-path gate is defence in
depth rather than a demonstrated check: with both trees pointed at one planted
common dir and a `filter.*.process` the existing screen misses, removing the
gate still executes nothing, because the reset declines that tree for its own
reasons first.
* fix(review): stop editing git's answer, and gate the pointer that is actually used
Two blockers, both inside the previous round's own fixes.
`.trim()` on `rev-parse`'s stdout re-opened the exact divergence the paragraph
above it closes. It strips U+00A0, U+FEFF and the U+2000 block, so an admin
entry whose directory NAME ends in one is resolved by git with the character
and judged here without it — and a twin of that name minus the character,
symlinked outside the mount, is where the judgment then lands. Only the
terminator `rev-parse` adds may be removed.
And the fresh-path gate checked the wrong pointer. `git()` sets no cwd, so
`worktree add` resolves the repository from `process.cwd()` — while the gate
asked about `wt`, which `cleanStale` has just removed, and a tree that does
not exist has no pointer to distrust. It was a no-op on every reachable path.
It asks about the launch directory now, which is the one the nested geometry
`mountRootFor` documents lets an outer review's containerized phase rewrite.
* fix(review): let the launch directory resolve the way git does
Last round's gate asked `untrustedGitfile` about `process.cwd()`, and that
function demands `<tree>/.git` — right for the trees this pipeline builds,
wrong for the directory a command was launched from. Any subdirectory of a
checkout has no `.git` of its own; git walks up. So in the nested geometry the
pipeline runs its own reviews in — cwd at `<review tree>/packages/cli` — the
gate answered "has no .git to resolve" and fetch-pr aborted before the
`worktree add` that would have succeeded. 77 tests in one suite, at the
reviewer's HEAD.
My checkout is not under `.qwen/tmp`, so `mountRootFor` returned null here and
the gate never fired: the suite stayed green while the dogfood lane broke.
That blind spot is now a test — a subdirectory of a mounted checkout, asserted
to resolve.
The launch directory gets its own question: ask git, from there, what
repository `worktree add` will find, and judge only where that answer lives.
No shape check, because the shape belongs to a tree and this is not one.
* fix(review): three failures of my own, and the block gate that stops the fourth
`realpathSync.native` is not always there. A suite that mocks
`node:fs.realpathSync` as a bare `vi.fn` gives it no `.native`, and reaching
through it threw a TypeError straight into the fail-closed catch — refusing
every worktree creation in any checkout that happens to sit under `.qwen/tmp`,
which is the geometry this pipeline reviews itself in. Falls back to the plain
call, and a test asks that shape directly, because the suite that carries the
mock never reaches this gate from an unmounted checkout.
The real-git fixtures ran without `isolateHostGitConfig`, which every other
real-git block in the same file installs. On a host with `commit.gpgsign=true`
and no usable key the fixture commit fails and the whole block goes red for
reasons unrelated to the gate — reproduced with a poisoned HOME.
And one more case was left ungated for the Windows lane. Rather than gate it
and wait for the fifth, the whole block is gated: every case in it builds a
layout under `.qwen/tmp`, and on Windows neither the question nor the fixture
exists.
Worth recording why these three reached the branch at all: my checkout is not
under `.qwen/tmp`, so `mountRootFor` answers null here and every one of these
gates is inert on my machine. Green locally proves nothing about the geometry
they are for.
* fix(review): gate the reads and the launch directories, not only the writes
The location gate this PR added was asked at the writes and nowhere else,
which is how the same class kept resurfacing: a class closed one call site
at a time re-opens at the next call site somebody adds. Four entrances stood
open behind it, and two more of the same shape were a census away.
`worktreeResidue` measured BEFORE the gates its two call sites carry, and a
measurement is not a safe read: `status` refreshes the index, an index
refresh runs the repository's clean filter, and every identity check the
probe carries is honest about WHICH repository answers and blind to where
that repository lives. A coherent plant writes both halves of the round
trip, HEAD comes from the copied admin entry so the sha pin matches, and no
symlink is involved. The gate now runs first, and a refusal is
unmeasured-with-reason — the channel every caller already reads as "not
clean".
`fetch-pr` asked at step 4, after `cleanStale`'s force-remove and after the
PR fetch, both of which discover their repository from `process.cwd()`. In
the nested geometry `mountRootFor` documents, that launch directory is
inside the outer review's read-write mount, and a fetch through a rewritten
pointer loads the planted repository's transport config. The gate moves to
the top of the command, before the lease is even read; the step-4 ask stays
as the narrower window around the checkout `worktree add` performs.
`captureLocalDiff` gets the same treatment for the local lane, before its
first `rev-parse`.
`comment-status` and `repo-context` read through the pointer without asking
at all. No execution witness is claimed for `log`/`ls-tree`/`show` — what is
at stake there is the answer: `changedSinceComment: false` sourced from a
planted repository is this pipeline certifying that the code behind a
blocker thread did not move, and `repo-context` exists precisely to take
identity files from the merge base rather than from the PR head.
And the containment predicate judged against the innermost `.qwen/tmp` only.
The MOUNT has to stay innermost — the first occurrence would pull
`<repo>/.git` and every sibling checkout into the container — but "which
directory may a container have been given read-write" and "where may
reviewed code already have written" are different questions, and in the
nested geometry they have different answers. The judgement now takes the
outermost layer. It costs the honest layouts nothing: `git worktree add` run
from inside a linked worktree still writes its admin entry under the main
repository's `.git/worktrees/`, outside every layer.
`mountRootFor` moves from `sandboxed-exec` into `worktree` (re-exported from
its old home) so the gates can default to it instead of taking it as an
argument every new call site has to remember to pass.
Tests: `host-execution.canary.test.ts` asks the property once for every
route, with a live plant as its oracle — a rewritten gitfile naming an admin
entry whose commondir names a planted repository carrying a filter git
executes, plus a canary the filter writes on the host. Its first case
asserts the canary IS written when the same command runs ungated, so a
fixture that stops being an attack fails there rather than certifying the
gates that walked around it. Six mutations, six distinct reds. The committed
fixtures that wrote a `gitdir: `-prefixed backpointer now write the bare
path git actually writes; the prefixed form failed the round-trip check
first and masked every route behind it.
* fix(review): gate the base tree's reuse fast path too
The census that found `repo-context` and `captureLocalDiff` found one more,
and this one returns before the gate rather than running beside it: the
base-tree reuse branch answers "already built at <sha>" on two facts that
both live inside the mount — a marker file in the base tree, and
`rev-parse HEAD` resolved through that tree's own `.git`. A planted
repository answers the right sha for a working tree holding anything at
all, and the A/B's BASE side is then the reviewed code's own; every "the
base behaves differently" verdict a shard reports would be its author's.
Not a refusal: an unusable leftover is what the rebuild exists for.
Falling through discards the tree — removing the plant with it — and
creates a fresh one through the review worktree's pointer, which the gate
before `worktree add` already checks. Same shape as `scratch-tree`'s reuse
path, for the same reason.
The canary suite gains the arm: with the plant in place the report must not
say "reusing it". Removing the gate reds it, and the mutant's own note names
the planted tree as an already-built base.
* fix(review): ask the launch-directory question where all of it goes through
Hoisting the gate inside `fetch-pr` fixed one command. The census that
followed found the same shape in `load-rules` (this review's own rules, read
with `show <base>:<path>`), `submit` (where a submission goes),
`match-remote` (which remote gets fetched), `run`, `revert-hunk` and
`cleanup` — none of which knows it is asking this question, and none of
which should have to.
They all share one thing: every wrapper in `lib/git` runs git with no `cwd`,
so each discovers its repository from `process.cwd()`. That is the choke
point, so the question moves there — four functions in one file instead of a
gate per command, which is the pattern that kept re-opening this class.
The throwing wrappers throw. `gitProbe` answers its own documented "could
not be run at all" value instead, because that is a probe's contract and
every `gitOpt` caller already handles it; the command does not run either
way, which is the part that matters. `releaseWorktree` is the case that
shows why this is the right split: through a poisoned launch directory its
`worktree remove` is refused, and the `rmSync` behind it still clears the
path — the remedy keeps working without git acting through the plant.
Memoized per directory; outside a review temp dir it costs no syscall at all
(`mountRootFor` returns null on a string scan). The cached admit is the same
TOCTOU residual the per-tree gates already document and do not close.
`captureLocalDiff`'s own gate goes: it now contributes nothing but a nicer
message, and a duplicate that has to be kept in step with the real one is a
liability. `fetch-pr` keeps its own, because that command changes state that
is not a git call — the lease read and write — before it makes one, and the
ordering test pins exactly that.
Seven mutations, seven distinct red sets, baseline green.
* refactor(review): let the gates default to the mount root everywhere
Every production call site now asks `untrustedGitfile(tree)` and
`untrustedRepositoryFrom(cwd)` rather than threading `mountRootFor` through
by hand. That is the point of moving it into this module: the argument stays
injectable for the fixtures that drive the judgement with their own root, but
a new call site cannot get it wrong by forgetting to pass one — and two
spellings of the same check in one file is the kind of thing a reader has to
stop and reconcile.
The test-only reset seam on the launch-directory verdict goes with it: the
verdict is keyed on the directory, and every fixture stands its plant up in a
fresh temp dir, so a cached answer can never be served for a different one.
* fix(review): gate revert-hunk's tree reads, and reflow a comment
The last tree-scoped command that asked nothing. `git apply` writes into the
tree without running a filter, so no execution is claimed here — what the
gate refuses is deciding a probe's outcome on answers that come out of the
plant. `--show-toplevel` still prints this tree when the gitfile has been
rewritten (that is the shape the location gate exists for), and `check-attr`
and `ls-files` then report the planted repository's EOL regime and index: the
two facts this function refuses a conversion on. A scratch tree is inside the
directory the sandbox hands the reviewed code read-write, so that writer is
the code under review.
A harness fact like every other refusal there — nothing is claimed about the
hunk, and the tree is untouched.
That leaves one deliberate exception, stated rather than left to be found:
`cleanup` still runs `worktree prune` and `branch -D` from the launch
directory ungated, because it is the remedy — refusing there would leave a
planted tree with nothing able to sweep it, and its `rmSync` clears the path
whether or not git can act through the pointer.
* fix(review): let a refusal say it refused, and re-ask where git resolves
Six review Criticals with one theme: a gate that answers correctly, then
reports through a channel its callers read as an answer.
`gitProbe` returned the launch-dir refusal as `{out: null, status: null}`
and every consumer used the status-blind `gitOpt`, so `releaseWorktree`
deleted the directory with `rmSync`, reported `freed: true`, and left
behind the registration and branch that wedge the next `worktree add`;
`load-rules` wrote an empty rules file into every agent brief while
printing "No review rules found". `status: null` already documented "the
command could not be run at all" — git answers a path absent at a ref
with 128 — so the fix keys on that and carries the refusal text beside
it, rather than adding a third state. `releaseWorktree` now reports the
release it could not make, and `load-rules` separates a source it could
not read from one that is absent.
The launch-dir verdict was memoized for the whole process, so a pointer
rewritten after the run's first git call was served that first clean
answer forever — and in the nested geometry the outer review's
containerized build holds the directory read-write for the length of a
command, which is a rewrite between calls, not between a check and its
use. Only the negative is cached now, and only outside a review temp dir
where it costs no syscall.
fetch-pr's step-4 refusal was thrown inside the try whose catch rolled
the fetched ref back with an ungated `branch -D`, so the gate's own
answer executed the plant's reference-transaction hooks on the host and
the run died as `Failed to create worktree at …`, indistinguishable from
an infrastructure failure. The ask is hoisted out, and the five remaining
spawns in fetch-pr and cleanup that resolved their repository from
`process.cwd()` go through lib/git's gated wrappers. Cleanup still
sweeps: the `rmSync` that clears the path is not a git call, and a
measured ungated prune was clearing the PLANT's registrations, not the
real one.
A lease moved out of the mounted directory with no fallback read, so an
older build's live lock was invisible to the gate and the next
acquisition deleted it before sweeping its worktree mid-run — #9205, the
incident the lease exists to prevent. The gate reads both locations for
one release, and acquisition refuses a foreign legacy lease instead of
removing it.
base-tree's reuse fast path policied the tree's pointer but not its
contents, and `rev-parse HEAD` does not move when working files change,
so the reviewed build could overwrite the base checkout and the A/B
compared the PR against a copy of itself — a regression it breaks then
reads as pre-existing on both sides. Tracked dirt falls through to the
rebuild; untracked build output still reuses, or the concurrent-shard
clobber the fast path prevents comes back. Measured: `npm run build`
here modifies no tracked file.
revert-hunk decided where `--tree`'s repository resolves but never asked
whether `--tree` IS that tree, so a symlink at the scratch path aimed
`git apply -R` at another legitimate tree while the report certified the
scratch path. The leaf is refused, and re-read immediately before the
write. The bounded ancestor walk `resetScratchTree` also does is not
added here: this command has no common dir to bound at, and an unbounded
walk refuses every tree under a linked `/tmp`.
Also one definition of the attack fixture instead of ten copies across
five suites, which repairs the one copy whose admin-entry backpointer
carried a `gitdir: ` prefix git never writes there.
* fix(review): ask a pointer where it resolves to, and stamp the run that built a tree
Four review Criticals with one theme: a gate that asked half the question,
and a marker that recorded what it vouched for but not who vouched.
A gitfile rewritten to `gitdir: <repo>/.git` names the repository's OWN
common dir, which resolves OUTSIDE the mount, so every gate that asked only
where the answer lives admitted it and then acted on the main repository
through it — measured, `status` refreshed the main index and `rev-parse
HEAD` answered the main head, so the reads a review treats as fact came
from a tree nobody verified. Both questions are now asked in one place, so
the tree gate, the launch-directory gate and the residue probe close
together instead of one entrance at a time, and the two `rev-parse` calls
share one sanitized spawn instead of duplicating its boilerplate.
scratch-tree's reuse route asked about the scratch pointer only, never
about the review worktree's — the one `headSha` and every comparison inside
the reset resolve through — so an unpinned run answered `available: true,
reused: true` with the main head and reset the verifier's tree to the
user's own commit. Suspicion there falls through to the rebuild, whose gate
refuses, rather than refusing on the spot.
`mountRootFor` matched no temp dir for a process standing AT `.qwen/tmp`,
because the marker ends in a separator: the launch-directory question went
unpoliced there and `lib/git` memoized that negative as trusted.
The base-tree reuse marker recorded a sha but not a run, and `cleanStale`
never releases `-base`, so an earlier round's tree was certified as this
run's A/B base after a whole containerized phase could drop untracked
executables into it — `dist/cli.js` is what a host-side A/B runs, and
`--untracked-files=no` cannot see it. Refusing any untracked file at all
would disable every legitimate reuse and bring back the concurrent-shard
clobber the fast path exists to prevent, so the marker carries the run
epoch the deadline stamps and the session ledger already key on: same-run
reuse survives, an earlier run's tree rebuilds.
And the lease move left `isReviewPlumbing` naming its directories by hand,
so a checkout holding a lease captured that churned lease JSON as the
user's untracked change and an interleaved local round could never again
report "no changes". The pattern is built from the `paths.ts` constants
now, which is what keeps the next move from re-opening it.
Two existing witnesses were repaired rather than left green for the wrong
reason: the canary's base-tree fixture and the different-sha marker case
both hand-wrote a one-line marker, which the epoch fence now turns away
before the gate each exists to exercise. Both stamp the epoch, and a probe
removing the reuse pointer gate reds the canary again.
* fix(review): fail closed where a gate could not be run, and stop editing git's answer
Seven review Criticals, all in code this pull request added: four ways a gate
reported a success it had not earned, one that threw where its contract says it
degrades, one lane-breaking test, and one regex that edited the very answer it
exists to pass through unedited.
`rev-parse` answered null for three different things — git ran and said this is
no repository, git timed out, and git died on some other fatal — and two of the
three gates read all three as "no objection". The launch-directory gate budgets
30s against the protected commands' 120s, and the residue probe's spawns carry
no budget at all, so a config sized to parse between the two left the gate
silent while the command it gates resolved through the pointer nobody had
judged: measured, `status` refreshed the planted index and ran its clean filter
on the host. The null is classified once, at the spawn that produces it, and
both gates refuse the class they cannot judge. A genuine not-a-repository still
passes through — that is the caller's own error path to own, and refusing it
would answer a question nobody asked. Pinning `LC_ALL` is what makes that
classification depend on git's behaviour rather than on the runner's locale.
The same strip that removed git's record terminator also removed a `\r` that
was the last byte of the path itself, which is precisely what the docstring
above it forbids: a plant named `evil\r` was judged at its twin `evil`,
symlinked outside the mount, and admitted while every gated command resolved
through the plant.
`gitProbe`'s launch-dir pre-check read `process.cwd()` outside its try, so a
review whose worktree was swept out from under it threw ENOENT through
`releaseWorktree`'s documented never-throws contract and aborted the sweep
before the branch delete and the lease release — leaving the stale lease that
refuses every later cleanup of that target.
`cleanup` collapsed a refusal into each of its two benign answers: `refExists`
said "no such branch", so the branch leg was skipped silently and the lease
released over a surviving branch, and `pruneWorktrees` said "nothing to prune",
so the symlink arm announced `Removed … link` over a registration that stayed.
Both report the refusal and hold the lease now. A genuine prune failure stays
swallowed, because it must not mask the error that got us here.
And the rewritten `toContain('common dir')` assertion depended on the
mount-relative gate answering first, which it never does on Windows — the same
lane this pull request has broken three times before. Split by the file's own
convention rather than branching one expectation on `process.platform`, so each
lane asserts a single unconditional sentence.
Every guard above has a witness that reds without it, including the admit arms:
a genuine not-a-repository keeps passing, and the residue probe's `status` is
shown never to run at all.
* fix(review): fail cleanup's branch probe on any non-answer, survive a deleted cwd
With --verify --quiet only exit 1 is genuine absence: a null status
(spawn failure, timeout kill) or a 128 fatal used to read as "no such
branch", so the delete was skipped, the lease released, and "Nothing
to clean" printed over a surviving branch (R19-1). Name the non-answer
and hold the lease instead.
Also capture the launch directory at runCleanup's entry and hand it to
the ancestor walks as an explicit stopAt: their default reads
process.cwd() outside any try, so a deleted cwd threw uv_cwd out of the
best-effort sweep before any degradation could run (the cleanup half of
R19-4). The holder-skip note now names the path the lease was found at,
which during the rollout can be the pre-move path (R20-1).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): walk --tree's ancestors for a planted symlink, not only its leaf
The redirect gate checked the leaf of --tree alone, at entry and again
before the mutating git apply. A symlink swapped in at an ancestor
between the review temp root and the leaf — after the create/sweep
checks ran — was invisible to every check in the chain, and git apply -R
then reverse-applied into whichever tree the link names while the report
certified the scratch path (R24-1). Walk the ancestors bounded at the
outermost review temp root, at both checks; a --tree outside any review
temp root keeps the leaf check so a linked /var refuses nothing.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): mirror new leases at the legacy path, bound the legacy read
The dual-location lease read was one-directional: a pre-move build stays
blind to leases written at the new path, so for the rollout window an old
fetch-pr passes its gate over a live lease and force-removes the new
session's worktree mid-run (R16-1). Mirror the lease at the legacy path
after the new-path wx write wins; an honored foreign lease surfacing
before the mirror backs the acquisition out instead of clobbering it.
The legacy read also re-granted gate authority to the one directory
reviewed code can write: a planted legacy lease naming a foreign session
was a permanent denial of service (R24-2). A legacy lease is now honored
only while its mtime predates LEGACY_LEASE_CUTOFF_MS (the release date of
the first build carrying the move — MUST be set before shipping).
Release and diagnostics see both locations (R20-1): clear removes the
legacy path too (recursive, so a directory at the lease name cannot
EISDIR-wedge), and the held-lease messages name the path the lease was
actually found at via the new readReviewWorktreeLeaseAt. The finalizer
sweeps the legacy directory only for leases content-identical to the
new-path twin — the mirror is readable from inside the mount, so copied
credentials prove nothing, and a plant must never drive a host-side
removal. readLease lstats before opening, so a planted FIFO hangs no
gate read (R24-4).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): fence base-tree reuse with a per-run secret kept outside the mount
The reuse fence keyed on inputs the sandbox's read-write mount exposes:
the run epoch was the plan's mtime and the plan lives inside the mount,
so reviewed code could forge the stamp and have the pipeline certify an
attacker-shaped tree as the A/B base (R15-4). The stamp's second line is
now a random per-run nonce persisted beside REVIEW_LEASE_DIR — created
atomically by the run's first host process, adopted by its shards,
unreadable and unwritable from inside the mount. The failed marker
carries the same fence: a planted sha-only marker no longer settles the
A/B lane into "already failed" with no build attempted (R22-1).
At marker write the untracked path set the build legitimately left is
recorded host-side, and reuse requires the current set to be a subset —
an untracked plant dropped after the stamp (dist/cli.js, what a
host-side A/B executes as the BASE side) falls through to the rebuild
that sweeps it (R25-1). And when a tree THIS run built fails a state
check — legitimate codegen dirt, a concurrent shard's snapshot update —
the command declines busy instead of discarding the live tree another
shard may be mid-A/B in; discard-and-rebuild is kept for cross-run
leftovers, forges, and plants (R26-1).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): fail the trust gates closed when the mount root is refused
mountRootFor's null was overloaded: "outside any temp dir" and "inside
one, but refused" were the same value, and every trust gate read a
refusal as "nothing to police" — measured end-to-end with a symlinked
.qwen/tmp, where a gated worktree add ran through the plant and its
smudge filter wrote its canary on the host (R26-3). The gates now ask
the containment question lexically (insideReviewTmpLexically, no
filesystem) beside mountRootFor, and where the spelling says inside and
the mount says null they refuse with a reason of their own — including
a launch directory that no longer exists at its spelling, the rename
attack's shape.
The launch-dir gate's memo made that inversion permanent: a null
produced while mountRootFor consulted the filesystem was cached TRUSTED
for the process lifetime, so one from inside the
outer mount switched the gate off and a planted filter.<x>.clean
executed as the review user (R26-2). Only the pure no-marker string
scan is memoized now; inside a review temp dir the question is re-asked
on every call, as documented.
Also: releaseWorktree's not-freed predicate keyed only on refusal, so a
probe's {status: null} (spawn failure, timeout kill, deleted cwd) read
as "no objection" and freed:true was certified over a surviving
registration and branch — the next add met "missing but already
registered" (R24-3); and its first statement read the cwd outside any
try, so a deleted cwd threw uv_cwd through the never-throws contract
(R19-4). comment-status evaluated the same trust gate twice per run and
could certify a trust state its own threads[] contradicted; the verdict
is now computed once and handed to makeGitProbe (R26-4). mountRootFor's
walk bound moves from the checkout's parent to the geometry-aware root
(a linked direct parent refused every macOS /tmp checkout, R19-3), and
its Windows refusal covers the colon-less UNC shape as a class
(R19-2).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): round-trip the admin entry's backpointer in the location gate
untrustedPointer admitted a gitfile rewritten to a SIBLING worktree's
legitimate admin entry: the entry is outside the mount and is not the
common dir, so both location questions passed while every host-side
command through it measured and mutated the sibling tree (R23-2). The
gate now carries the same backpointer round-trip the write paths always
have: the entry's gitdir file must resolve back to the tree it is asked
about, or the tree is refused. Only a definitive mismatch is this arm's
to name — an unreadable or dangling backpointer redirects nothing, is
refused by the downstream identity checks with their own reasons, and
the one writer who could corrupt a backpointer to duck the mismatch
cannot reach an entry outside the mount at all.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): count a lossy decode of git's answer as no answer (R8-2)
encoding: 'utf8' maps an undecodable byte in git's byte-exact rev-parse
output to U+FFFD, so a gitfile target holding an invalid UTF-8 byte was
judged at the U+FFFD spelling while git acted on the original bytes: a
plant under the raw-byte name inside the mount and a decoy under the
U+FFFD one, symlinked to the tree's real admin entry outside it, passed
every question the location gate asks — the round-trip included — while
the gated write resolved through the plant. No gate may judge a spelling
git never printed: a U+FFFD anywhere in the answer now counts as the
question not answered, which every caller already fails closed on.
The witness plants the full shape (raw-byte admin entry, U+FFFD decoy)
and is itself gated to Linux: NTFS is UTF-16 and APFS rejects invalid
UTF-8 names with EILSEQ, so neither the fixture nor the attack can
exist there.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): fail the prune arm when git could not be run at all (R23-8)
The symlink arm's pruneWorktrees() surfaced only the probe's refusal, so
the third shape {out: null, status: null, refusal: null} — a spawn
failure, the timeout kill, a deleted cwd — read as a successful prune:
the arm announced "Removed ... link", wrote no stderr line, and released
the lease over a registration git never swept, and the next worktree add
met "missing but already registered" with nobody told why. Same
shape-conflation class as R19-1 at a second site. Only genuine non-zero
exits stay swallowed.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(review): type the lease-read mocks with their parameters
tsc --build (unlike the vitest run) rejects the readReviewWorktreeLeaseAt
delegation calling a zero-argument vi.fn with two.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(review): close what the trust-artifact move's first pass missed
Four follow-ups from an adversarial audit of the fence:
The untracked record was git status's collapsed path set: dist/ and
node_modules/ recorded as one entry each, so a plant dropped INSIDE a
directory the build left — the exact executable a host-side A/B runs —
changed no membership and passed the subset fence. The record is now
file-level (ls-files --others, which never collapses), so an addition
inside a recorded directory IS a membership change.
The trust file was keyed by the plan's path and mtime, and the plan lives
inside the mount: a utimensat backdate to an earlier run's exact stamp
re-keyed this run to that run's file, adopting its nonce. The key now
takes max(mtimeMs, ctimeMs) — ctime cannot be set from userland, so the
identity cannot be rolled back.
The busy arm over-generalized: a rewritten pointer or moved HEAD on a
tree this run built has no benign cause, and declining left the plant
standing for the run. Those arms now fall through to discard-and-rebuild
— while tracked dirt AND untracked additions (an A/B's own snapshot
--update or cache output is the benign shape) decline busy instead.
And the discard itself could not rebuild a planted tree: worktree remove
refuses a tree whose gitfile no longer points back, so the removal
cleared the coherent plant's admin dir while the real registration
survived and the next add met "missing but already registered" — the
deferred refusal->rebuild wedge. When the remove fails, the reverse scan
now also runs, narrowed to the tree's own basename so a tampered sibling
entry is never deleted.
Also pins the sweep's per-field equality with a same-target doctored
twin (the worktreePath comparison had no witness) and gives
readReviewWorktreeLeaseAt a legacy found-at-path witness.
* fix(review): close the audit's second pass — witness gaps and one substitution route
From the adversarial audit of the takeover batch:
- releaseWorktree's symlink arm now has its own witness for the
status-null prune (the main arm had one; the sibling arm was pinned by
nothing — the mutation survived green).
- The launch-dir memo's comment now names the rename attack's actual
premise — Node's cachedCwd, not a kernel property, so a future
in-process chdir is the thing that re-opens the window — and the canary
pins the stale-spelling premise instead of assuming it.
- scratch-tree's --standalone shape asks the location question before
buildStandaloneTree: headSha was read through the review worktree's own
gitfile, and with no --fetched-sha pin a planted HEAD would put the
plant's content in the tree presented as the commit under review.
- revert-hunk's outermost-bound arithmetic gains a nested-geometry
witness (a swap at the OUTER layer), so a drift to lastIndexOf reds it.
* fix(review): anchor the scratch sweep at the captured root (R30-6)
The entry-captured repositoryRoot was handed to the ancestor walks but
not to scratchWorktreesOf: its resolve(worktree) and redirectedAncestor
default stop both read process.cwd() again, so a cwd deleted mid-run
threw uv_cwd out of the sweep — past the entry guard that exists to
catch exactly that. Both now anchor at the captured root. The witness is
the call count: a full cleanup reads process.cwd() exactly once.
* fix(review): the base-tree fence reads only host-side state
Rounds 27-30 found the nonce-in-marker design's hole: the markers carried
the run secret in cleartext inside the tree — a child of the read-write
mount — so reviewed code read it and forged a settling failed-marker (the
A/B lane suppressed as "infrastructure") or an ok marker over a tree it
shaped. The fence now reads the trust record alone, kept outside the
mount: per tree it carries the pinned base, the build's verdict, and the
untracked inventory per file with its size+ctime pair — an in-place
rewrite of a recorded file is as visible as an added one. Markers in the
tree are informational baseSha notes, excluded from every fence decision.
The trust file is one per plan path, rotated in place on identity drift
so no earlier generation survives to adopt; the run identity is the
worktree lease's mtime when one is held (host-side, mount-untouchable)
and the plan mtime with the ledger's tolerance otherwise. A mid-run
mergeBaseSha rewrite conflicts with the pin and declines rather than
rebuilding at a sha named after the fact. Same-run trees with a torn or
missing record decline busy instead of being discarded; pointer/HEAD
rewrites still discard-and-rebuild. The record step re-asks the pointer
question first, and the git helpers carry the inert-config pins and a
512 MiB buffer so a real tree's ignored listing cannot silently drop the
record.
* fix(review): the mount-null is three answers, and the gates split them
Where containment cannot exist — a drive-letter colon, a UNC share, a
colon-bearing POSIX checkout — the review never ran sandboxed, so the
mount is no trust boundary and the gates' docstrings promise silence.
The fail-closed arm read every null as a refusal and refused
unconditionally there (measured: 43 red tests under the declared Windows
model). mountRootFor now shares one implementation with a discriminated
nullKind (outside / unmountable / refused); the gates fail closed only on
a redirect or a root that vanished mid-check, and stay silent where
containment cannot exist.
The pointer gate's round-trip is now exact: the admin entry must own the
tree the question is about (untrustedRepositoryFrom compares against
git's own --show-toplevel, which a borrow cannot bring into agreement),
an owner that cannot be resolved fails closed, and a redirect planted
between the mount root and the owner is refused before anything resolves
through it — the round trip must not agree with a link. The mount root is
threaded through the questions rather than re-asked mid-gate, so a
mid-gate flip of the mount cannot split the answer. The flat-case walk
bound moves to .qwen: the checkout's own directory is the user's layout,
not the pipeline's.
releaseWorktree's not-freed predicate keys on the prune alone now: a
remove that answered 0 already cleared the registration, and keying on
the remove's null published freed:false over a release the prune had
completed. Witnesses pin each arm, including the symlink arm that had
none.
* fix(review): the legacy lease is never believed, the mirror never fatal
The mtime-bounded honored read is gone entirely: a pinned-to-the-past
cutoff froze the honored population at release day while remaining a
utimes-forgeable permanent DoS channel from inside the mount. A
legacy-path file is residue now — the mirror still replaces it (loudly,
naming the displaced session, when it parses as another session's lock)
so pre-move builds keep seeing the lock, and only the new path answers
the gate. The mirror write is best-effort (an obstruction in the mounted
directory is mount weather, warned and skipped, never a failed
acquisition) and replaces via tmp-file + renameSync, which never follows
a planted symlink and never opens a readerless FIFO.
The finalizer is driven solely by the trusted directory: one destructive
pass per target, the legacy twin deleted only when content-identical —
and never listed, so a poisoned mount cannot shape the sweep. The lease
directory re-roots lexically to the outermost enclosing repository in the
nested geometry, so a review launched from inside another review's
worktree keeps its host-trusted state outside every layer's mount.
fetch-pr's step 4 also asks the destination's ancestors: a symlink left
standing by a declined releaseWorktree would otherwise have mkdirSync and
worktree add create and check out the PR's code at the link's target —
outside the review temp dir, unsandboxed. And the review job's lease
sweep feeds both globs through remove_review_tree's repair ladder instead
of a bare rm -rf, so a mode-000 wedge is reported rather than silently
surviving. Tests that delete the process's own cwd or need a mount are
gated to hosts where those exist.
* fix(review): judge the launch directory where the kernel stands (R30-47)
The launch-dir gate judged Node's cached process.cwd() spelling while
every wrapper spawns with no cwd, inheriting the KERNEL cwd — and the two
diverge exactly once an ancestor is renamed. A leaf rename inside the
mount plus the spelling re-stood as a symlink to an honest sibling made
the cached spelling pass every arm while git ran in the renamed plant.
The gate now asks the kernel (/bin/pwd -P) where the process stands and
judges that spelling; a divergence is the rename, refused. Windows keeps
the cached-spelling judgement: no /bin/pwd there, and no containment to
protect either. The canary witness drives the full leaf-rename shape and
goes red with the kernel judgement disabled.
* fix(review): the second audit round's findings on the batch itself
- base-tree trust: key under the OUTERMOST repository in nested geometry
— the lease re-roots there (R27-6) and the trust file must follow, or
it lands inside the outer review's writable mount where the fence's
record is the outer code's to forge; runIdentityMs then reads the lease
where it actually lives.
- redirectedAncestor: keep walking past a nonexistent component — a leaf
ENOENT under a symlinked ancestor used to end the walk with the link
itself un-lstat'd, which defeated the R27-9 gate when the link sat
above an unresolvable leaf.
- createReviewWorktreeLease: a same-session refresh preserves the lease
mtime — the base-tree run identity keys on it, and a resumed run would
otherwise rotate the trust state and discard the standing base tree.
- untrackedInventory is a null-prototype map: a file literally named
__proto__ would otherwise land in the prototype setter, invisible to
the fence.
- mirror/atomicWrite tmp files are removed on a failed rename; the
standalone refusal no longer prescribes a remedy (--fetched-sha) that
the same gate would refuse.
* fix(review): the mirror's tmp cleanup must not throw either
The mirror is documented never fatal, but the rename-failure catch called
rmSync unguarded: an rm error there escaped createReviewWorktreeLease
after the new-path lease had already won, rolling back the acquisition —
the fatal outcome the contract exists to prevent, one failure deeper.
Same wrap in the trust store's atomicWrite, where a failing cleanup would
have masked the original write error.
* fix(review): import rmSync in the trust store, type the rmSync mock's path
esbuild-transpiled vitest runs don't typecheck, so the round-3 commit
built red in CI: atomicWrite's new catch used rmSync without importing
it, and the lease suite's rmSync mock typed the path PathOrFileDescriptor
where the real signature takes PathLike.
* refactor(review): split the base-tree reuse fence out of this PR
The fence landed here on 2026-09-08 to answer R29-1, and it has been the
cluster the review keeps returning to: 11 of the 18 open Criticals live in
`base-tree.ts` / `lib/base-tree-trust.ts` or in code those two pulled into
other files, against 22% of the diff. Round 33 said it plainly — a cluster
that keeps producing siblings is one root cause being fixed instance by
instance, and splitting it ends the loop faster than another round.
The dependency runs one way: the fence uses the location gates from
`lib/worktree.ts`, nothing in the gates uses the fence. So it lifts out
whole, and it comes back as its own PR stacked on this one.
What leaves:
- `lib/base-tree-trust.ts` and its suite — the host-side trust store.
- `base-tree.ts` and `base-tree.test.ts` return to their state before the
fence commit. The location gates this PR is about (`untrustedGitfile` at
the reuse fast path, at the worktree read and at the rebuild) predate the
fence and stay; `GIT_NEUTRALIZE`, `gitOutZ` and the 512 MB `maxBuffer`
were introduced for the fence's inventory listing and leave with it.
- The canary's base-tree case goes back to certifying on the in-tree marker
— which is what the command reads again — and the fence's own
record-step canary leaves.
- `createReviewWorktreeLease`'s mtime preservation: it exists only so the
fence's run identity survives a resumed run's re-acquisition, and it is
R33-1's subject. Its tmp-file cleanup on a failed mirror rename is
unrelated and stays.
`packages/cli` is green (120 files, 5997 tests), `tsc --noEmit` clean, and
the one deterministically-red test on Linux (R33-2) leaves with the fence
file it lived in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vBGpv2LrLQ8QNEYu2gsNn
* fix(review): the seven Criticals the split leaves behind
Every one is mutation-probed: the guard is removed or negated, the case
named beside it goes red, and the rest of the suite stays green.
`releaseWorktree` (R32-2). The `refusal !== null` disjunct that forced
`freed: false` was not gated on "neither arm cleared the registration",
while `couldNotRun` beside it was. Each probe re-asks `launchDirRefusal()`
from scratch, so a refusal on one arm negated a release the other had
completed — the same miskeying R31-1 and R28-11 record for the null-status
arm. A refusal is already inside `couldNotRun` for its own arm, so it now
only ever supplies the REASON for that verdict, never makes one.
`untrustedPointer`, question 3 (R32-10). The containment test that gates
the `redirectedAncestor` walk was bounded at the DEEPEST review temp root
while the location question two above is widened to the OUTERMOST. A
sibling of the enclosing review's worktree, relinked at this tree, sat
outside the walk entirely and the realpath round trip agreed with the link.
Both questions ask against `outermostReviewTmpRoot` now.
`untrustedPointer`, question 2 (R33-3). `resolvedCommonDir` dropped the
`notARepository` half of the taxonomy, so a timeout, a spawn failure or a
git too old for `--path-format` read as "no objection" — and question 2 is
the only one of the three that sees `gitdir: <repo>/.git`. It carries the
taxonomy now and refuses every no-answer that is not git's own.
`dropWorktreeRegistration` (R32-12). git appends a counter to a worktree id
whenever the basename is taken, so the reverse scan's `id === basename`
narrowing could not match `wt1` — the entry the tree actually owns — and
left the registration standing after `rmSync` took the directory. Matched
against git's own id grammar instead; the gitdir-names-this-path and
tree-is-gone narrowings still carry the "never a sibling's" half.
`fetch-pr` step 4 (R32-11). The destination guard walked `dirname(wt)` and
never `wt`, so a link AT the worktree path redirected the create and the
checkout. The walk starts at the leaf; `redirectedAncestor` lstats its
first component before any stop test, so the ancestors are unchanged.
`clearReviewWorktreeLease` (R32-4). The legacy delete is the one
mount-resident removal in the clear path and it ran unfenced, after the
trusted lease was already released — a planted mode-500 directory or a
Windows handle threw out of `runCleanup`, whose tests pin it as never
throwing. Loud instead, like the mirror write.
The mirror displacement warning (R32-3). It interpolated a session id read
out of the writable mount verbatim; a newline in it forges a second stderr
line and a raw ESC repaints the terminal. Through `inertPath` now, the
flattener the rest of the repo already uses, and bounded — the warning
names the displaced run, it does not carry its payload.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vBGpv2LrLQ8QNEYu2gsNn
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: wenshao <nigolaschao777@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
779cfe913b
|
fix(cli): drain notification queue when a turn errors out (#11559)
* fix(cli): drain notification queue when a turn errors out * test(cli): reconcile error-time drain expectation with the fix The daemon Todo Stop Guard test pinned the old invariant that an unarmed turn does not drain the automatic queues when it errors out. That behavior stranded mid-turn background completions and pinned activeWorkState, which is exactly the bug this fix removes. The test now asserts the completion is drained and delivered after the error. |
||
|
|
dfcf07accc
|
feat(serve): support 256 workspaces by default (#11515)
* feat(serve): support 256 workspaces by default Make workspace registration capacity configurable from 1 to 256 with a default of 256. Preserve independent channel and ACP child policies, bound persistent registrations, and keep the expanded global session default at 800. Refs #11386 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): correct capacity docs and diagnostics from review - docs/users/qwen-serve.md documented --channels all; the flag is --channel. - The promotion path lost its server-side diagnostic when an oversize store refused the write, while the sibling create path kept logging. - The startup overflow message labelled explicit --workspace values as Restored, sending operators to a store that holds fewer records. - Registration-capacity validation named the environment variable even when an embedder supplied an out-of-range option. - The channel-owner message asserted retained owners were counted and told operators to stop a worker, neither of which holds at boot. - Session-total and new-409 documentation still described pre-change behaviour, and the design table's line anchors drift on merge. - Tests read the registration cap from the ambient environment, and neither the environment-driven cap, the legacy session derivation, the injected route cap, nor the internal-runtime exemption had a witness. --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
8259daeed0
|
fix(vscode): preserve active file exclusion (#11568)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
4c6e2adcea
|
feat(web-shell): render URLs in user messages as clickable links (#11464)
* feat(web-shell): render URLs in user messages as clickable links User-typed URLs in the Web Shell transcript rendered as plain text, so they could not be opened by click. Split message text with a small tokenizer (explicit http(s) scheme only, CJK characters terminate a match, trailing sentence punctuation and unmatched closing brackets are trimmed, a bare scheme stays text) and render URL segments through a LinkifiedText component that mirrors MarkdownLink's safety check and desktop-shell external-open routing. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(web-shell): harden user-message URL boundaries from review Address review on #11464: - Invert the tokenizer's character class from a hand-maintained denylist to an ASCII URL-grammar allowlist. The denylist absorbed unlisted scripts into the href (emoji and Thai shifted the host via IDNA) and truncated URLs containing an apostrophe (en.wikipedia.org/wiki/L'Aquila linked to the unrelated /wiki/L page). The allowlist fails closed and matches remark-gfm's boundary behavior. - Trim markdown emphasis delimiters (*, _, backslash) from the match end so a pasted **url** does not request /docs**. - Rewrite trimTrailing to count each bracket pair's excess closers once and trim by index, making the trim O(n) instead of O(n^2) on inputs with long runs of unmatched closers. - Pin the host-parser parts path in UserMessage with a URL test; tag the Playwright spec @smoke so PR CI actually runs it; reuse Markdown's .link rule instead of copying it; fix the garbled backtick bullet in both design docs and record the raw-IRI prefix tradeoff. Mutation witnesses: reverting the allowlist reddens 9 tokenizer tests, dropping * _ backslash from TRAILING_PUNCT reddens the emphasis test, and reverting the parsed-parts branch reddens the new UserMessage case. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(web-shell): drop dead TRAILING_PUNCT entries and correct the gfm-parity claim Address round-2 review on #11464: - TRAILING_PUNCT held three entries the allowlist can never produce (double quote, backtick, backslash), so the round-1 commit message's backslash mutation witness was false. The set is now exactly the reachable trimmable chars, and the quote/backtick it.each cases are retargeted as termination tests (they pin the allowlist boundary, not the trimmer). - The design docs' decision row claimed the allowlist matches remark-gfm boundary behavior; measurement says otherwise in both directions (trailing ~ kept, no left-boundary rule, stricter on trailing backtick/backslash, balanced a[0] keeps its ]). The row now says inspired-by and lists the known deltas, in both language versions. Mutation witnesses: dropping * and _ from TRAILING_PUNCT reddens the emphasis test; adding double quote to the allowlist reddens the new termination test; the removed dead entries were verified to change no test outcome (that is why they are dead). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(web-shell): trim trailing tilde, normalize bare % in href, pin tokenizer coverage Address round-3 review on #11464: - Add GFM's strikethrough delimiter ~ to TRAILING_PUNCT so a pasted ~~https://github.com/QwenLM/qwen-code~~ does not 404 on click, while interior tildes (/~user) stay intact. The docs' delta list drops the now-fixed 'trailing ~ kept' entry and gains the measured character- reference divergence (& kept verbatim where remark-gfm resolves it). - Normalize a bare % (not followed by two hex digits) to %25 in the rendered href only — byte-identical to the assistant markdown path's normalizeUri — keeping the visible text verbatim; the docs' 'the one case' superlative is replaced by the measured wrong-link shapes. - Pin the tokenizer axes the suite previously left green under mutation: interior membership for all 11 unpinned allowlist characters plus a host:port port fixture (dropping : now reddens two tests), termination for all seven excluded printable ASCII delimiters, the angle-bracket autolink form, and the no-left-boundary decision. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(web-shell): match normalizeUri's % rule and pin the desktop opener's href Address round-4 review on #11464: - The bare-% normalization used a hex-only lookahead, while the assistant path's normalizeUri (mdast-util-to-hast) skips encoding when two ASCII alphanumerics follow — so 50%off diverged between user and assistant bubbles. The lookahead is widened to [0-9A-Za-z]{2}, measured identical to the real normalizeUri on every reachable input (space cannot appear in a match). The comment now also names the remaining delta: raw [ ] { } stay verbatim where normalizeUri percent-encodes them, and both design docs record it; the English IRI row's inverted claim about the assistant path is corrected (it does normalize). - Pin that the desktop external opener receives the normalized href, not the raw matched text — the segment.value mutation survives all previous tests and now reddens the new desktop-click case. - Correct the interior-fixture comment in linkify.test.ts: only six of the eleven characters are trimmed by TRAILING_PUNCT at a trailing position; the other five are interior for fixture uniformity. Mutation witnesses: hex-only lookahead reddens the 50%off case; passing segment.value to the opener reddens the desktop-click % case; both stay green on the committed source. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
76635386e4
|
fix(review): dispatch review agents through parallel workflows (#11524)
* fix(review): dispatch review agents through parallel workflows * fix(review): respect workflow preferences and validate loaded scripts * test(web-shell): align fixtures with branding and compact footer |
||
|
|
424e40cc1f
|
test(core): cover bundled ConPTY fallback and replies (#11555)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
pnpm Worktree Smoke / Install (ubuntu-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (windows-latest) (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
pnpm Worktree Smoke / Install (macos-latest) (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
585589ac67
|
fix(dev): install hooks during worktree bootstrap (#11496)
* fix(dev): install hooks during worktree bootstrap
* fix(dev): keep worktree hook setup out of the shared git config
Husky runs `git config core.hooksPath .husky/_` with no --worktree, so
from a linked worktree the value lands in the config every worktree of
the repository shares while `.husky/_` is created only in the checkout
being bootstrapped. Skip the Husky step and report it when the key is
unset and this checkout does not own the repository config, so a
bootstrap can no longer repoint hook resolution for roots that never
received the wrappers. A primary checkout still installs hooks, and an
already-configured `core.hooksPath` is untouched.
Also drop the caller's success exit, which `install()` made unreachable
when it started exiting on every successful path, and bring the
pnpm-worktree-bootstrap design doc in line with a hook step it still
recorded as deliberately skipped.
The new fixture runs the real script against a throwaway root whose
`.git` is a file or a directory and whose config comes from a real
`git init` repo, which makes both new branches reachable and pins the
fail-closed guard: the injected `GIT_CONFIG_*` constant holds one value
for the child's whole lifetime and cannot express the unset state that
asks husky to write.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtuo29vduf
* fix(dev): ask git which root owns the worktree hook config
`ownsRepositoryConfig()` inferred repository-config ownership from the
filesystem shape of `.git`, and the proxy is wrong at both ends. With no
`.git` at all, `statSync(..., { throwIfNoEntry: false })` returns `undefined`
and the predicate folded that absence into "owns the config", so a
repository-less checkout ran husky into its `.git can't be found` soft failure
(exit 0) and the fail-closed check then turned a successful dependency install
into exit 1 blaming Husky. A `.git` file is not only a linked worktree either:
`git clone --separate-git-dir` checkouts and submodules have one too and do own
their config, so hooks were declined where they would have been correctly
scoped.
Ask git instead: `rev-parse --git-dir` differs from `--git-common-dir` only in
a linked worktree, and a failed `rev-parse` names the no-repository state, so
the skip notice stops asserting "linked worktree" and the bootstrap does not
gain a hard git dependency.
Also bind the fail-closed check to an artefact husky's own write produced, not
only to the config value. husky 9.1.7 exits 0 on every soft-failure path
(`index.js:16` git command not found, `index.js:17` refused `git config` write)
before the `mkdirSync(_())` on line 19, and a linked worktree inherits
`core.hooksPath` from the config it shares, so re-reading that value compared
it against itself and passed exactly when husky had created nothing.
Restore the registry-fallback case's hermeticity: `PATH` holds only the stub
directory again, which now also pins that the retry needs no ambient git.
Rebuild the ownership fixture from real git layouts, because `rev-parse`
resolves nothing for a `mkdirSync`'d `.git` or a hand-written `gitdir:` file,
and give the stub husky a failing mode so the exit code husky returns is pinned
rather than the install result's.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtuys2rjuu
* fix(dev): surface hooks-path read failures in worktree bootstrap
- getHooksPath() no longer collapses a refused git config read (exit 128/2/3) into "unset": only an absent key (exit 1) or a missing git binary keeps the skip path; anything else fails the bootstrap with the read error instead of a green, hook-less worktree.
- The linked-worktree skip notice now names the recovery path: re-run this script once the primary checkout has hooks installed.
- Tests cover a git stub exiting 128, the real-world unset HUSKY state, and pin the recovery sentence in the skip notice.
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
||
|
|
0b018e23b1 | fix(ci): sync main and complete continuation test fixture | ||
|
|
fe7cf68644
|
fix(ci): grant the packaging check the permissions its callee declares (#11532)
The scheduled packaging check failed to start. A caller must grant every permission the called workflow's jobs declare, and the release workflow's publish job declares `contents: write` while the OSS mirror declares `actions: read` — the check granted only `contents: read`. That validation runs when the workflow file is loaded, before any `if` is evaluated, so the two jobs being unreachable in a dry run does not exempt their permissions. The run died in two seconds with no job and no annotation, which is what a startup failure looks like. Grant both. Nothing here writes: the check hardcodes `dry_run` to true, which skips the publish and the mirror job on their own conditions. |
||
|
|
0ef35351e9
|
feat(acp): register daemon-managed sessions in the session registry and let them send peer messages (#11488)
* feat(acp): register daemon-managed sessions in the session registry and let them send peer messages Only the interactive UI registered, so a session `qwen serve` drives was invisible to `qwen sessions ps`, absent from every peer's `list_agents`, and unable to send at all: the send path reads this process's own record to learn its reply address, and with no record it reported the feature disabled. A user with a terminal open and a daemon session working in the same repository could not have the two speak to each other. The registry was keyed by PID, one record per process, which was right while a process meant a session. The `qwen --acp` child a daemon spawns hosts up to thirty-two at once, each with its own id, name and working directory, and one record cannot describe them. So a process hosting several writes one record per session, `<pid>-<8 hex>.json`. The suffix is minted at registration and never moves — deliberately not derived from the session id, because an id is swapped underneath a live session by a `/clear` or a session load, and a record that renamed itself would strand every reader holding the old name. The PID in front of the suffix is still what liveness, the sweep and the namespace guards read, so a minted record is reaped exactly like a shared one, and a reader too old to know the name does not see it rather than mistaking it for litter. Which record a caller means is a slot, threaded through patch, unregister and the own-record read. A Config learns its slot from the registration itself, so a session's `/clear`, its `/cd` and its exit each touch its own record and no sibling's. The process binds one inbox for all its sessions and writes that address into every record. Senders already pin `toSessionId` on every frame, so that is what tells the sessions apart; a frame naming a session this process does not hold — or naming none, which used to be deliverable because there was only one session it could have meant — is answered `misaddressed`. That sharing broke an assumption elsewhere. The send path and `list_agents` excluded "this session" by matching either its id or its reply address, the address being a proxy for the id that held while one process bound one socket. Siblings share an inbox, so the proxy stopped holding and every sibling of the sending session would have been filtered out as if it were the sender. Identity is the session id alone now; a twin under a second process is still excluded, because that is what the id catches. Inbound is refused rather than held. A hold is a question put to a person, and nobody is watching a held-message list on a daemon-managed session's behalf, so holding would leave every sender waiting out a five-minute expiry to learn nothing. Where a held message should surface for these sessions is a real question and separate work; the protocol page says so rather than leaving a reader to find out. Registration happens only while cross-session messaging is on. The interactive UI registers unconditionally because its record also answers "what is running right now"; a daemon-managed session's record exists to be addressed, and one with no inbox behind it would put a name in every peer's listing that can be addressed and never answered. The inbox is bound by the first session that needs one, and the transport is imported at that moment, so an ACP process that never messages a peer never loads it. Closing is registered as exit cleanup and also runs from both teardown methods, because a bare signal reaches neither. Whether to bind at all is read from the settings the process started with: the agent's `settings` field is re-pointed at whichever session is being handled, and this is a question about the process. Part of #10925 Unblocks #10118 (M3) * fix(acp): import the peer-messaging transport statically so the fast-path bundle closure stays clean The agent imported the transport and `registerSession` with dynamic `import()`s, on the theory that a process with messaging off should not load them. The theory bought nothing and cost something: every module behind both imports already sits in the agent's static closure, and a dynamic import of the core barrel turns that barrel into a code-splitting entry. esbuild then re-partitions the shared chunks, and one of the chunks the agent imports statically came to hold iconv-lite's encoding tables — a module the ACP fast path must not load, which is exactly what the startup bundle closure check exists to catch, and did. Both imports are static again, the catch-up loop that existed only to cover the import's latency is gone, and the design note says why the transport is loaded the way it is. Part of #10925 * fix(acp): judge hosted sessions by live ids and their own settings Review follow-ups on daemon-managed session registration: - ownsSessionId tested the frozen publication key, so a frame pinned to the id the record actually advertises after /clear was answered misaddressed; test each hosted Config's live id as well. - Registration and the inbox bind followed the process's startup settings; each session's own settings decide instead. - A failed inbox bind latched as started, so no session ever retried it; treat null/failure as not-started, and latch closed on teardown so a retry cannot resurrect the socket. - disposeSessions awaited the inbox drain before aborting generations; abort first so a running turn cannot keep executing tools after the client is gone. - The send-side mirror re-minted its bucket per addressed session id while the receiver meters per address, letting alternating siblings reset the allowance and duplicate window; keep one bucket per inbox address and stop the repeat refusal naming "that session". - The self-exclusion could be beaten by a re-id patch landing between the own-record read and the directory read; re-check a peer on this session's own inbox against a fresh read, on the send path and in list_agents alike. - The record filename check accepted a zero-padded pid on platforms without a namespace id; require the canonical decimal form. - Drop the dead sessionId parameter from the misaddressed-frame re-assert, document the two record filename forms and the one-sender consequence, and add the Chinese design doc. * fix(ipc): probe each inbox once, and keep a minted record nameable Two behaviour fixes from review, plus the tests that would have caught them and the cases the slot plumbing was missing. A process hosting several sessions writes one record per session, all pointing at the one inbox. The liveness pass dialled that socket once per record, so a daemon with 32 hosted sessions opened 32 simultaneous connections at one address every time a peer listed the directory — enough to trip the inbox backlog. Probe each distinct address once and apply the verdict to every record that names it. Re-registration deleted the captured path of a minted record before it knew whether the delete would happen. On a transient read error it returned early with the capture already gone, and a minted suffix cannot be derived a second time: the record stayed on disk under a name this process could no longer patch or remove, outliving the session as a directory entry pointing at a dead inbox. Keep the capture on that one branch. Tests: every registration fixture returned the shared slot, which is also the parameter default, so nothing could tell "passes the slot it registered under" from "passes the default" — config and send_message now register a minted slot and name it in the assertion. Also covers the unknown-slot path against a live shared record, a minted filename surviving an id swap, near-miss record filenames, probe-once, the ownsSessionId branches of the inbound gate, and an ACP host running two sessions through one inbox. * test(core): name the minted slot in the list_agents identity read --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
1740eaa653 | feat(web-shell): continue interrupted sessions | ||
|
|
2488d12d8b
|
fix(web-shell): realign two suites with shipped behavior to unbreak main CI (#11525) (#11530)
The brand PR #11244 landed on top of #11470 and left two web-shell suites failing on main, redding the Test job: - The sidebar brand tests assert the version tooltip's title while mounting at the default 260px width, but #11470 hides the version label below the 344px compact footer breakpoint, so the query finds nothing. Mount those brand tests at 360px, matching the convention of the footer-version suite. - The workspace-session loading test pins the exact daemon requests issued on load, but the workspace provider now fetches GET /brand beside capabilities. Answer it with a 404 (an older daemon without the route, which settles the fetch instead of arming the retry timer) and expect the call, including the StrictMode remount double. Both are test-environment alignments; no production behavior changes. Fixes #11525 Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: Shaojin Wen <szujobs@gmail.com> Co-authored-by: 易良 <1204183885@qq.com> |
||
|
|
b74592785b
|
fix(ci): record the qwen-autofix.yml size drift two in-allowance PRs accumulated (#11529) (#11537)
The ratchet tolerates 4096 bytes of growth per PR without a baseline bump. Two consecutive qwen-autofix.yml PRs — #9940 (to 465612, +2522 over the recorded 463090) and #11516 (to 469259, +3647 more) — were each inside the allowance against their own base, so neither was forced to touch the recorded number, and the sum (6169) sails past it. Since #11516 landed, every strict gate run — a local run without a PR base SHA, the push run of the PR itself whose event.before predates the growth, and any future PR that touches qwen-autofix.yml — fails on growth it did not author, the red-wall shape #9904's leniency cannot cover for the file's own editors. Record the measured size (469259, wc -c on main) as the gate's own error message prescribes. The file now sits 741 bytes under the 470000-byte absolute gate, so the next real growth has to move prose into qwen-autofix.md instead of reaching for the ratchet again. Mutation probe: with the entry at 463090 the strict vitest mirror fails ("6169 bytes over its recorded 463090"); at 469259 the workflow-size suite passes 208/208 and check-workflow-size.sh exits 0. |
||
|
|
b3d022b357
|
fix(vscode): restore pre-cutover conversation history in the panel (#11495)
* fix(vscode): surface pre-cutover conversations in the panel history The WebShell cutover (#9811) switched the companion's history list to the daemon catalog filtered by sourceType 'vscode'. Sessions recorded before creator attribution existed carry no source metadata, so upgrading to v0.23.x emptied the panel history even though every transcript is still on disk (#11489). Claim the panel's own legacy sessions back instead of widening the filter: the host ships its globalState conversation ids as an allowlist, the panel intersects it with the daemon's default (unattributed) catalog, and opening one of them lazily stamps the 'vscode' source through the existing restore-time attribution path. globalState stays untouched for downgrade safety, and unattributed CLI/browser sessions stay out of the panel. * fix(vscode): run the legacy session scan once per bootstrap Review feedback: the allowlist never converges for ids that can no longer match (other workspaces' conversations, deleted transcripts, sessions already stamped by a restore), so keying the scan to every history-dropdown open re-paged the default catalog indefinitely. Gate it on a per-bootstrap done flag instead; failures still retry on the next open. Also unpin the scan page size from the test. --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
11e629b3a1
|
perf(export): split the transcript renderer's embedded CSS into a versioned asset (#11485)
* perf(export): split the transcript renderer's embedded CSS into a versioned asset The export renderer carried the web-shell component stylesheet as a ~2.3 MB string literal, so every reader parsed and compiled 4.1 MB of JS (56% of it dead CSS) before a transcript could render. Lift that literal out at export build time into a version-pinned, SRI-protected export-transcript-document.css served from unpkg and loaded via a nonce-bearing <link>, dropping the renderer JS to ~1.83 MB. The transform is an esbuild onLoad plugin in the web-templates export build that strips the injected CSS constant from web-shell's dist/transcript.js; web-shell source and runtime behavior are untouched. The document's fail-closed load-error path is extended to the stylesheet so a missing CSS asset fails the same way as a missing renderer. * fix(export): match the transcript CSS entry on Windows paths too esbuild hands plugin callbacks the platform-native absolute path, so the extract-transcript-css `onLoad` filter never matched on Windows: the callback did not run, `extractedTranscriptCss.css` stayed undefined, and the mandatory extraction guard below aborted the build. That build is not platform-gated — `scripts/prepare.js` runs it from `prepare`, so `npm ci` itself would fail on every Windows contributor and on the windows-latest legs of test_windows and desktop-release. Widen the separator to `[\\/]`, keeping the `transcript\.js$` tail so the barred `web-shell/dist/index.js` package root still does not match. The filter moves to transcript-css-entry.mjs because build.mjs is a top-level-await script with no harness — the same reason scripts/sdk-node-exporter-stub.js exists — so scripts/tests/transcript-css-entry-filter.test.js can pin both separators. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtufhms2u4 * fix(export): latch a stylesheet failure ahead of the parser block The `<link id="transcript-stylesheet">` sits in `<head>` while the `window` error listener that catches its failure is registered by an inline script in `<body>`. Chromium parser-blocks that script on the pending stylesheet, so when the CSS failure settles first the error event is dispatched with no listener to receive it: nothing marks the render as failed, both renderComplete guards in document-main.tsx pass, React mounts the transcript without any of the component CSS, and the requestAnimationFrame stamps `data-render-complete="true"`. The reviewer measured this fail-open above roughly 2.1 MB of document HTML (272 of the 1,000 permitted blocks) for a 404, an SRI rejection, a truncated body and a destroyed socket alike, and fail-closed for a *late* failure — so size, not failure kind, decides it. Latch the failure in `<head>` before the `<link>` is parsed and act on the latch from the existing body IIFE. The head script only records: `showLoadError()` writes `document.body.dataset` and `#app`, neither of which exists while the parser is still in `<head>`. It carries `nonce="__EXPORT_NONCE__"` because the document CSP allows no inline script, which is safe — `formatters/html.ts:53` replaces every occurrence. The listener is capture-phase because resource error events do not bubble. Not the `link.sheet === null` variant: the reviewer measured `sheet` non-null for a 404, a truncated body and a destroyed socket, so it only detects SRI rejection. scripts/tests/export-transcript-document-template.test.js pins the position, the nonce, the capture phase and the record-only shape; all five cases go red against the unpatched template. The behavioural witness (real Chromium, large document, instant CSS abort) belongs to the playwright transcript gate, which is out of budget on this host. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtufhms2u4 * fix(scripts): name the missing export renderer assets, pin the CSS gate The bundle copy became all-or-nothing over two artifacts but its `else` warning still named only the renderer, so the one new way to reach that branch — a tree built before the split, then `npm run bundle`d without rebuilding web-templates, which has the JS and no CSS — told the operator to go looking for a `export-transcript-document.js` that was sitting right there, and silently discarded it. List the paths that are actually absent, matching the sibling Web Shell warning twenty lines above. Stays warn-and-skip: prepare-package.js is the release gate. Also pin that release gate. Every fixture that reached `preparePackage` staged `dist/export-transcript-document.css` unconditionally, so deleting the new required-path entry left the whole test:scripts lane green; a release built with `npm ci --ignore-scripts` would then publish documents whose stylesheet 404s on unpkg for that version. `verifyBundleArtifacts` reports through console.error + process.exit(1) rather than a throw, so the new case stubs exit instead of copying the audio-capture sibling's `toThrow` idiom. Both cases were flip-checked: restoring the old warning text, and deleting the CSS line from prepare-package.js, each turn their case red. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtufhms2u4 * fix(export): pin the stylesheet-failure id contract and sync the design docs The <head> latch, the body listener and the <link> each spell 'transcript-stylesheet' independently and nothing compared them, so renaming either listener's id left the whole suite green while the latch recorded nothing - reinstating the fail-open the latch was added to close. Derive the id from the <link> and assert both listeners compare against it. Verified red under both mutations: latch id -> 'transcript-renderer' (1 failed | 5 passed), and the mirror with the body listener's id wrong and the latch intact (same). Both design docs still specified the two shapes the previous round replaced: the forward-slash-only onLoad filter that never matches on Windows, and the body-listener-only fail-closed extension. Section 1 now quotes the shipped TRANSCRIPT_CSS_ENTRY_FILTER and names transcript-css-entry.mjs, section 2 describes the <head> latch (position, nonce, capture phase, record-only), section 3 names the module-level render guard, and "Files affected" lists the three omitted files. EN and zh-CN are updated in the same commit. Also correct the shape-guard comment in build.mjs: the document nonces every <style> created through document.createElement, so the CSP would not block an un-stripped duplicate, and a 367-byte regrowth stays inside both byte budgets. That throw is the only guard on the duplicate-injection path. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtuo29vduf * fix(export): close out transcript CSS review comments * fix(export): tighten transcript CSS closeout Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
1097b9fe0a
|
fix(web-shell): move scheduled task row icon into trailing meta slot (#11356)
* fix(web-shell): drop the per-row scheduled-task icon in the sidebar The row-level calendar icon sat left of the group header icon, so scheduled-task sessions read as outside their group; the group header already carries the calendar marker. * fix(web-shell): preserve scheduled task row interactions * fix(web-shell): keep touch taps on session rows * test(web-shell): cover scheduled task row statuses * fix(web-shell): keep session row actions reachable on touch and focus The hover-gated reveal left session actions unreachable on hoverless devices, and the meta-slot width reservation no longer matched the reveal conditions. Always reveal the overlay under (hover: none) with the tap-through guard intact, key the focus reveal and reservation on container focus instead of :focus-visible, and merge the forked marker hide list into one rule. Pin the stylesheet invariants in a css source test and the coloured-row marker slot in the workspace-removal suite. * fix(web-shell): keep session row markers visible on touch devices Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(web-shell): preserve touch markers after actions --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
b1ac3e2970
|
docs(daemon): add a REST integration entry point, guarded against the route surface (#11477)
* docs(daemon): add a REST integration entry point The daemon REST surface is documented — 3.5k lines of route reference plus 21 deep-dive pages, all published. What is missing is the way in. An integrator lands on a 237-route surface with no signal about which routes are meant for them, and `users/overview.md` answers "how do I start using the CLI", not "I am putting this inside my product". Add one page that answers the second question: - Which of the six daemon integration paths to take, and what each one leaves the integrator owning. - The 25 routes a REST integration actually uses, grouped by job and linked to their reference sections — against 237 registered, the rest being Web Shell internals that change with that UI. - The minimal flow end to end: preflight, create, subscribe, prompt, answer permissions, close. - Two facts that are load-bearing at design time and currently buried: the daemon spawns `qwen --acp` children so the CLI must be on its host, and the auth model is single-operator with no per-end-user principal. This deliberately links rather than restates. The reference stays the authority; 17 of the 25 routes have a dedicated section there and the page says plainly which 8 do not, rather than papering over it (#11359). Refs #11427, #11359 * test(serve): guard the REST guide's promises against the route surface The guide tells integrators a specific 25-route subset is what they should build on, and links 17 of them into the protocol reference. Both claims are hand-maintained against a 237-route surface, so a rename upstream would leave the page promising a route that no longer answers, or pointing at an anchor that no longer exists, with nothing failing. Four checks, same shape as capabilities-docs-contract.test.ts: - every route the guide names is still registered somewhere under serve/ - every protocol-reference anchor the guide links resolves to a real heading - the 8 routes the guide marks "no dedicated section yet" are exactly the 8 that lack one, so writing a section forces the note to be removed - the source walker still sees >100 routes, so the first check cannot pass vacuously if registrations move to a style the regex misses Scope is the guide's own promises, not an inventory of all 237. A full coverage ratchet would need a ~174-entry "known undocumented" baseline; that is bulk in service of a problem nobody has reported, and can be added if the undocumented set starts moving. Verified statically against this tree: all four pass, and the walker sees 213 routes. Refs #11427, #11359 * docs(daemon): correct the process model and the memory flags Review on #11477 caught two accuracy defects and two smaller ones. All four verified against source before fixing. **The process model was stated backwards.** The page said each session's agent gets its own process and a crash is contained to one session. It is one child per *bridge*, and one bridge per *workspace runtime*: "At most one `qwen --acp` child per bridge… Multiple sessions multiplex onto the child via `connection.newSession()`" (bridge.ts), and the single `channel.exited` cleanup "tears down ALL multiplexed sessions" (ensureChannel). `daemon/00-index.md` and `users/qwen-serve.md` already said so. The container-sizing advice was wrong in the same direction, and "crash contained to one session" inverted the fault domain — it is the workspace. Rewritten, with the real lever named: run separate daemons, since `--max-sessions` caps concurrency, not blast radius. **The memory row read as enforcement.** Both flags are observe-only: `--memory-budget-mb` "does not change how any `qwen --acp` child is sized", and `--child-heap-mode` reports a modelled partition where "Nothing is applied: no child is sized from the budget and no spawn is refused". **Dropped the `stopReason` enum** rather than repeating it. Source has `end_turn`, `cancelled`, `error`, `max_tokens`; `refusal` and `max_turn_requests` also exist in packages/ but not in the protocol doc, and the `length` the protocol doc lists appears nowhere in source. Copying a list that is already wrong upstream would make a fourth copy of it — the page now links to the route section instead. The upstream enum still needs fixing (#11359). **Dropped the hardcoded "237 routes".** It disagreed with this PR's own test — 237 counts method+path pairs, the walker counts 213 distinct paths — and volatile totals in prose are what capabilities-docs-contract.test.ts exists to discourage. The point stands without a number. Contract assertions re-verified after the edits: 0 unregistered routes, 0 broken anchors, the undocumented set is still exactly 8, walker sees 213. * docs(daemon): correct the stopReason values in the protocol reference The list under `POST /session/:id/prompt` was wrong in both directions. It gave `end_turn`, `cancelled`, `max_tokens`, `error`, `length`. Against source: - `length` appears nowhere in `packages/`. It is not an ACP `StopReason` and the daemon never emits it. - `max_turn_requests` and `refusal` were missing. Both are in the ACP union (`@agentclientprotocol/sdk` `schema/types.gen.d.ts`: `StopReason = "end_turn" | "max_tokens" | "max_turn_requests" | "refusal" | "cancelled"`), so they arrive from the agent even though qwen-code never constructs them itself. - `error` is real but is ours, not ACP's — `create-sub-session.ts:695` and `live-session-coordinator.ts:1659` set it when a turn fails inside the daemon. - `reconstructed_from_transcript` was missing entirely (`prompt-terminal-ledger.ts:295`), for turns recovered from persisted history. Rewritten to separate the ACP values from the two the daemon originates, and to say plainly that the field is an open string — it is typed `string` on the wire (`bridgeTypes.ts:1258`), the ACP set can grow, and a client that exhaustively switches on it breaks on the next addition. That last part is the guidance the old list quietly worked against. Found while fixing #11477's own copy of this list, which is now a link rather than a fourth transcription of it. Refs #11359 * docs(daemon): correct six daemon claims in the REST guide and protocol reference Each claim below was checked against the code at this head and was stated the wrong way round. - qwen-serve-protocol.md: `turn_complete.data.stopReason` never carries `error` or `reconstructed_from_transcript`. A daemon-side failure publishes `turn_error` with its `code`/`errorKind`; the two `stopReason = 'error'` sites are SSE consumers' local variables (create-sub-session.ts:695, live-session-coordinator.ts:1659), and `reconstructed_from_transcript` is a ledger record (prompt-terminal-ledger.ts:295) that surfaces only as `promptTerminals[].stopReason` on POST /session/:id/load. The ACP five and the open-string instruction are kept. - SDK row: the Python SDK has no daemon transport at all (sdk-python.md:15-16), so the row names TS and Java and states the gap instead of implying it away. - Step 3: `model_switch_failed` is the only signal on the attach path; a fresh create also returns `modelApplied` in the 200 body (bridge.ts:5755-5816 -> session.ts:3563). The attach return object has no such field (bridge.ts:9828-9845) and swallows the switch deliberately (:9794-9806). - Step 5: the permission timeout defaults to 0 = wait indefinitely (bridge.ts:2726, commands/serve.ts:623-628); it does not "fire". - Step 5 + Permissions table: lead with the session-scoped vote route. POST /permission/:requestId is wired to primaryBridge only (routes/permission.ts:107-121, server.ts:3286-3287), so it 404s for a session owned by another registered runtime, with the same body as a lost vote. - Operations memory row: `--memory-budget-mb` has a live consumer — the adaptive live-journal growth pool (daemon-memory-budget.ts:19,37,38 -> run-qwen-serve.ts:3802-3812 -> bridge.ts:2912) — so it is not observe-only. The route/anchor contract in rest-integration-docs-contract.test.ts is unchanged: no route renamed, no anchor link added or removed, and the session-scoped permission route stays outside the protocol reference so the pinned undocumented eight still holds. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtufhms2u4 * docs(daemon): correct the turn_error, promptTerminals, modelApplied and approval-mode claims Four review findings, one defect class: a documented claim the code does not back. Docs only — no product code touched. - `turn_error`: `data` always carries `message`; `code` only when the daemon classified the failure, and `errorKind` on none of the three named paths. A prompt rejected because the ACP child died mid-request carries neither (bridge.ts:2119-2120 conditional spreads, :1949-1956 extractErrorCode needs a `code` property, :2065-2071 classifyTurnErrorKind matches only 'terminated', status.ts:100-107 BridgeChannelClosedError has none). Teardown flush and deadline do carry `code` (:2434, :10303). - `promptTerminals`: reconciliation appends two record shapes and the interrupted one has no `stopReason` key at all, so the reader is keyed on `terminal` instead (prompt-terminal-ledger.ts:283-297). The field is omitted entirely when the ledger holds no evidence (:374) and is attached only for `load` (routes/session.ts:4471-4478). - The deadline paragraph four lines below named `errorKind: "prompt_deadline_exceeded"`; the publisher sets that value as `code` (bridge.ts:10300-10306). - `modelApplied`: assigned only inside `if (modelServiceId)` and spread only when defined (bridge.ts:5755-5766, :5816), so step 2's bare create body yields a `200` with no such key. - `permission_request`: the daemon pins no approval mode at spawn (acp-child-extra-args.ts:15-20, spawnChannel.ts:484-492), the settings default is ApprovalMode.AUTO (settingsSchema.ts:2905, config.ts:1642), and AUTO's filter skips requestPermission for the allowed class (Session.ts:12346-12354) — so auto-approval is the default posture, not a decision. An untrusted folder is forced to DEFAULT (config.ts:1646-1655) and /capabilities reports the vote policy only (routes/capabilities.ts:101-104). Verified: packages/cli rest-integration-docs-contract.test.ts 4 passed, and prettier --check clean on both files. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtusclhbul * docs(daemon): correct the REST integration guide's route, token and sizing claims * docs(daemon): correct REST integration review findings Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(serve): check guide tables and documentation targets Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
f6540d1994
|
fix(web-shell): make the workspace provider guard self-diagnosing and reload on root retry (#11421)
* fix(web-shell): make the workspace provider guard self-diagnosing and reload on root retry
The strict useDaemonWorkspace guard fails closed with a message that
cannot distinguish an absent provider from a duplicated module copy
(two DaemonWorkspaceContext instances in one page, seen in dev when the
module graph is refreshed under a live page), so the root boundary
showed a dead-end fallback that only a manual reload could clear.
The provider now registers a per-module-copy marker on first render,
and the guard's error states which of the three cases it hit: no
provider rendered, a provider rendered from a different module copy
(with both copy ids), or a provider from this copy rendered and the
consumer is outside its subtree. The standalone root boundary's retry
now reloads the page, since re-mounting the same broken module graph
would throw again.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(web-shell): restore spies in afterEach so a failing retry test cannot leak the console.error mock
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(web-shell): guard the root-fallback reload behind token survivability and sharpen copy diagnostics
Review round on
|
||
|
|
d8baa8730f
|
fix(core): release node-pty's conout worker after every PTY on Windows (#11313)
* fix(core): release the ConPTY host after every PTY on Windows On Windows every completed tool call orphaned one headless `conhost.exe` (~8 MB) for the lifetime of the CLI process: #11303 measured 347 of them (~2.8 GB) under a VS Code Companion session after 12 h, growing 1:1 with tool shell commands. `taskkill` (`windowsKillPid`, `performCancelKill`) owns the *shell* process. It cannot reach the pseudo-console host, which is a separate process bound to the HPCON handle this process holds, not a descendant of the shell. Only `ptyProcess.kill()` releases it — it is the sole caller of node-pty's `WindowsPtyAgent.kill()` -> `conptyNative.kill()` -> `ClosePseudoConsole()`. On a natural shell exit node-pty runs `_$onProcessExit`, which only flushes buffered data and destroys its sockets, so the pseudo-console stays open. Three teardown paths dropped the PTY without that call: - `disposeForegroundPtyResources` — the healthy path. Its `isPtyActive` guard correctly skips taskkill when the shell exited cleanly, and that is exactly the per-tool-call leak. - the background-promote settle path — which has already removed the pid from `activePtys`, so the process-exit `cleanup()` cannot reclaim it either. - `WebTerminalRegistry.release()` — skips `killPtyTree` entirely for a session whose shell already exited. Release the host in all three. This is narrower than #5892, not wider: it closes only the HPCON we opened, so unlike a `taskkill /t` it can never reach a third-party application (the #6067 collateral-kill regression). win32-only, deliberately — there is no ConPTY host elsewhere, and `UnixTerminal.kill()` would signal an already-exited, possibly recycled pid. Fixes #11303 * fix(core): release the ConPTY host without killing a recycled pid Follow-up to the previous commit, correcting how the host is released. `ptyProcess.kill()` does three things on Windows, and only two of them are wanted here. Besides `ClosePseudoConsole` and disposing the conout worker, node-pty's `WindowsPtyAgent.kill()` forks a helper to run `GetConsoleProcessList` on the shell pid and then `process.kill()`s every pid it returns. On the healthy path the shell has already exited, so `AttachConsole` throws, the helper dies with an uncaught error, node-pty's 5s timeout falls back to `resolve([shellPid])` — and we `TerminateProcess` a pid `ClosePseudoConsole` just freed for reuse. That is exactly the #6067 collateral-kill failure mode, and the previous commit would have fired it on every tool call. Drive the two teardowns directly instead, extracted to `conpty-host.ts`. The shape is checked before use and the release degrades to a no-op if node-pty's internals ever change, so a dependency bump can only bring the leak back, never a kill we did not intend. Verified unchanged in 1.2.0-beta.15. This also fixes a second leak found while tracing the first: node-pty runs a `worker_threads` Worker per PTY to read the conout pipe, and `ConoutConnection.dispose()` is likewise reachable only from `kill()`. That accounts for the 353 threads the reporter measured against 347 orphaned conhosts — one leaked worker each. `WebTerminalRegistry` now goes through the same helper rather than `pty.kill()`, via a new optional `releaseHost()` on `WebTerminalPty`. `performCancelKill` deliberately keeps using `ptyProcess.kill()`: it early-returns unless the shell is still running, so the console process list is real there and killing it is the intended tree-kill fallback for when taskkill fails to launch. * fix(core): make the ConPTY host release idempotent node-pty's native `PtyKill` looks its baton up by pty id and calls `ClosePseudoConsole` without removing the entry from its handle list, so closing the same pseudo-console twice is a double-free on an already-closed HPCON — undefined behavior in-process, not a catchable throw. The cancel path reaches teardown twice: `performCancelKill` runs `ptyProcess.kill()` (which closes the host itself), and the finalizer then runs `disposeForegroundPtyResources`. Without a guard the release added here would have turned that into a crash. Track released PTYs in a WeakSet, and have the two sites that call node-pty's own `kill()` — `performCancelKill` and the process-exit `windowsStrategy.killPty` — record it, so a later release is a no-op. `performCancelKill` keeps using `kill()` on purpose: it early-returns unless the shell is still running, so node-pty's console-process-list lookup resolves for real there and killing it is the intended fallback for a taskkill that never launched. * test(core): cover the Windows release branch, which CI never reaches `Test (windows-latest)` is skipped on PRs, so the web-terminal release test only ever ran its POSIX half — the win32 assertions were dead code in CI, on the platform the whole fix is about. `web-terminal-registry.test.ts` now steers `os.platform()` (the only thing conpty-host reads; `killPtyTree` branches on `process.platform`, so it is untouched) and asserts both halves: the host and conout worker are released on win32, and nothing is touched elsewhere. The `os` mock passes everything else through, since Storage — reached via debugLogger — needs the real `homedir()`/`tmpdir()`. The shellExecutionService tests already mocked `os.platform`, so those were exercising the win32 paths on Linux runners. * docs(core): stop claiming the cancel-path kill() proves the shell is alive performCancelKill runs `spawnSync(taskkill /f /t)` immediately above this call, by design, so on the normal cancel the shell is already dead when kill() runs and node-pty's console-process-list lookup takes the 5 s `[innerPid]` fallback (windowsPtyAgent._getConsoleProcessList has only a message listener plus that timeout) instead of resolving for real. The comment asserted the opposite invariant, which would mislead the next maintainer on a file already flagged as revert-correlated. Comment only, no behaviour change. kill() stays as the fallback for a taskkill that never launched, and noteConPtyHostReleased stays paired with it so the finalizer does not close the same pseudo-console twice. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtrow8ztpw * test(core): pin the ConPTY release placement and the worker-dispose guard Two coverage gaps in the #11303 release path, both measured green before: - the release in firePostSettle sits above the `!postPromote?.onSettle` early return on purpose, but no test told the two placements apart: sliding the call one statement down kept the whole suite green while every promoted shell whose caller passes postPromote without onSettle leaked a conhost and a conout worker. The existing onData-only test now asserts the release fired. - the try/catch around `_conoutSocketWorker.dispose()` had no test at all. It is load-bearing because of where the release runs: in finalize()'s finally, immediately before activePtys.delete(pid) and after the result already settled. An escaping throw skips that delete and leaves a finished pid registered for the process-exit `taskkill /f /t`, against a pid Windows may have recycled. The new case drives dispose to throw and asserts a following cleanup() does not tree-kill that pid. Both run on Linux (the platform is mocked), so neither is a win32 skip. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtrow8ztpw * fix(core): close the two remaining ConPTY release gaps on Windows Three review findings, all on the release path this PR added: - Attach the post-promote settle listener unconditionally. It was gated on `postPromote`, so a background promote that passed no handlers dropped the pid from activePtys, disposed the foreground exit listener, and left nothing that could ever release the conout worker or the ConPTY host. Only the caller forwarding stays gated: firePostSettle early-returns on `!postPromote?.onSettle` after the reap and the release, so the PR-2 detach-everything contract still holds for callers that did not opt in. Attaching the 'error' listener unconditionally also keeps a post-promote pty error from being emitted on an EventEmitter with no listener. The PR-2.5 compat test now pins 2 onExit registrations and still asserts no data listener and no caller callback. - Stop recording a ConPTY release that never happened on the cancel path. WindowsTerminal.kill() runs its whole teardown through _deferNoArgs, which queues it until `_isReady` — set only on the conout socket's first data byte — so a cancel before the shell's first output byte queued a teardown that may never run while noteConPtyHostReleased permanently suppressed the finalizer's release. The note now sits inside the try, after kill(), and is skipped when the teardown was deferred; reading the optional `_isReady` degrades to the previous behavior if the field is ever renamed. - Pin the activePtys invariant directly in the dispose-throws test instead of only through the process-exit taskkill argv, so the conout dispose guard keeps a witness if windowsStrategy.killPty is ever refactored. Tests: 145 passed in src/services/shellExecutionService.test.ts. Each new or changed assertion was mutation-checked red against the unfixed source (restore of the `if (postPromote)` wrapper, unconditional note, and removal of the dispose guard in conpty-host.ts). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmts86p0gqr * docs(core): stop certifying a ConPTY host close that never happens Review found that `releaseConPtyHost` cannot close the pseudo-console on any of its three call sites, and the finding was confirmed independently against the exact pin. At `@lydell/node-pty` 1.2.0-beta.10, the native exit-watcher thread in `src/win/conpty.cc` erases the pty baton *before* it delivers the JS `onExit`, and `PtyKill` skips `ClosePseudoConsole` when `get_pty_baton` returns null — silently, so not even the warn fires. `struct pty_baton` has no destructor, so the erase leaks the HPCON rather than closing it. Every call site here runs strictly after `onExit`. So the split is: - `_conoutSocketWorker.dispose()` is pure JS and genuinely frees the worker thread. That is the 353-threads half of #11303, and it is real. - `_ptyNative.kill()` is a no-op. The `conhost.exe` half — the dominant term the reporter measured — is NOT fixed. The unit tests cannot see the difference, because `_ptyNative.kill` is a `vi.fn()`: the assertion passes on a call that does nothing on Windows. Comments and names only, no behaviour change. Corrected here: the doc block, both call-site comments, the `releasedHosts` rationale (which after this finding applies only to the cancel path, where the shell is still alive and the baton still exists), the version clearance (the JS field names were re-checked on beta.15, the native semantics were not and did change at beta.14), the warn text, the web-terminal comments, and the test names and describe block that read as certifying the host. The call itself stays: the call site is right, and it starts working unchanged the moment upstream closes the HPCON when the baton is erased. Until then the only mitigation for the host half is `tools.shell.enableInteractiveShell: false`. * fix(core): release the conout worker on the remaining Windows release paths Address the closeout findings on the ConPTY worker release: - conpty-host.ts: stop gating the conout worker dispose behind the native-kill shape check — a node-pty bump that renames _pty/_ptyNative must not also lose the only teardown that frees anything today. Correct the doc block's two overbroad claims (the 'error' entry point can reach a live baton, and enableInteractiveShell only gates the shell-tool path). - web-terminal-registry.ts: release the conout worker on the live branch too. killPtyTree's pty.kill() defers its whole teardown while _isReady is false, so a terminal released before its first output byte stranded the worker. The wrapper now notes the close only when kill() really ran, so release() never double-closes a pseudo-console kill() already closed. - shellExecutionService.ts: fix the settle-path comment (it released only the worker, not the host), name the settle-time taskkill reap the unconditional attach adds, correct the 'error'-listener rationale (node-pty's listeners('error').length < 2 throw), and qualify the three "PR-2 detach-everything" statements that the unconditional attach now falsifies. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtsxwmdgrw * fix(core): format web-terminal-registry.test.ts to satisfy prettier Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtt2wnugs4 * fix(core): avoid double-close on web-terminal deferred live release The web-terminal live-release path called releaseConPtyHost even when node-pty's kill() was still queued in _deferreds (the shell had not yet emitted its first byte), closing the same HPCON a second time. Route that case through the new disposeConoutWorker so the queued kill() stays the single closer, and correct the two now-false invariants in the comments. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmttf1wj0so * docs(core): reattach the releaseConPtyHost contract block to its function Inserting disposeConoutWorker between the contract block and releaseConPtyHost orphaned it: the block closed with */ and a new /** started on the very next line, so TypeScript resolved the leading trivia to disposeConoutWorker and releaseConPtyHost documented as nothing. The export all three cross-file call sites invoke (shellExecutionService.ts:1871 and :2142, web-terminal-registry.ts:321) had no hover doc. Move disposeConoutWorker and its own JSDoc above the contract block, so the block sits immediately before releaseConPtyHost with a blank line on each side and neither reads as a continuation of the other. The block is relocated verbatim -- the caller enumeration in it is the text a resolved thread already ruled on. Leave a one-line pointer in the disposeConoutWorker doc for the #6067 and win32-only rationale rather than duplicating them. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtu0hicwtk * docs(core): make the releaseConPtyHost caller enumeration match the call graph The three-group enumeration omitted the web-terminal release of an already-exited session and filed the cancel path wholly among the note-suppressed sites. Verified against the real call graph: - release() reaches session.pty.releaseHost?.() from two arms -- :464 for !session.exited and :472 for session.exited -- and both land on the same releaseConPtyHost(spawned) at web-terminal-registry.ts:321. The exited arm runs strictly after onExit, records no note, so it is a member of the no-op group, not the note-suppressed one. It is the primary web-terminal path for #11303. - releaseHost branches only on _isReady (:317), never on session.exited, so the disposeConoutWorker route at :318 is reached from BOTH arms, not just the live one. - performCancelKill records the note only when _isReady !== false (shellExecutionService.ts:2418), so a cancel that lands before the shell's first output byte reaches the finalizer at :1871 with no note. - windowsKillPid's interactive-shell kill at :619-623 notes unconditionally and was unlisted. That is six distinct production states reaching releaseConPtyHost plus two routing to disposeConoutWorker; the previous text named six entries total and mis-classified two of them. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtu0hicwtk * test(core): witness the throw guard in disposeConoutWorker Its twin inside releaseConPtyHost already had a dedicated test, but this one had none: disposeConoutWorker's only call site is web-terminal-registry.ts:318 and the sole test reaching it used a bare vi.fn() for conoutDispose, so deleting the try/catch left the whole suite green. The guard is load-bearing because release() calls session.pty.releaseHost?.() bare (:464/:472), after the session is already deleted from the map (:445), and dispose()'s loop (:422-426) has no per-iteration guard -- an escaping throw would abort teardown for every session still queued behind it. Mutation-verified: with the try/catch deleted, 1 failed | 27 passed; intact, 28 passed. Runs on the Linux lane because the file mocks node:os platform, so this is not a skip-on-CI witness. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtu0hicwtk * docs(core): state both arms of the deferred releaseHost, and test the exited one The deferred branch's rationale was written only for the live release, where killPtyTree has just queued a kill() in node-pty's _deferreds. But release() reaches releaseHost from two arms -- :464 for !session.exited and :472 for session.exited -- and releaseHost branches on _isReady alone, never on session.exited. So a shell that exits before its first output byte (COMSPEC resolving to a binary that quits immediately, or a releaseWorkspace drain racing pwsh startup) lands in the same branch, where no kill() ran, nothing is queued, and the double-close argument does not apply -- the native baton is already erased by the exit-watcher, so a close would no-op rather than double-close. The worker is still the resource to free, which is what the else arm's own comment says it is there for. Split the comment into LIVE and EXITED so each arm carries its real reason. Add the missing case: _isReady = false, then onExit({ exitCode: 0 }), asserting conoutDispose once and kill/nativeKill/spawnSync never. Per conpty-host.ts the test asserts the worker dispose and the ABSENCE of the native kill, never that the conhost half was freed. Mutation-verified: deleting the _isReady === false branch turns the new case red (3 failed | 26 passed); intact, 29 passed. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtu0hicwtk --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |