mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-10 17:40:17 -10:00
feat/btw-qodercli-alignment
484
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
444ff228ee |
fix(subscription): restore model switching and align session recovery (#16738)
* fix(cli): restore model switching after subscription logout * fix(cli): mention /effort after subscription sign-in * docs(serve): guide new setups through subscription sign-in * docs(users): remove obsolete Qwen OAuth troubleshooting guidance - Remove the error message about the Qwen OAuth free tier ending on April 15, 2026. - Remove the explanation of the error and the steps to resolve it. - Remove related guidance on using API keys and Alibaba Cloud Coding Plan as authentication alternatives. - Remove SSL certificate error messages and troubleshooting steps. * fix(auth): align subscription settings scope with API key setup * fix(subscription): use gateway model order at sign-in Send the running Qwen Code version in model catalog requests so the gateway can identify client versions. Select the first returned model on explicit sign-in. * feat(subscription): use gateway model metadata * fix(subscription): restore startup models and sync desktop defaults * fix(subscription): align workspace startup and draft model handling * fix(subscription): preserve gateway model order in pickers * fix(subscription): align draft startup and history recovery * test(cli): update subscription success rendering expectations --------- Co-authored-by: luoyun <[email protected]> |
||
|
|
fa40879b4a |
feat(channels): protect the GitHub mention lane from watch budget spend (#16701)
* feat(channels): protect the GitHub mention lane from watch budget spend The PR state watch runs before the notification lane in every poll and is best-effort, so when it spends the REST budget down to a rate-limit answer the interactive mention handling in the same poll is what fails. The channel was also budget-blind on the success path: x-ratelimit-remaining was read only after a request had already been rejected. A single request hook now records the remaining REST budget and its reset time from every successful response, regardless of the call's return shape (it sees each paginate page too). When the last known remaining is at or below a configurable reserve (prWatchBudgetReserve, default 200, 0 disables), the watch pass yields for that poll so a human mention keeps the budget; a stale reading whose window has reset does not gate. The skip and the recovery are journalled as edge-triggered degraded rows, and the reactive fail-fast backstop is unchanged. The reserve is wired through the descriptor, validation, the Web Shell editor (Bound Sessions panel, EN/zh labels), and the timeline labels the new recovery reason as recovered rather than degraded. A reserve of 0 short-circuits the gate outright so an exhausted budget (remaining 0) cannot skip a watch the operator opted out of protecting. Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): release the watch budget latch when the watch loses eligibility An adversarial audit pass reproduced a suppressed starvation report. The low-budget latch was only cleared inside the branch that runs when the watch is eligible, so a binding refresh that started failing while a skip was latched left the flag set: the budget could recover with no recovery row, and — worse — the next genuine starvation after the watch returned was swallowed as a repeat of an episode that had already ended. The timeline's last budget word then named a skip no longer in effect, which contradicts the documented promise that both the skip and the recovery are recorded. Release the latch when the watch block is skipped wholesale, mirroring the sibling watch-guard cause. Claim recovery on that path only when the budget really recovered: the watch being ineligible says nothing about the budget, so a still-low budget releases silently and the next eligible low poll journals a fresh skip. Two cases pin it: recovery across an ineligible window, and a second starvation after that window still journalling. Disabling the release reds both; making the recovery claim unconditional reds the second. The design docs also named a watchBudgetCause field that the implementation never had; corrected to the watchBudgetLow latch in both languages. Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): close the review round on watch budget awareness Seven review findings plus three converged suggestions, all addressed here. The reserve was only checked at the poll boundary, so a pass entered just above it could spend the whole reserve before the notification lane ran — the starvation the reserve exists to prevent, and it widened as prWatchLimit rose. The pass now re-reads the budget before each watched PR and breaks, journaling the same low edge through a shared helper; break rather than return so the baseline observed so far still reaches savePrWatchState. A low-budget episode could still end with no recovery row: the skip latch was released during an ineligible window, so a budget that recovered while the watch stayed ineligible never journalled its closing edge. Episode tracking is now separate from the skip latch — the latch still releases so a later starvation journals a fresh skip, while the episode stays open until the BUDGET recovers. Both flags are in-memory, so the guide now says the recovery is recorded while the channel stays connected instead of promising it unconditionally. "Skipping only delays detection" overpromised: watchOnePr compares against the stored baseline, so a transition that appears and reverts between two passes is never observed, and a closed gate widens that window. Narrowed in the user guide, both design docs and the code comment. The timeline labelled the recovery row but rendered none of the low row's payload, so 199/200 and 12/200 looked identical. It now renders both numbers in EN and ZH. Coverage gaps closed: a mention delivered in the very poll whose watch was skipped (the feature's central promise, previously untested); the recovery row asserted payload-free on both emission sites; an episode closed by window reset rather than a climbing count; and the hook driven through a REAL multi-page paginate, which is the per-page coverage the design claims. Also: reject a negative reserve at the validation boundary instead of letting the getter silently default it; hoist RECOVERY_REASONS to module scope; drop the new disabledHint keys, which no editor branch reads. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
25e69c0f69 |
feat(daemon): expose last user prompt time and session sorting (#14654)
* feat(daemon): expose last user prompt time in session summaries * fix(daemon): keep last user prompt metadata hot * feat(daemon): support last-question session sorting * test(ci): align session sort capabilities and admission waits * test(managed-agent): use the managed store in row lock test * fix(daemon): isolate prompt-time reads and gate sorting on presence An unreadable transcript must not fail a whole session list or the status route: the reader deliberately throws when a snapshot is replaced mid-read, so degrade the single row instead, keeping abort semantics intact. The SDK forwarded any defined sortBy while only pre-flighting the non-default value, so `sortBy: 'updatedAt'` reached daemons whose query allowlist and strict catalog schema reject the parameter itself. Gate on presence. Also parse the cursor before enriching, so a cross-sort cursor is rejected without scanning every transcript, and drop the pagination cursor when a sort change discards the loaded pages. * fix(sessions): keep question sort consistent across hosts and pages (#14654) Fan-out host groups now read their own stored sort and only ask for question ordering when that host advertises it. Organized pagination keeps an unknown-time live-only row reachable on later pages, and the sort control falls back to the projects header when the brand row is hidden. Add witnesses for the warm-cache ownership gate, the numeric-timestamp case, and the bridge notification/registration wiring. * fix(sessions): pin question-order paging and drop unrelated Java churn (#14654) The metadata page cursor now has a witness that ties its boundary key to sortBy instead of activity time. Restore two runtime-broker files to the merge-base content so the PR no longer carries unrelated line wrapping. * test(core): pin that the numeric-timestamp case really parses The cache reset added for this case is itself unobserved: removing it leaves the whole reader suite green, because the assertion is then satisfied by the previously cached undefined snapshot. Assert the index was rebuilt once, so dropping the reset turns the case red instead of silently making it vacuous. * fix(sdk-typescript): keep sortBy session lists on native REST (#14654) The ACP session/list mapping drops sortBy, so a client with an ACP transport received updatedAt order after the capability pre-flight succeeded. Force native REST whenever sortBy is requested, mirroring liveOnly, and pin the routing with a DaemonClient test. Also cover archived question-time sorting in the CLI session-list suite, where the archived transcript is the source of truth. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: yungsen.hys <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
c503584221 |
docs(serve): state the worktree lock key rule and the restore's lock-mismatch 500 (#16601)
* docs(serve): state the worktree lock key rule and the restore's lock-mismatch 500 The ownership lock key is derived by the restore, the reset route and the delete-path orphan cleanup, and the restore also compares it with the checkout path its validation resolves. The rule they must share (JS fs.realpathSync of the recorded worktree path; the restore and the cleanup fall back to the raw path, the reset refuses an unresolvable one) is now stated with acquireWorktreeOwnershipOp, and each site points to it. The protocol reference and the session lifecycle page now say what the design document says: a later Part 4A sidecar read that names a checkout the restore did not lock fails closed with a 500 that carries no code, instead of a typed restore 409 or an attestation, and a retry classifies under the lock once its reads agree. The design document's superseded-restore bullet gains the same exception. * docs(design): add the Chinese version of Channel Named Sessions Part 4B docs/design/README.md requires both language versions of a design, and adding the missing one whenever a single-language design is updated. Part 4B had only the English version and this branch updates it, so add the complete Simplified Chinese translation of the current English and link the two versions below their titles. Translating it showed that the English "Sidecar schema addition" paragraph called `supersededBy` the forward link and `supersedes` the reverse link, the opposite of every other section; it now names the fields instead. * docs(serve): list the restore's worktree 500 under load/resume errors The load/resume Errors list named no 500 for worktree validation, so a client enumerating the branches it must handle would not find it. Add a bullet saying what happens to a session restored before the failure, that these bodies carry no daemon-defined code, and which causes a retry resolves. The Part 4A paragraph points to it. * docs(design): list every reader of the worktree supersede links The Part 4B "Sidecar schema addition" paragraph said only the restore and reset paths read `supersededBy`/`supersedes`. The shared worktree-context restore, the exit_worktree tool's stale-marker check and the delete-path orphan cleanup read `supersededBy` too, and the guards they back keep a resumed superseded session and the cleanup off the replacement's checkout. Name them in both language versions. The #11024 requirements list read as binding rules, but the cleanup built from it departs from them: only `supersededBy` refuses, because refusing on `supersedes` too left every post-reset worktree uncleanable. Its intro now says the list is the review's inputs and points to channel-worktree-orphan-reap.md for the rules as built. * test(serve): pin the restore lock-mismatch 500 body and detach branch The protocol pages say the lock-mismatch 500 carries no `code` and that an already-live session is detached rather than closed. Assert the missing `code` in each lock-mismatch case, and drive a lock mismatch on an attached restore: the request's client is detached, the session is not killed and the sidecar stays. |
||
|
|
e12a6bc2d1 |
feat(web-shell): add workspace collapse controls and drag sorting (#16197)
* feat(web-shell): add workspace sidebar controls and shared ordering * fix(web-shell): address CI baseline and repeated expansion reads * docs(serve): index workspace-order REST endpoints * fix(web-shell): address workspace sidebar review findings (#16197) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: 钉萁 <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
b577cbdbbe |
fix(channels): journal GitHub mentions added by editing an older comment (#16588)
GitHub notifies the bot when an edit adds its @mention to an older comment, but the comment lanes read only comments created after the poll cursor, so the notification was marked read with no trace. The bot still does not answer such an edit. The channel now journals an inbound/skipped event with the reason edited_comment, once per window, for a listed comment created at or before the cursor and updated after it that mentions the bot, is not the bot's own, and is not already in the channel's dedupe record. Dispatch, the cursor, the dedupe state and mark-read are unchanged. Document it in the GitHub channel guide, including that an edited issue or PR body is answered only on first contact, and list the new reason in the channels overview, the serve protocol reference and the channel events timeline design (English and Chinese). |
||
|
|
7fd185883f |
docs(tools): add You.com free MCP to the web search MCP services (#14000)
Adds You.com to "Supported MCP Web Search Services" in docs/developers/tools/web-search.md, after the SerpApi entry, in the same format as Firecrawl/SerpApi: description, Documentation/Cost/Get API Key/Best for bullets, Available Tools, and setup (CLI, settings.json without a key, settings.json with a key). One new section; nothing else on the page changes. Links use bare URLs (no UTM parameters) to match every other entry on the page. Rebased onto current main so it applies cleanly after the SerpApi entry. Fixes #13987. Co-authored-by: valueadd-lgtm <[email protected]> Co-authored-by: 易良 <[email protected]> Co-authored-by: Brian Sparker <[email protected]> |
||
|
|
edaa4e3826 |
feat(agents): make default background execution configurable (#15178)
* feat(agents): add configurable default background execution Signed-off-by: ZedingZhang <[email protected]> * fix(agents): clarify configurable background guidance * docs(agents): align execution guidance with configurable defaults --------- Signed-off-by: ZedingZhang <[email protected]> |
||
|
|
1150e89c37 |
docs(daemon): document session-scoped permission vote for channel auto-approve (#16541)
The permission auto-approve sequence diagram now shows session.respondToSessionPermission for daemons advertising the session_permission_vote capability, and keeps the legacy session.respondToPermission branch for hosts that do not advertise it. Part of #10849 (finding rv:5090771085, deferred from PR #10793). Patrol-Run: qwen-issue-patrol/jmw6cw7jo0v Co-authored-by: yiliang114 <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
5b44cfad71 |
fix(web-shell): include channel sessions in release dialog (#16537)
* fix(web-shell): include channel sessions in release dialog The release dialog inherited the sidebar's default source filter, which excludes sourceType: "channel". A workspace filled by idle channel sessions therefore opened an empty recovery list. Query the workspace catalog without a source filter for this dialog and keep the default, channel, and qwen-live rows. The sidebar and every other useScopedSessions caller keep the default filter. Add hook/dialog regressions and verify banner -> release -> create with a real sourceType: "channel" session filling a one-session workspace. * fix(web-shell): keep live sessions visible in release dialog Expanding the recovery list from the sidebar's default source filter to persisted history can still bury the sessions holding slots: closed channel rows fill the first page before they are reached. Add a liveOnly session-list option on the daemon route and SDK so callers can read the workspace bridge instead of persisted history. The release dialog requests the shared page size with that option and keeps listing default, channel, and qwen-live rows, so closed history can no longer crowd out what can be released. Co-authored-by: Qwen-Coder <[email protected]> * fix(sdk): serve liveOnly session lists over native REST The ACP session/list mapping drops liveOnly, so a DaemonClient configured with an AcpHttpTransport silently returned persisted history for the release dialog's live-only request. Route liveOnly through the native REST transport for both the legacy and workspace-qualified list methods, and cover the real ACP transport with a regression test. Co-authored-by: Qwen-Coder <[email protected]> * test(web-shell): pin the release dialog's source whitelist Live-only listings can include side_task, scheduled_task, and standalone sessions that the server does not hide. Add those rows to the dialog test so widening the whitelist fails, and state the 100-row live-page ceiling next to the page-size request. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
1853b1505f |
fix(subscription): isolate activation and restore sign-in recovery (#16453)
* fix(web-shell): wait for session before checking subscription commands * fix(cli): keep signed-out subscription startup in the composer * fix(subscription): preserve model selection across sign-in * fix(subscription): align sign-in with model selection scope * fix(subscription): apply reasoning defaults across models * fix(subscription): tag Qwen web links by client * fix(subscription): prefix Qwen web client tags * fix(subscription): attribute Qwen web links by paywall scene * fix(subscription): recover historical sessions with saved API models * fix(cli): remove the signed-out subscription startup notice * fix(subscription): support standalone desktop activation and expose errors * fix(subscription): isolate activation and session model selection * fix(subscription): enforce auth policy and restore home login * test(cli): preserve the default initializer mock on restore --------- Co-authored-by: luoyun <[email protected]> Co-authored-by: 易良 <[email protected]> |
||
|
|
1b1f4455d9 |
ci: build the PR tree once and shorten the required path (#16450)
* ci: build the PR tree once and shorten the required path The no-AK integration gate, the four web-shell smoke shards and Lint & Static each rebuilt the tree test_build already uploads, and the build cache rarely helped: most runs logged "29 of 29 workspaces changed" because the arch-keyed cache was split across the mixed x64/ARM64 pool and a prefix restore returned the newest entry instead of the base commit's. - test_build restores the build its base commit saved (ci-build-cache.mjs key --at), drops runner.arch from the key, and runs the serve fast-path closure and core subpath checks against the built tree. - The no-AK integration gate unpacks that tree and runs as three shards; integration_no_ak_result keeps the required check name. - The web-shell smoke unpacks the same tree instead of building; consumers recopy the opentui native assets for their host (host-assets). - The standalone runtime regression moves from unit shard 1 to shard 4. - A java_only profile skips the TypeScript lanes for changes confined to the Java packages; Lint & Static runs the scripts/tests suites that name them. - Drafts skip the integration gate and the browser smoke; ready_for_review starts the full run. * ci: check the settings schema in test_build, which has the core dist Lint & Static only had core's dist because the serve fast-path check built it; with that check moved, Generate settings schema failed to import it. * ci: keep the standalone regression on unit shard 1 Moving it did not shorten shard 1: the regression runs in the side lane, in parallel with the CLI and core suites, and shard 1's time is its CLI slice (576 s against 147 s on shard 4 for the same 181 files). That imbalance is left to duration-based sharding. |
||
|
|
a051a6a796 |
fix(web-shell): batch session deletions at the route limit (#14637)
* fix(web-shell): batch session deletions at the route limit * test(web-shell): cover bulk delete in visual previews * fix(web-shell): retain bulk delete interruption errors * fix(web-shell): retain deletion failures across workspace batches Collect per-session and request failures throughout a deletion without losing later causes or earlier successful current-session cleanup. Keep archive behavior unchanged and document the daemon request-limit coupling. Add gated regressions for both cross-workspace failure orders and multiple per-session errors across batches. The new gates reject parallel-workspace and per-workspace-error-reset mutations. * test(web-shell): cover bulk delete merge boundaries Preserve workspace and standalone failure causes for per-session and rejected requests. Verify each deletion batch captures its UI owner before the request and invokes its own invalidation callback with that batch result. --------- Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
ff775787f5 |
feat(channels): journal private bound-session delivery and scan health (#16237)
* feat(channels): journal private bound-session delivery and scan health The event timeline could not tell the private delivery lane's story. A prompt injected into a PR-bound session ended on the same `running` word as an event still in flight, no task row named the session it reached, and the three private watch retires plus the dead-letter left only stderr behind — so a silent consumption looked like a quiet repository. A bound event rejected by the channel's own gates was worse than invisible: its skip row was followed by running rows, reading as a delivery. The private delivery method returned a boolean meaning "settled", which conflated an injected prompt, a private retire and the public fallback. It now returns a verdict, and the task runner derives the journal's terminal word from it, so an event that was never delivered can no longer be recorded as delivered. Routing, fallback, retry-budget and cursor decisions are unchanged. The binding scan is a new hard dependency: while it has never succeeded the cold-start gate stalls every notification, yet the poll reports ok. Degraded rows now surface that beside the stderr lines the adapter already throttles, and on the edge of the existing watch-reconcile flag, so a permanent outage cannot flood the journal. Attaching this to the poll detail was rejected: both cold-start gates return before the detail is set, which is exactly when it would matter. No new event type and no protocol change — an outcome is already an open string. The dead-letter's message body and its failure-path payload dump stay out of the journal. Also labels `failed_ambiguous`, which was already written but rendered as a raw token. * fix(channels): make the bound-delivery journal claims true Review round 1 found sixteen places where the journal, its labels or its documentation promised something the code did not do. All sixteen are addressed here; none is deferred. Three were ordering or attribution defects in the new rows. A watch retire was journaled before the removal it describes, so a throwing removal left a terminal claim for an event the next poll re-runs and retires again under a different reason. Rows written while a delivery is cleared lost the session they were about, which is the one fact the private lane's attribution adds. And the pre-claim watch retire sliced the loaded state before saving with no rollback, so a failed save lost the event with no trace at all — the comment claimed the opposite. Two were counts that measured something other than their label: the partial scan published the routing map's size, which counts bound PRs, as a session count; and the streak advances per binding scan, not per poll, because a gone-session retry refreshes again inside the same poll. The session count now comes from the scan's own rows, and the streak is labelled as scans in both languages and in the stderr lines that share the counter. Re-cadencing the counter itself would change the throttle timing the previous PR shipped, so the label moved instead. Two were missing surfaces: the pending-queue-overflow retire wrote only stderr and the dead-letter file, and an unreadable watch state was reported as a pending backlog whose stated remedy cannot clear it. The overflow now journals a body-free skip, and the watch guard classifies a read failure separately while its gate stays fail-closed. The recovery signal the prose promised did not exist: a poll/ok row is written after every poll that does not throw, including all of an outage, so it cannot mark the end of one. The watch guard now journals its clearing edge, and the prose says why ok rows cannot serve. The rest were documentation and test-coverage corrections: the protocol reference promised detail.streak on every degraded row when only the binding-scan rows carry it; the design file map certified the protocol reference as unchanged while this branch edits it; the override's stated consequence was backwards; and five guards had no witness or only half of one. Each now has a test, and the suite grew from 438 to 444 in the GitHub package. Verified by 37 mutations, all killed. Two initially survived and were investigated rather than accepted: a loose sessionId count that three unattributed-row-free rows still satisfied, and a filter that matched no test after a rename. Both were tightened until the mutation died. * docs(channels): align the journal ledger with the round-2 verification The validation section still cited the round-1 mutation count after the review fixes added fifteen more, and the Chinese half had lost the count altogether during the rewrite. Both now state 37 applied and 37 killed. Also records why two mutations initially survived and what was done about them, since a sweep that cannot distinguish "no test ran" from "the guard held" is not evidence: a loose sessionId count that three correctly attributed rows still satisfied, and a filter left matching nothing by a test rename. The Web Shell line now mentions the absence coverage too. * fix(channels): keep the lost bound session named on every journal row The second review round of the bound-session journal found fifteen gaps. Three were consequential, and two of those were introduced by this PR's own previous round: The gone-session retry never updated the local delivery, so with two followers both gone the terminal row named the first one — a session evicted a step earlier — while the row above it named the last. An operator reading "which session lost this event" would open the wrong workspace. Remembering the target only for the transition that clears the delivery is not enough: once the clear is persisted, every later poll re-enters the absence gate with no delivery to read, and the dead-letter at the deferral horizon reads a record that has none either. A driven run of that sequence left 239 of 242 rows unattributed, with the terminal row naming nobody — the one fact this lane's attribution exists to supply. The lost target is now persisted as attribution only; the delivery is deliberately not restored, since that would re-enter bound delivery and change routing. The overflow retire was journaled before its queue shift was durable, with no rollback, so a failed save left a skip row for an event a restart would later deliver. It now saves before it journals and restores the shift when the save throws, matching the sibling pre-claim path. Also: the partial-scan session count now includes the sessions retention keeps routing (a draining workspace lists none while still delivering to three), and both pre-claim retires carry the frozen target so the timeline and the dead-letter agree about the same event. Four coverage gaps whose mutants left the whole suite green are closed: the guard keeping the partial-scan row off the cold-start path, the session on the no-candidate retire, a second healthy poll so the recovery row's edge trigger is distinguishable from an unconditional write, and both a shape test and a Chinese render test for the interpolating labels. The docs said more or less than the code: detail.sessions is carried by one row, not by every partial scan; the normative sessionId definition read a retire chip as a delivery; the emit-point inventory missed the overflow site; and bindings_partial was described as recovering implicitly when a successful multi-page scan is its producer. Auditing that last correction found the stderr census was still incomplete — the lane has five recurring health and capacity sites, not three — so it now names all five, ranks them by consequence and states its own scope. * docs(channels): define what the partial-scan session count actually counts The count became listed-plus-retained when retention was included, but three surfaces still described the old value. The protocol reference called it "the sessions the map still routes to", which is wider than the code: the map also skips channel-owned sessions and unparseable binding URLs. Both design halves still said it was "the scan's own row count", which is narrower than the code and was the exact wording the review asked to correct. Define it as a session-row count — the rows this scan listed plus the sessions retention keeps routing, bounded by the retention cap — and say explicitly that it is not the number of entries surviving the map's own filters. Also record that "listed" in the row label and in the stderr line is shorthand for listed-or-retained and that both print the same number, so the wording cannot quietly drift apart from the value again. Documentation only: the adapter and its tests are byte-identical to the previous commit, so every runtime result reported for this branch still applies. * fix(channels): close the review gaps in the private-lane journal Address the eight open suggestions and the five deferred findings from the round-3 review, each with a mutation-verified guard (10 new mutations, 10 killed; one cap-count fixture initially survived because the first empty partial already capped lastBindings, and was rebuilt so retention is above the cap exactly at the emitting scan). - R1-15: the overflow dead-letter append and the sidecar save now fail independently. A failed save rolls the shift back, names the watch-state path instead of blaming the dead-letter file, drops the payload dump (the body is already durable), and remembers the head so the next detection skips the re-append. The save-throws test now drives a second detection and asserts exactly one dead-letter line. - R2-6: the pre-claim skip comment and both design languages now say only the overflow retire writes a dead-letter record; watch_no_candidate and no_role_matched leave the skip row as the event's only trace. - R2-3: bindings_cold_empty no longer claims implicit recovery — an empty partial is a successful scan, the streak resets only on a complete or non-empty snapshot, and past the escape the silence is the throttle maxing out, not recovery. - R3-1: a 1100-session fixture pins detail.sessions to the 1024 retention cap at the emitting scan; swapping retainedCapped.length for retained.length reds it while the two sibling count tests stay green. - R3-5/R3-6/R3-15: the design's outcome cell lists failed_ambiguous and degraded, the watch-guard bullet admits the read-failure error field, and the sessionId carve-out covers every row written on a clear plus the pre-claim skip; the chip sentence says reached-or-lost. Both languages. - R3-7: the three private-retire watch tests assert the public path stayed empty; leaking the envelope through handleInbound reds them. - Deferred: the gone-horizon attributes the in-memory task before retiring and the dead-letter record names the lost session; the watch-guard row re-journals when the blocking cause changes; the subscriptions-off recovery edge has a test; the recovery row renders Recovered/已恢复 instead of Degraded (wire outcome unchanged); a gone-retry fixture pins the scan-scoped streak at 10 inside poll 4. Verified: channels/github 595 passed, web-shell channels 343 passed, eslint and prettier clean; design mutation ledger updated to 47/47. * fix(channels): rework the overflow retire ordering and close round-4 review Critical R4-1: the overflow branch saved the sidecar mid-pass, committing the advanced baseline before the pass's later events were queued; a crash then never re-derived them. Remove the mid-pass save entirely — the head is sliced in memory only, and the baseline and full queue commit together in the single end-of-pass save, with the skip rows journalled after it. The dead-letter record is now the retire's commit point. A lazily loaded, file-backed key set (restart-durable, replacing the process-local map) skips the re-append (R4-16), and both drains recognize a dead-lettered head and complete its retire instead of delivering it, so no event is both delivered and filed as retired undelivered (R1-15 follow-up). Also addressed: - R4-12/13/14: terminal, transition lost-session and pre-claim rows carry sessionWorkspaceCwd so Open-session reaches a session in another workspace. - R4-11: the no_role_matched comment states the real ordering — the dedupe cursor is in memory until the base saves it, so the skip row is at-least-once. - R4-15: already_delivered_other_role is documented as a defensive, unreachable guard and dropped from the published vocabulary. - R4-2/3, R3-5: §1 outcome cell adds suppressed/posting, reason covers task and poll rows, sessionId carve-out adds cancelled and distinguishes live running rows. Both languages. - R4-4/5/6: the pre-claim census acknowledges the role-aware drain's pre-existing fourth retire; the retire-carries-session rationale notes the preflight exception; the dead-letter write-failure note names the non-recoverable public-suppression exception. Both languages. - R2-3/R4-7: cold-empty reset is keyed on the routing map and trust flag, and the escape's lane-open claim is qualified for the refresh-failure producer. - R4-8/9: the recurring-site census adds the credential/rate-limit pass abort and corrects the no-existing-flag claim for the stale-map refresh failure. - R4-10: watch-guard error wording covers reconciliation failure too. - R4-17/18/19: openTarget joins the interpolation guard with a ZH render assertion, the EN streak label says consecutive, and ZH degraded/retired/ recovered labels get render assertions. Verified: channels/github 601 passed, web-shell channels 331 passed, eslint and prettier clean; 4 new mutations killed (mid-pass save, file-dedupe, drain guard, workspace lookup); design ledger updated to 51/51. --------- Co-authored-by: verify-round <verify-round@local> Co-authored-by: verify-pr <verify@local> |
||
|
|
738c4bbf7e |
fix(serve): bound command snapshot replay memory (#16413)
* fix(serve): bound command snapshot replay memory Suppress unchanged command snapshots and retain only the latest full skill details so periodic refreshes cannot grow daemon heap indefinitely. Bound replay rings by serialized bytes while preserving ACP live delivery and reconnect IDs, and expose retention diagnostics. * fix(serve): avoid redundant replay seed serialization Size only retained complete command snapshots when seeding replay history, and forward the replay byte limit through the development daemon launcher. Pin snapshot accounting and lifetime eviction counters with independent regression assertions, and document the count/byte retention bounds and conditional command notifications. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
2666e3552d |
fix(core): make persisted session pagination safe for equal mtimes (#16024)
* fix(core): make persisted session pagination safe for equal mtimes
listSessions() ordered files by mtime alone and resumed the next page with a
strict `mtime < cursor`. An mtime is not unique — bulk copies, backup restores
and coarse-timestamp filesystems all produce ties — so a group of files sharing
an mtime that straddled a page boundary was dropped while hasMore still reported
progress, then the next page came back empty: 25 files at one mtime with size 5
showed 5 items in total.
Sort on (mtime desc, name asc) so the order is total, and let the cursor carry
that position: nextCursor is now `"<mtime>|<name>"`. A bare number is still
accepted with its existing strict `<` meaning, so callers holding a legacy
cursor keep working, and the daemon's opaque string cursor now passes the
composite form through instead of rejecting it as invalid.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmvwpo7jxrk
* fix(cli): carry the composite session cursor type through callers
listSessions() now hands back a "<mtime>|<name>" cursor so a page boundary
inside a group of equal mtimes does not drop sessions. The widened
`number | string` contract left the CLI callers that store or compare that
cursor typed as `number`, which is a type error in `tsc --build` even though
nothing changes at runtime.
Widen the three consumers to `number | string`: the daemon's persisted scan
cursor, the live coordinator's paging plus its loop guard, and the session
picker state used by both the ink and opentui pickers. No behaviour change.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmvwrtdc1rm
* fix(cli): route composite session cursors to the persisted listing
The persisted listing mints `"<mtime>|<name>"` cursors when a page ends
inside a group of files sharing an mtime, but the route still chose between
the persisted and the live listing path with a numeric-only predicate. A
composite cursor therefore fell through to the live path, whose parser only
accepts numbers, and the second page answered 400.
Export one predicate from session-list.ts carrying the same shape test the
cursor parser already applies, and use it for that routing decision.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmvwtyj3bro
* fix(session-list): keep composite cursors usable by ACP and VS Code clients
listSessions mints "<mtime>|<name>" cursors whenever a page ends inside a
group of files sharing an mtime, but two consumers still parsed every cursor
as a number: ACP session/list rejected page 2 with "not a valid numeric
cursor", and the VS Code companion turned the composite cursor into NaN so
hasMore read false and only page 1 was ever listed. A malformed "|name"
cursor also slipped through the persisted-cursor classifier and answered 200
with an empty page instead of invalid_cursor.
- acpAgent: accept and forward "<mtime>|<name>" cursors to listSessions
- session-list: require a non-empty mtime prefix before classifying a
cursor as persisted
- vscode companion: pass the daemon cursor back verbatim, widen the cursor
type and treat only a non-empty cursor as "append to the current page"
Tests: ACP composite passthrough + empty-prefix rejection, VS Code
getSessionListPaged composite nextCursor round trip.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvxke6bm6i
* fix(vscode): keep composite session cursors type-safe in the companion
|
||
|
|
da7af136e5 |
feat(skills): expose native archive routes and receipts (2/4) (#16422)
* feat(skills): add file-backed archive installation kernel * feat(skills): expose native archive routes and receipts |
||
|
|
9b1ce46d77 |
feat(serve): add atomic user Skill activation (#16186)
* feat(serve): add atomic user Skill activation * test(serve): cover conditional user Skill activation capability * docs(serve): index user Skill activation endpoint * fix(serve): preserve User Skill refresh recovery |
||
|
|
ecc64a735a |
feat(qqbot): deliver proactive loop messages as active group messages (#15635)
* feat(qqbot): deliver proactive loop messages to the group target
* fix(qqbot): fail blocked proactive deliveries and post loop output once
- pushProactive now raises ChannelProactiveDeliveryError when
sendMessageWithReplyContext reports a blocked outgoing route: the
blocked send resolves instead of throwing, so a loop delivery that
never reached the wire was being recorded as delivered. The block's
disposition is kept so the scheduler can distinguish a permanent
route failure from a retryable one; ACTIVE_MSG_DISABLED still throws
unwrapped from the send path.
- QQ's streaming chunk sink is skipped for unattended turns (loop and
webhook task, identified by the new protected isProactivePromptTurn
predicate), so the full answer is posted once through pushProactive
instead of being streamed to the group first and posted again. Base
chunk bookkeeping, lifecycle events and attended streaming are
unchanged.
- Tests: permanent and transient route blocks reject the loop and never
send; streamed chunks of an unattended turn produce exactly one send
carrying the full response.
* fix(qqbot): keep permanent delivery errors permanent and document proactive scope
- pushProactive rethrows QQ's permanent DeliveryError codes
(RETRY_EXHAUSTED, ACTIVE_MSG_DISABLED, FALLBACK_FAILED) as
ChannelProactiveDeliveryError('permanent', ...) so the daemon boundary
classifies a group that cannot receive active messages as rejected
instead of retryable. RATE_LIMITED stays transient.
- Drop the threaded-target guard copied verbatim from the base override;
every entry point already gates on supportsProactiveTarget and QQ never
produces a thread id, so the copy was unreachable drift.
- Name all four callers in the pushProactive doc comment (loop, webhook
task, background/named-task response delivery, daemon deliverProactive)
and state that background responses intentionally go out unanchored now.
- Document QQ in the proactive-delivery capability matrix and in the QQ
channel guide: active messages, group quota, ACTIVE_MSG_DISABLED and
the 5-failure auto-disable, no threaded targets, learned chat type
required.
* feat(qqbot): retire loops for chats that can no longer receive proactive messages
A loop created before a group disabled active messages, or before the bot
was removed from the group, kept firing: each run rebuilt a session, ran
a full unattended agent turn, and could never deliver. Both chat-wide
state changes now retire the chat's loops.
- base: add an optional chat-scoped controller lookup (listForChat) and
protected disableLoopJobsForChat(chatId), which disables every enabled
loop in a chat regardless of the creator's exact target; the CLI
controller implements the lookup over the loop store.
- qqbot: handleGroupDelRobot folds the disabled count into its existing
cleanup log line; handleGroupMsgToggle disables the group's loops when
active messages are turned off (both handlers become async and are
dispatched with an explicit rejection log).
- qqbot: refuse proactive targets for a group whose active-message
toggle is known to be off (supportsProactiveTarget), so loop creation
is rejected up front and a firing refuses before burning an agent
turn; direct messages never consult the group toggle.
- tests: creation refusal via the loop tool, run refusal with the prompt
never called, removal and toggle-off teardown against the real
controller, DM exemption, mid-flight toggle classification, and the
base helper's contract.
* test(qqbot): keep the loop-store integration test honest
- Pin a rendered next-fire ISO timestamp instead of base's 'n/a'
fallback: the copied controller now delegates nextFireTime the same way
the production controller does, and the store clock is fixed so the
value is deterministic.
- Redirect QWEN_HOME at the test's tmpdir before the channel constructor
runs and assert the channels directory lands inside it, so the test no
longer creates ~/.qwen/channels on the machine running it.
- Resolve the session through the locally constructed SessionRouter
instead of reaching the protected ch.router, which was a TS2445 that
no gate in the repo catches (test files are excluded from tsc).
* fix(qqbot): close the round-2 proactive delivery findings
- Resolve the target chat type through chatTypeFor() instead of
chatTypeMap, and refuse an explicit group target whose type is unknown,
so config-declared groups and never-learned groups are gated on the
same type resolveRoute routes on instead of burning an agent turn that
cannot deliver.
- Route background/named-task responses through pushProactive
unconditionally: a toggle-off group must not silently receive them
through the anchored passive reply path.
- Extract one PERMANENT_DELIVERY_CODES/isPermanentDeliveryError predicate
and use it at the proactive seam and the five existing classification
sites, preserving each site's behaviour.
- Fix the two comments that named a scheduler disposition check that does
not exist (the daemon classifyChannelDeliveryError is the only reader)
and word the blocked-route error as 'outgoing route unresolved
(<disposition>)'.
- Include the group openid in group-event failure logs and pin the
websocket dispatch rejection guard with a corrupt-store test.
- Stop retiring a group's loops when its active-message toggle turns
off: the toggle is reversible, the pre-flight gate already prevents the
turn burn, and the scheduler's 5-failure auto-disable retires the job.
Retirement stays on bot removal, where the gate cannot see the change.
- Pin the permanent/transient arms with FAILED-fallback and RATE_LIMITED
tests.
* test(cli): pin the chat-scoped loop lookup
listForChat decides which loops chat-wide teardown retires, and the QQ
suites substitute hand-rolled controllers whose filter mirrors it. Test
the real controller over a real ChannelLoopStore seeded with two chats
and two channel names, so dropping the filter (or only its channelName
clause) reds instead of silently retiring unrelated users' loops.
* docs(qqbot): record the proactive delivery contract
- Add the design record in English and Chinese covering the capability
flip and the four base call sites it activates, the single-delivery
rule for unattended turns, permanent/transient classification, chat
retirement on bot removal, and the group active-message toggle
contract.
- Complete the opt-in adapter list (DingTalk Workspace, Email with its
proactiveRecipients qualifier) and state QQ's target restrictions and
the unanchored background-response behaviour.
- Document isProactivePromptTurn in the base hook table and the
adapter-author proactive delivery section.
---------
Co-authored-by: Shaojin Wen <[email protected]>
|
||
|
|
6611b8aeb2 |
fix(cli): land a Web Shell update on the current latest release (#16374)
* fix(cli): land a Web Shell update on the current latest release The daemon pins its update target when it prepares. Once the state is `ready` the check returns the cached status even for `?refresh=true`, so polling never re-downloads — and Web Shell prepares automatically on mount. For a long-lived daemon the prepared release is therefore whatever was latest shortly after it started. Clicking Update restarted into that stale target, and every release published in between needed another prepare-and-restart round, which reads as advancing one version per click. The CLI resolves and installs at each process start, so it always reached the current latest in one step. Re-resolve the release once at activation, after the restart has been acknowledged, and stage the newer one when it is higher and one-click covers the installation. Both the standalone and the managed npm paths share one resolver, so eligibility cannot drift between the check and the click. Readiness stays pinned and background polling still never re-downloads; only the explicit click re-resolves. A failed re-resolve or staging applies the prepared release instead, so a registry failure cannot strand an explicit update request on the running version. The activation guard still preserves an equal or newer active installation, and a failed activation of the newer release cleans both payloads. Staging runs after the acknowledgement and can take minutes, during which the daemon keeps answering. The browser now measures its give-up deadline from continuous unreachability rather than from the click: a reachable daemon is still working, and the old click-time budget would have reported a failure while the update was in fact succeeding. The protocol reference and both language versions of the update design doc state the pinned-readiness contract and the new deadline semantics. * fix(cli): keep an update restart observable and its payload committed An audit of the activation-time re-resolve found three defects in it. Discarding the superseded payload ran before the newer one was committed, so a failed discard fell back to a payload that might already be half removed and orphaned the one just staged, losing the update entirely. Commit to the newer payload first and discard the old one best-effort: a failed discard now leaks disk instead of losing the update. Dropping the browser's click-time deadline had also removed its only overall bound. A daemon whose process replacement does not land on the new build comes back reachable and settled on the same version, and the control then waited forever for a version change that would never arrive. Restarting is sticky until the process is replaced, so an acknowledged restart followed by any other state with an unchanged running version now terminates the wait and reports a failure. That path publishes the settled state itself, because the poll immediately after it can drop while the daemon is still coming back, which would otherwise leave a stale restarting status rendering no control at all. The rule is gated on the restart having been acknowledged: polling starts on the click, before the request resolves, and can still observe the pre-click state. Browser fixtures now model the daemon's contract — restarting from the acknowledged restart until the process is replaced — because a fixture that keeps reporting a prepared state after the click cannot exercise that distinction. Assertions are unchanged; only the stimulus became faithful. Also pin the edges the re-resolve depends on: a prerelease below the prepared release, a registry rollback, build metadata that does not change precedence and a malformed registry value must none of them restage, and an in-flight restart must survive the operator disabling automatic updates, concurrent polls and retries, and ordinary shutdown. Every mechanism added here is mutation-witnessed, including the two halves of the deadline change and the acknowledgement gate. * fix(cli): sample the restart acknowledgement when the poll starts The settled-state termination read the acknowledgement ref after awaiting the status request, which does not establish that the response was sampled after the acknowledgement. The status and restart requests are independent, so the first restart poll can be issued while the restart is still pending and deliver its pre-restart snapshot after the 202 arrives. The ref is then true, the stale snapshot looks like a daemon that settled without updating, and the control reports a false failure and clears the restart — after which a genuine version change is handled by the idle branch, so the page never reloads and the captured task URL is never restored. Sample the flag when the poll is initiated instead. A response delivered after the acknowledgement by a poll started before it is now ignored, and the next poll — initiated afterwards — is the first one allowed to conclude the replacement did not land. The regression test holds the status request open, acknowledges the restart, then delivers the old snapshot, and requires no error plus a successful reload once the new version is reported. Also restore an absolute backstop, which the move to a continuous- unreachability deadline had removed. A daemon that stays reachable but never leaves restarting — a hung activation or process replacement — would otherwise leave the control disabled forever with no error, and a page reload hides it entirely. The cap sits above the daemon's own staging ceilings (five-minute archive download, ten-minute managed npm child) so a slow but progressing update never reaches it, and a test pins both halves: the wait survives fourteen minutes of a reachable restarting daemon and ends at the cap. Both the protocol reference and the two language versions of the design doc state the sampling order and the backstop. * fix(cli): log why a click fell back, and keep the entry after giving up Three review findings on the update restart path. A registry outage at click time left no trace. The release check resolves an error result rather than throwing, so the fallback returned silently and "the registry said nothing newer" was indistinguishable from "the registry was unreachable" — both install the older prepared release. Log the classified failure before falling back. The catch below only ever sees detection or staging throw, so it now names restaging instead of sending the reader to the registry. The test that covered this used a rejected check promise, a shape the real function does not produce for an outage; it now resolves the real error result and asserts the log, and a second test pins that a staging failure is not reported as a registry failure. Giving up took the update entry with it. While the wait runs, each successful poll publishes the daemon's state, so a wedged daemon leaves it at restarting; the give-up branch cleared the restart without publishing anything, and the render gate does not display restarting, so the entry unmounted and could not return while the daemon kept reporting it. The user got one notification and then no affordance at all. Every give-up path — both deadlines and a daemon-reported error — now keeps a retryable entry on screen. The design sentence claiming the control is never hidden was untrue of these paths; it is now true of all of them. The wedged-daemon test asserted only that the spinner was gone, which a vanished control also satisfies, so it now asserts a visible enabled entry. The absolute cap was sized against a ceiling the code does not have. The standalone archive download tries OSS and then GitHub sequentially with a five-minute budget each, and the checksum and signature fetches add up to two more minutes, so one restage can spend about twelve minutes downloading and verifying before activation — not the five the docs and the constant's comment claimed. That left the fifteen-minute cap about three minutes of headroom while the text promised a slow update would never reach it. Raise the cap to twenty minutes and state the real chain in the protocol reference, both design docs and the comment. |
||
|
|
b87d60ddf0 |
fix(serve): serve qualified hooks status for untrusted workspaces (#16388)
* fix(serve): serve qualified hooks status for untrusted workspaces The workspace-qualified hooks status GET sat behind the trusted-runtime gate alongside env/preflight, so with folder trust enabled an untrusted workspace got 403 exactly when the Web Shell approval card needed the pending decision. The card treats the failed read as no hooks and never renders, leaving no interactive way to approve. Route the hooks read through the same trust-free resolver as the qualified approve POST; env/preflight keep the trusted gate. Fixes #16387 Co-authored-by: Qwen-Coder <[email protected]> * test(serve): pin pending hooks decision for untrusted workspace Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
0cc37bf444 |
feat: add Qwen subscription support across CLI, VS Code, and Desktop (#16378)
* feat(subscription): add development login, models and web search * feat(subscription): refine account flows and host integration Align desktop and VS Code subscription UI, add account logout and confirmed plan display, and map subscription thinking parameters. Remove simulation code and obsolete native authentication paths, consolidate design documentation, and cover cancellation and recovery boundaries. * fix(subscription): preserve request errors and remove unused compatibility * fix(subscription): refine command visibility and client logout flows * fix(subscription): simplify account actions and desktop sign-in feedback * docs(subscription): clarify sign-in and provider guidance * feat(subscription): support switching accounts across clients * fix(subscription): align account actions and open plans on demand * fix(subscription): restore model selection after logout Filter subscription-only commands consistently across terminal renderers, preserve model catalog error classification, and display the subscription authentication name consistently. * fix(subscription): unify status authentication labels across clients * fix(subscription): preserve provider connections across client logout Restore configured providers after subscription logout across CLI, Desktop, and VS Code. Keep unrelated connections and conversation history, synchronize model state silently, and return to an unconfigured chat when no provider remains. * fix(subscription): correct native client logout state * feat(subscription): enable sign-in without development mode * refactor(ui): 优化会员登录成功提示和界面交互体验 - 移除订阅欢迎提示相关状态和行为,简化 UI 状态管理 - 替换登录成功提示为四行摘要,包含邮箱、套餐、模型及常用命令提示 - 更新国际化文本,支持新的登录成功摘要内容 - 取消订阅模式下输入框下方的命令提示行,改为在登录成功摘要中展示 - 移除多余的测试用例,新增登录成功消息的单元测试校验 - 优化 AppHeader 组件,不再显示订阅启动重复提示 - 调整 Composer 组件逻辑,删除订阅提示相关副作用及渲染条件 - 修改历史消息显示组件以适应新的成功提示格式 * fix(subscription): enable Flash reasoning controls in shared UI * fix(subscription): prioritize authorized models and simplify login dialogs * fix(subscription): preserve gateway errors and request IDs * fix(subscription): synchronize account state across clients Preserve model selections after sign-out, synchronize account commands and model catalogs, and retain explicit browser sign-in flows across CLI, Web Shell, and VS Code. Cancel subscription background agents on logout and normalize expired refresh credentials without SDK error wrapping. * fix(subscription): reset default model on explicit sign-in Choose the default served model when explicitly signing in to Qwen Subscription, preferring Max over a previously saved Flash selection. Preserve normal startup behavior and other provider flows, and cover CLI and workspace activation with regression tests. * fix(vscode): refresh models after successful sign-in * fix(web-shell): hide account actions during automatic sign-in * fix(web-shell): align model details and auth keyboard navigation * docs(subscription): revert previous guide changes * fix(core): reuse subscription catalog for same-login model switches * docs(subscription): document Qwen membership sign-in * fix(subscription): unify quota notices and preserve provider switching Carry structured subscription quota details through chat and search errors for localized CLI, Desktop, and VS Code display. Keep other providers available when subscription discovery fails and refine sidebar account actions. * fix(web-shell): align Chinese subscription entry title with CLI * fix(subscription): open plans for saved non-member accounts * fix(web-shell): show subscription commands after desktop sign-in * fix(subscription): reauthorize explicit account sign-ins * fix(auth): add model switching hint to subscription success * docs(auth): list Qwen OAuth as the first option * docs(models): simplify subscription sign-in guidance * docs(models): remove legacy OAuth from supported auth types * docs(subscription): link membership references and remove obsolete allowance note * fix(subscription): preserve session until reauthorization succeeds * fix(subscription): use pre for browser authorization and plans * fix(subscription): use production endpoints * test(vscode): fix editor message test formatting * fix(subscription): preserve unauthenticated session startup * fix(cli): classify subscription auth during ACP preflight * fix(subscription): scope activation to the current project --------- Co-authored-by: luoyun <[email protected]> |
||
|
|
bff144a528 |
fix(channels): keep GitHub notifications unread on transient thread errors (#16333)
* fix(channels): keep GitHub notifications unread on transient thread errors A notification whose comments, events or issue could not be read was skipped while the same poll still marked every notification read and advanced the cursor, so a GitHub 5xx, network error or rate limit on one thread lost its @mention for good, without a timeline entry. Keep the cursor and the unread mark when a thread fails transiently, so the next poll reads it again. Permanent answers (404, a SAML-protected 403) still skip the thread at once, and a thread GitHub keeps failing is skipped after 30 polls so it cannot hold mark-as-read back for every other thread. First-contact issue fetches follow the same rule, and every such failure is recorded in the channel event timeline. * fix(channels): keep exhausted GitHub retry streaks until the window commits Dropping a thread's failure counter when it reached the cap re-armed the same unread notification whenever another thread still held the cursor: two threads that started failing one poll apart kept resetting each other and the window was never marked read. Keep the exhausted streak until the window commits or the thread succeeds, so the hold ends once every failing thread has recovered or been skipped, and say so in the docs. * fix(channels): separate shared GitHub limits from per-thread failures Treat a rate limit or credential failure on one thread as what it is, an answer every thread would get: hold the window and stop reading the rest of the poll, without charging the thread's 30-poll budget, so a limit is not hammered once per notification every poll. Only a 5xx (Octokit also reports network errors as 500) counts against a thread's budget. A local exception without a status skips the thread again, as before, which keeps the inbound-task persistence guards observable on their own. A held window re-reads failing threads every poll, so report a streak in stderr and the event timeline when it starts, every tenth retry and at give-up instead of on every poll. Pin the shared-limit stop, the local-exception skip, the reset after a success and the clear on commit. * fix(channels): bound the GitHub notification hold per window, not per thread - A success reset a thread's 5xx streak while the window stayed held, so two threads failing in turn held mark-as-read and the cursor forever. The 30-poll budget now belongs to the window: on the 30th poll that sees a 5xx, the threads still failing are skipped and the window can be marked read. Rate-limited polls do not spend it. - A held window re-reads every thread and comment on each poll. Permanent failures and given-up threads are journaled once per window, rate limits on their first and every tenth stopped poll, and the duplicate and mention_required skips of a re-read comment once per window. - Tests stub the first-contact issue fetch they fall through to, so they no longer pass only in file order. * fix(channels): spend the GitHub 5xx budget only on a window that can commit - A thread left unread at the cursor (its mark-as-read never applied) with nothing newer to mark read held nothing, yet each failing poll spent the window budget, which then never reset; the next new notification was skipped on its first failing poll and its comment lost. Only a window with something to commit now spends the budget. - An error a failing task store raises inside a thread is journaled as retried, not skipped: the store failure holds the whole window and the thread is read again. - Pin the 500 boundary (Octokit's network-failure status) and the stderr throttle; list github_api_error among the Channel events skip reasons. * fix(channels): read only failed or changed threads while a GitHub window is held - A held window re-read every listed thread on every poll, so one failing thread cost a read of the whole window per poll, and a secondary limit set off by that volume stopped every poll at the same thread: the window could neither commit nor reach the notifications behind it. A thread read without error is now remembered with its notification's updated_at until the window commits; it is read again only once new activity moves that timestamp, so a hold re-reads the failing threads and a poll stopped by a rate limit resumes where the last one stopped. A failing task store or audit keeps the thread unremembered. - A notification listed again at or before the cursor (its mark-as-read did not apply) has nothing new to read; a failure reading it no longer holds the window or spends its budget. - The once-per-window journal keys stop growing at their bound instead of clearing, so only the overflow repeats. - Tests pin the budget surviving a failed commit, a 403 whose only signal is retry-after, and two repositories with the same issue number. * fix(channels): journal each GitHub thread outcome under its own window key - A retried store failure, a stale notification's skip and a real skip shared one once-per-window key, so whichever came first silenced the others: a thread could be skipped, and its comment lost at the commit, with the timeline still showing only the earlier row. Each outcome now has its own key. - Only a local error (no HTTP status) counts as held by a failing task store or audit; a 404 on another thread in that poll is a skip. - A permanent answer or a stale notification reads the same next poll, so the held window remembers it; a thread it gave up on is still read again. Stale failures no longer count toward the thread's failure milestones, and the per-thread counts share the per-window bound. - A stale notification that hits a rate limit still holds the window (the threads after it were not read); the doc now limits the stale rule to 5xx. * fix(channels): read cleanly read GitHub threads again on every held poll Skipping a thread that read cleanly while its notification timestamp stayed put lost data: the comment lane defers a first-contact body to the re-read after a failed comment turn, so the body was never fed; and with GitHub's one-second timestamps or a late notification, an unchanged updated_at does not prove the thread has nothing new, so a comment posted after the read was missed before the commit. A held window is read in full on every poll again, as on main; only threads that failed with a permanent answer or were stale, which read the same next poll, are not read again until the window commits. * fix(channels): read a GitHub thread again after an error without a status A thread whose read ended in an error without an HTTP status was settled for the held window like a permanent answer. Such an error can come from the channel's own state part-way through the thread: an unreadable pending-deliveries file throws after the first comment was claimed, with no persistence flag set, so the later comments of that thread (or its comment lane after a direct dispatch) were never read while the claimed task held the window, and were lost at the commit. Only permanent HTTP answers and stale notifications are settled now; any other failure is read again, as on main. * fix(channels): read every GitHub thread again on each held poll Settling a thread for the held window after a failed read lost items main delivers. A stale notification is not empty: a late notification at the committed cursor's second can carry a first-contact body or a direct event, neither bounded by the cursor window, and its read can fail on one poll and succeed on the next. A permanent HTTP answer can be short-lived too, such as the 404 of an issue that was just created. Every thread of a held window is read again on each poll, as on main; a stale notification's 5xx still neither holds the window nor spends its budget. * docs(channels): say what the GitHub failed and skipped timeline rows mean The journal contract listed inbound rows as accepted or skipped only; GitHub thread read failures now add a failed row (the window is held and the thread is read again) beside the skipped one (the channel stopped holding the window for it). Name both in the protocol reference and the channel guide. * fix(channels): share the GitHub rate-limit test with the watch pass A secondary rate limit sent as a bare 403 (quota left, no retry-after) stopped the notification poll but not the PR watch pass, which kept issuing one rejected pulls.get per watched PR. The limit's wording now lives in the shared predicate, so both stop on the same answers. A failing thread whose count no longer fits the per-window bound was reported on every poll; it is now reported once per window. A test pins that the hold budget starts again after a restart, and the journal helpers in the test file share one reader. * docs(channels): say what a github_api_error row means, not only its cause A failed row also comes from a local error while channel state cannot be persisted, the journal schema now lists the reason on inbound failures, and a rate limit stops a held window's poll at the first refused thread rather than reading it in full. * fix(channels): report a GitHub thread its own task still holds as retried A local error inside a claimed task's run, such as an unreadable pending-deliveries file, leaves that task running: the cursor waits for it and the thread is read again, yet the timeline called the failure skipped. The poll now checks the thread's own tasks, as the store was last read or written, and reports such a failure as failed until the task settles. The restart case now saves the cursor and loads it in a fresh instance, as a real restart does, so a hold count kept in the cursor would fail it. * docs(channels): keep the task store wording for the guide's first hold exception * docs(channels): state the bounds of the GitHub held-window journal rows A skipped row for a notification listed again at or before the cursor normally loses nothing, since its activity was read before an earlier window was marked read. The design page now lists the held-window causes as examples, including an audit it cannot read and a failing mark-as-read. The once-per-window rows are tracked under up to 8,192 keys, which the guide, the protocol page and both design pages now say, and a credential failure is journaled like a rate limit. |
||
|
|
de6ccb4e3c |
feat(serve): allow configuring the Conversations working-directory root (#16105)
* feat(serve): allow configuring the Conversations working-directory root * test(serve): register Conversations root E2E in no-AK CI * fix(serve): address Conversations root review feedback * fix(serve): clarify Conversations root configuration errors --------- Co-authored-by: Jiankai Xiao <[email protected]> |
||
|
|
2f4608c00b |
fix(web-shell): align welcome models and make UI language sessionless (#16297)
* fix(web-shell): align welcome models and make UI language sessionless * docs(serve): index workspace UI language endpoint * fix(web-shell): address sessionless language review feedback * fix(serve): preserve workspace language during deferred sync --------- Co-authored-by: luoyun <[email protected]> |
||
|
|
13e65c1a6e |
feat(web-shell): link GitHub PR/Issue targets in channel events (#16226)
* feat(web-shell): link GitHub PR/Issue targets in channel events The channel events page showed the repository#number target of GitHub journal events as plain text, so operators had to copy the reference into a browser to inspect the PR or issue behind an event. Chat prompts and the PRs panel already link out; this closes the last unlinked surface. The URL is synthesized in the client from the channel config and the journal fields the GitHub adapter already records, so existing journal history becomes linkable without any daemon or journal change. The origin mapping mirrors the adapter's web origin derivation (github.com, GitHub Enterprise /api/v3, ghe.com data residency), and links pass the same safety and desktop-opener routing as chat links. Non-GitHub channels, removed channels, and malformed references stay plain text. * fix(web-shell): link GitHub events of channels inherited from user settings The events page resolved link metadata from the management listing, which is scope-local: a channel configured in user/system settings with cwd pointing at the project (the setup the GitHub channel guide recommends) has journal rows on the page but no listing entry, so its targets stayed plain text. The journal route now attaches non-secret link metadata (platform type and API base URL only) resolved from the merged settings the workers actually run with, filtered by the same cwd-ownership rule the worker grouping uses so one workspace never sees another's channel metadata. Resolution failure degrades to unlinked rows instead of failing the journal read. The page falls back to this metadata when the listing has no entry, and a source contract test now ties the client origin mapping to the adapter's so the deliberate mirror cannot drift silently. * fix(web-shell): make channel event link metadata safe and authoritative Review of the journal-metadata round found the default settings loader leaking the queried workspace's .env into the daemon's process environment (and able to rewrite a corrupt settings file) on a read-only polling route, and the client preferring the scope-local listing over the merged metadata even though system settings override workspace values. The route now resolves metadata through readSettingsSnapshot on the runtime's effective environment — side-effect-free, corruption throws into the existing degrade path — and the helper requires that view instead of defaulting to loadSettings. The page prefers the journal's merged metadata, keeping the listing as the older-daemon fallback, derives the displayed number from the thread exactly like the URL does, and strips trailing slashes from the base URL on both sides of the origin-mirror contract. The serve protocol doc now documents channelLinkMeta. * fix(cli): resolve channel link metadata against the boot environment snapshot The serve process-env guard flagged the journal route's live process.env fallback: a read-only polling route must not couple to shared mutable process state. Thread the daemon's frozen boot-time environment snapshot through the route deps and use it as the parent-process fallback, the same source the session host resolution already uses, instead of reading the live environment or adding a guard exception. * fix(web-shell): emit the resolved GitHub web origin from the daemon Review round 2 flagged that the web-origin mapping had two owners pinned only by a source-scraping mirror test, and that the boot-environment guard had no test able to fail. Retire the second owner: the channel package now exports resolveGithubWebOrigin as the single mapping owner, the adapter and the journal route both use it, and the route emits the resolved origin as channelLinkMeta.webOrigin so the browser only concatenates. The client's baseUrl derivation remains solely as the older-daemon/listing fallback, and the scraping contract test is replaced by a mapping-owner table (including whitespace-padded env-resolvable values), route origin assertions, and emitted-origin consumption tests. Pin the boot-snapshot guard behaviorally: mount() accepts a daemonEnv override and a new case resolves channels from a boot home that disagrees with the process environment — reverting to a live process.env read now reds that case. Also call the exported resolveChannelOwnerCwd instead of re-implementing the ownership predicate, and correct the protocol doc's read-scope wording for the added settings snapshot. |
||
|
|
56fec7f3f5 |
ci: shard the Linux unit tests and web-shell smoke, and reuse cached builds (#16133)
* ci: shard the Linux unit-test lane eight ways behind one build The Linux Test job ran 45-59 minutes: a ten-minute prepare build, then the CLI and core suites back to back. A new test_build job runs the prepare build once and uploads its outputs; eight shards install without the build, unpack them and run --shard=N/8 of every Vitest suite. Test (ubuntu-latest, Node 22.x) becomes an aggregate that passes only when the build and every shard pass. * ci: record ci.yml's growth from the test_build prelude test_build carries the same pre-checkout cleanup, checkout and install prelude as the test shards; ci-platform-lanes.test.js pins the two copies identical. * ci: record the Linux lane's two new jobs in the PR-stage budget test_build and the test_ubuntu aggregate add two jobs; the declared timeouts drop from 305 to 295 minutes. * ci: shard the web-shell E2E smoke four ways The smoke lane ran 44-46 minutes on every recent PR, about 33 of them in the browser tests at two workers, and is the longest lane once the Linux unit tests are sharded. Each shard runs --shard=N/4 of the smoke; the transcript gate runs on the first. web-shell E2E Smoke (ubuntu-latest, Node 22.x) becomes an aggregate over the shards. * ci: bound the unit shards' tree download and retry it once A pool host stalled 17 minutes in download-artifact while the other shards took under a minute. * ci: rebuild only the workspaces a change reaches scripts/ci-build-cache.mjs hashes each workspace's tracked build inputs (tests excluded) with its workspace dependencies' hashes and the shared root inputs. test_build and the web-shell smoke shards restore the newest cached build and rebuild only workspaces whose hash changed. A push to main always builds from scratch and saves, so a pull request starts from a clean build of main. * docs(ci): describe the incremental build cache * ci: keep the build cache path the same on every pool host actions/cache versions an entry by its path; under runner.temp the path differs per host, so a run could never restore another host's save. * ci: save the ARM64 build cache from the smoke lane, and run the aggregates on the shards' runners The smoke shards run on the ARM64 pool, which test_build may not, so the first shard saves the clean build it made when its restore found nothing. The two aggregate checks queued 9 and 13 minutes for a hosted runner after their shards finished; they now take the shards' runners. * ci: bound the sharded test step and skip shards when the build fails Address the first automated review round on the Linux sharding lane: - Gate every build-consuming shard step on `test_build` success. `!cancelled()` stays true when a needed job fails, so a failed build still started all eight shards, each paying a full install before dying at the missing artifact and reporting artifact plumbing as its failing step. - Drop the test step cap to 45 minutes. The job cap is 60, so the old 110 could never fire; a step timeout keeps the diagnostic steps below it alive while a job timeout does not. - Keep the built-tree artifact for a week so a late "re-run failed jobs" still finds it, and correct the trigger comment that still named three post-merge jobs when five now accept push. Co-authored-by: Qwen-Coder <[email protected]> * ci: keep disk telemetry for a failed build and correct the design doc `test_build` installs with the disk sampler and then builds for up to its whole timeout, but the sampler dies with the install step's trap and no collector ran, so an ENOSPC build (or the OOM that took a shard host) kept no evidence. Give it the same failure-gated dump and last-step upload as `lint_and_static`, under its own artifact name. The incremental-lint design still said the test job keeps normal prepare behaviour; this lane now installs with `QWEN_SKIP_PREPARE=1` and unpacks the tree built once by `test_build`. Correct both language versions. Co-authored-by: Qwen-Coder <[email protected]> * ci: share the built-tree definition and the build preflight, harden the guards The archive step re-implemented `builtPaths()` from `scripts/ci-build-cache.mjs`, so the tree the shards unpack had a second definition that could drift from the one `pack` stores. A `paths` mode now prints that list for `tar -T -`, and a new test pins the four artifact-name literals that tie the producer to the eight consumers. The cached branch of `build` also hand-reproduced a subset of `scripts/build.js` and so skipped its ink-patch preflight: the same missing `frame-controller.d.ts` self-healed on a cache miss and surfaced as ~50 tsc errors inside ink's declarations on a hit. The preflight is now a function reachable as `scripts/build.js --preflight` and runs before both branches. The remaining review suggestions were all meta-test gaps: the standalone-runtime expression and the `test_build`/`test` prelude were guarded by nothing that compares them in order, `ci-build-cache.mjs`'s three CI modes were never executed by a test, and the infra-classification filter's step name was not joined to `ci.yml`. Each now has an assertion that goes red when the guard is removed. Co-authored-by: Qwen-Coder <[email protected]> * ci: bound the build steps so the failure collectors can run Round-2 review found four issues in the previous round's fixes and one gap in the new build tests. A job timeout cancels the job and skips the failure-gated collectors, so the `test_build` steps that can block now carry caps below the job's 45 minutes: the install, the build, the pack and the archive. The install caps are identical across `test_build`, `test` and `lint_and_static` so the prelude parity guard still compares the three copies field for field. A test pins every blocking cap under the job ceiling. The `--preflight` early exit was only asserted as a string, so it could be renamed with nothing going red while the cache-hit path silently ran the full build. A spawned case now copies `scripts/build.js` into the fixture and asserts the flag exits without generate, a workspace build or the schema step. The CLI mode tests write extension-less `#!/bin/sh` stubs and join PATH with ':', which cmd.exe cannot resolve; the Windows lane collects that file and downgrades its failure to a warning, so it would have carried a permanently red block. The suite is excluded on win32 alongside the other bash-fixture suites. The `test_build`/`test` intersection grew to 16 with the disk collector, but the floor stayed at 15, leaving exactly one shared step uncompared; the floor now matches the membership. The cache-hit-with-changes branch — the code that decides what is rebuilt and what is deleted — gains a case: a changed workspace is purged and rebuilt by directory, and an untouched workspace keeps what the cache restored. ci.yml's recorded size baseline is updated to the current file so the next PR gets the full growth allowance back. Co-authored-by: Qwen-Coder <[email protected]> * test(ci): keep cache fixture call logs outside the archived tree Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: 易良 <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
bf2f003294 |
feat(web-shell): edit GitHub repository settings from the channel editor (#16149)
* feat(web-shell): edit GitHub repository settings from the channel editor Three settings decide how a GitHub Channel behaves and none of them could be changed from the Web Shell: which accounts may speak in a given repository, whether that repository answers without a mention, and how often the notifications API is polled. Narrowing one repository meant hand-editing settings.json on the daemon host, so the permissive channel-wide default stayed in place by inertia. The poll interval becomes a declared descriptor field, which is also what makes it persistable: the store rejects an undeclared key outright. Per-repository settings stay in the editor, because the store already validates their shapes and a new descriptor field kind would touch the SDK and every client for a shape only one platform family uses. Each repository row is tri-state and defaults to inherit. The save compares every field against the same reading of the stored object it seeded the draft from, so an untouched repository keeps its keys verbatim -- including an empty allowlist, which means nobody, and a list with no policy beside it, which inherits the policy but overrides the members. Materializing resolved defaults instead would freeze today's inheritance and stop a later change to the channel-wide default from reaching those repositories. The section map is keyed by field name, so Email's existing poll interval moves to Connection alongside GitHub's; a test pins that rather than leaving it to prose. * fix(web-shell): let a chosen repository allowlist own its empty speaker list Picking "Allowlist only" on a repository row and leaving the speaker box empty persisted the policy with no list at all. The runtime resolves a missing per-repository list through `??` to the channel-wide one, so the operator's most restrictive available action kept the wider audience while the field beside it said "Empty means nobody." The merge only wrote the list when the speaker text changed. On a freshly added row the text starts empty and stays empty, so changing only the policy skipped the write entirely -- the default state of the feature, not an edge case. Choosing the policy now owns the list too. The rule is deliberately scoped to a policy the operator actually changed: a stored allowlist with no list is a legitimate hand-written shape meaning "members inherited", and writing an empty list there would narrow a channel nobody edited. Both directions have their own case and their own mutation. * fix(web-shell): make the repository speaker list symmetric and its hint truthful Two defects in the per-repository speaker list, both in the path where the operator moves the policy without touching the text. Leaving an allowlist behind kept its empty list. An empty array is not nullish, so the runtime kept denying everyone while the row read "Inherit" and the hint promised the channel-wide list was back. No gesture the form offers could repair it, because every one of them reads back as an empty box and wrote nothing. The policy-changed branch now handles both directions: entering an allowlist owns an empty list, leaving one releases an empty list, and a list with names in it stays because the box still shows it. The hint was derived from the draft policy alone, which tells two deliberately preserved stored shapes the opposite of the truth: a stored allowlist with no list inherits the channel-wide members but was told "empty means nobody", and a stored empty list with no policy admits nobody but was told "leave empty to keep the channel-wide list". The row now asks the save path what it will own, so the copy and the persisted object cannot diverge again. Also pins the paths the previous suite left unexecuted: returning a stored require-mention and policy to inheritance, writing a list while the policy stays inherited, keeping a stored list when the policy moves to an allowlist, each row showing and editing only its own speakers, an explicitly open row, and the localized interval label. * fix(channels): bound the poll interval and stop a new bound stranding old channels Declaring pollInterval exposed two problems the field did not have while it was unknown to the descriptor. Nothing bounded it from above. Node clamps a timeout past its 32-bit range to 1ms, so "poll once a month" persisted a hot loop that burns the notifications quota and settles into error backoff. Number descriptors now carry a maximum beside exclusiveMinimum, mirrored through the SDK, the registry's metadata validation, the store and the editor, and GitHub declares the same one-day ceiling Email already enforces at runtime. PollingChannelBase also falls back past it, so a hand-written configuration cannot hot-loop either. And a stored value the runtime tolerates became unsavable. A channel holding pollInterval 0 works today because the poll loop falls back to 60s, but the editor re-sends that 0 verbatim and both the editor and the store now rejected it -- blocking an operator who only wanted to add a repository. Both skip the range check for a finite number deep-equal to what is stored. The tolerance is deliberately not generalized: an existing contract pins that an unchanged stored scalar of the wrong type is still rejected, and a type error should be fixed rather than forgiven. Also names the repositories whose settings outlive a switch away from the allowlist policy. The runtime resolves per-repository senders and requireMention without consulting groupPolicy, so those entries stayed enforced while the rows disappeared; deleting them would destroy hand-written configuration, so the editor lists them and says how to edit them instead. Closes the test gaps the review found: the delete-to-inherit paths, writing a list under an inherited policy, the array guard, per-row speaker values and two-row editing, an explicitly open row, the localized label, a mixed-case chat group id, unmodelled keys on a case-retyped repository, the descriptor field itself, and row-scoped visual assertions with the last row in frame. * fix(web-shell): make the repository hints describe what saving actually does Three places where the form promised something the save would not do, plus a ceiling that repaired too much. The speaker hint asked the projection what the current draft owns. A repository storing a speaker list with no policy beside it therefore read "empty means nobody", while clearing the box deletes the list and hands the repository back the channel-wide members -- widening it while reporting success. The hint now asks the projection about an empty box, which is the hypothetical it describes. Row edits were projected only under the allowlist policy. Editing a row and then switching the group policy rebuilt groups from the stored config, so the save closed the dialog and restored the value the operator had just changed. The non-allowlist branch projects the same overrides now, and still drops entries that carry nothing. The retained-settings alert claimed the settings still apply under every policy that is not Allowlist. Under Disabled the group gate drops every envelope before either lookup, so the alert now says they are kept but not applied, and the Chinese copy names the option the select actually renders. The interval ceiling fell back to 60s for anything above one day. Values between one day and Node's 32-bit timer limit were honoured before and are not hot loops, so a two-day GitLab interval became 1440 polls a day. Clamp instead of replacing, and state the ceiling in the GitLab and hosting-contract docs. The range message also invented a lower bound of 0 for a field declaring only a maximum, and now names only the bounds the descriptor declares. Tests: the three registry maximum-validation branches, the maximum half of the unchanged-value tolerance on both the editor and the store, the inherit sentinel mapping through both row selects, the disabled alert, the empty-box hint, and both shapes of the range message. Docs: the design no longer claims no SDK or schema change, credits the right class for speaker resolution, and the serve protocol reference documents maximum. * fix(web-shell): say what an empty speaker box saves, and sweep the invariant The inherited-speaker hint promised to "keep" the channel-wide list, but a repository storing its own list is not keeping it: clearing the box deletes that list and hands the repository the channel-wide members, which can be many more people. "Use" holds for every shape that reaches the hint. The case pinning that shape now also clears the box and asserts the request, so the copy and the save are pinned together rather than separately. Pass the group policy into the retained-settings alert instead of deriving it a second time. The two derivations were equivalent, but nothing kept them that way, and a drift between them is what made the alert claim settings still apply under a disabled policy. Add a three-part invariant sweep over twelve stored shapes crossed with every single-field edit, driven through the real save path: an untouched row reproduces its stored object byte-for-byte, the empty-box hint agrees with what that save admits, and no edit widens the admitted set unless the row's own controls asked for it. Admission resolves per-repository then wildcard, so an absent list means the channel-wide members rather than nobody. Correct the docs where they described the superseded implementation: the hint decision still named the current-draft projection, the user guide contradicted itself on clearing an explicit allowlist, the clamp's blast radius omitted Email not extending the base at all, and the files table listed fifteen of the twenty-four files this change touches. * fix(web-shell): keep repository edits on every non-Allowlist save path The branch that retains per-repository settings when the group policy leaves Allowlist was gated on the STORED policy, so a channel already open never reached it: an operator who switched to Allowlist, narrowed a repository and switched back saved successfully while the request restored the old open entry. It also iterated stored groups only, so a repository typed during that visit was never written and fell back to the wildcard. Both admitted a sender the operator had just excluded, and the alert promises those edits are kept. Projecting the draft onto conflicting aliases was the third path. The draft holds one entry per lower-cased id, so two stored spellings that disagree both received the last one's settings, turning an alice-only repository open while the conflict check stayed silent — it only runs when the final policy is Allowlist. Those entries are now left as stored, which is what the runtime resolved before and cannot widen anything. Refusing the save here instead would strand the operator: the conflict error is reported on a field that is not rendered once the policy leaves Allowlist. Both consumers now share one alias set rather than two judgements that could drift. The invariant sweep missed all three because its fixture hardcodes Allowlist, so it never varied the policy dimension. A second sweep crosses the same shapes with the stored and final policies, for a stored repository and one that exists only in the draft, asserting the runtime's own rule: speakers are resolved without consulting groupPolicy, so a row must write the same entry whichever policy the save lands on. * fix(web-shell): keep an ambiguous repository alias exactly as stored Preserving conflicting aliases from the projection was not enough: the empty-entry filter still ran, so a no-op save on a channel stored Open deleted an empty alias. The runtime matches that exact key and inherits the wildcard allowlist, while deleting it lets the case-insensitive fallback reach the wider spelling — a repository nobody edited went from one allowed sender to everyone. The same gate one type over deleted a hand-written non-record value with the same result; the store preserves an unchanged non-record value, so passing it through keeps the save a no-op instead. An ambiguous entry is now exempt from the filter whatever it holds, and a non-ambiguous empty entry still drops so a row returned to inherit leaves no stub. Skipping the draft for ambiguous ids also discarded edits the operator had actually made, and the dialog closed on success while the request restored the stored policy. An edited ambiguous row is now refused, with the error on groupPolicy because the allowlist field it used to be reported on is not rendered once the policy leaves Allowlist. The refusal is scoped to a real change against the reading that seeded the draft, so an untouched switch still saves; refusing everything would strand an operator who only changed the policy. The alias dimension is now crossed into a sweep rather than left at two examples: every stored shape against a disagreeing lower spelling, over both stored policies and all three non-Allowlist final policies, asserting that an untouched save is a byte-for-byte no-op admitting the same people through the runtime's exact-then-fallback lookup, that a real edit is refused, and that a no-op edit still saves. Two dialog cases click the same flows and assert whether onSave is reached, which the state layer cannot see. * fix(web-shell): enforce the interval floor and stop rewriting entries the form cannot show The poll interval had a ceiling but no floor. The descriptor's lower bound is exclusive, so 0.5 passed the editor and the store, and setTimeout rounds it to about 1ms — the hot loop the ceiling exists to prevent, reached from below and persistable only because this change made the field managed. The getter now clamps into both ends from one pair of exported constants, and the GitHub descriptor declares its maximum from the same export so the two cannot drift. An exclusive minimum of 1 is not available, since it would reject the documented 1, so the field description and the user doc state that a smaller positive value is raised to 1. The claim that the store preserves an unchanged non-record group entry was wrong, and it was mine: the tolerance covers the whole groups value, while a single non-record entry is refused on every save. A case against the real store now pins that. Rather than weaken per-entry validation to make the comment true, the editor refuses such a save with a message that names the entry and points at settings.json, because no row can represent it and a generic daemon error leaves nothing to act on. The entry is still not deleted, whether or not it has an alias twin: deleting the exact key the runtime matches first is a rewrite nobody asked for. The retained-settings alert enumerated storage instead of the save, so it never named a repository typed during the visit even though the save writes it, and it announced an allowedUsers list beside an inherited open policy as still applying when senderGateFor builds no allowlist for it. The list now comes from the request the save builds and is classified by resolved effect. An unrelated edit also rewrote the file: a channel that never had an allowlist lost an already-empty entry, because the filter treated every empty entry as a stub. It now distinguishes leaving Allowlist from never having had one, while a repository typed during the visit stays a placeholder that is dropped when it carries nothing. |
||
|
|
f30da36c6f |
feat(channels): deliver GitHub events to PR-bound sessions (#16117)
* feat(channels): deliver GitHub events to PR-bound sessions A session that follows a PR (it created, reviewed, or verified the PR and carries the PR binding) never saw GitHub channel events: the channel routed everything to its own chat_thread sessions, and pure CI failures or merge conflicts produced no notification at all. This connects the two systems. When deliverToBoundSessions is enabled, a new read-only process-global route (GET /sessions/pr-bindings) reverse-looks-up the sessions bound to a PR, and the GitHub adapter injects the event into the most recently updated bound session as a prompt instead of a channel-owned session. The injected prompt marks GitHub content as untrusted, keeps the reply inside the session (no auto-publication), and runs the same group/sender/pairing preflight as the channel path. Dormant bound sessions are woken through the bridge's resume path; only a truly deleted session falls back to the normal channel path. Queue-full is treated as backpressure and does not consume the retry budget. prWatchEvents adds edge-triggered state watching for bound PRs inside the existing poll cycle (ci_failure, merge_conflict, head_change, state_change), with per-head dedupe, a bounded grace window for late-registering checks, partial-snapshot-safe pruning, and state persisted before dispatch so restarts never re-fire a transition. Both fields default off; existing deployments are unchanged. Design: docs/design/2026-11-02-github-channel-bound-session-events.md (EN) and .zh-CN.md (ZH). Verified with 2039 unit tests across channels/base, channels/github and the cli worker/route suites, plus isolated-daemon E2E against a mock GitHub API (bound delivery, dormant wake, watch transitions, default-off regression). * fix(channels): harden bound-session delivery against incomplete scans and crash windows Address the six review findings on the bound-session event delivery, each with a regression test: - Partial binding snapshots no longer replace the routing map or the accessor's wake cache: known bindings and cwds absent from an incomplete answer are retained, so a workspace listing failure can never switch a private delivery to the public channel path or turn a queued dormant delivery into a gone-session fallback. - A binding recorded merged/closed by the independent refresh stays watched until the poll itself observes the terminal transition, so state_change is never skipped by the refresh winning the race. - An empty combined commit status (total_count 0, which GitHub reports as state "pending" for SHAs without legacy statuses) no longer counts as active CI; Actions-only and no-CI heads settle instead of re-fetching checks on every poll. - Watch events now persist a per-head fired ring, so a flapping aggregate (failure -> success -> failure on the same head) delivers ci_failure exactly once, across restarts. - The bindings route flags live-merge degraded successes (liveMergeFailed) as partial, alongside truncation and scan-cap exhaustion. - Detected watch events are queued durably (pending) in the same write as the baseline, then claimed, marked fired, and delivered; a failed claim leaves the event queued, and a crash at any point leaves either a pending event or a claimed task for recovery. The watch pass runs before generic inbound recovery so recovery cannot remove a watch task the watch pass would then re-claim. Design docs (EN/ZH) updated to match. Tests: channels/github 258, cli worker+route 139, channels/base 1652 — all green. * fix(channels): close bound-delivery lifecycle and snapshot-authority gaps A multi-round undirected + adversarial audit of the bound-session delivery feature (61 confirmed findings, probe-verified) plus a follow-up review round and the triage escalation surfaced gaps around the very mechanisms the first hardening commit introduced. This closes them in one pass: Delivery lifecycle: watch dedupe keys now identify transition instances (episode counter) so dirty->clean->dirty, close->reopen->close and SHA revisits fire again while ci_failure stays once-per-head; pending events reconcile before generic recovery and independently of the API pass; terminal task records are replaced instead of silently swallowing recurring keys; acceptance is persisted before task removal so a crash cannot replay an injection; crash loops charge one attempt per recovery pickup; queue-full and the new BoundSessionUnavailableError defer within a bounded horizon and retire to a private dead-letter log instead of freezing the shared notification cursor or vanishing silently; watch events are private-only (maintainer decision: no public fallback that would publish operator-facing prompt text). Snapshot authority: the bindings route enumerates workspace entries and flags non-active trusted workspaces and multi-page scans partial; retention is session-granular and recency-competing; the comment lane defers until the first complete snapshot; cold-cache wake treats partial absence as transient; www host aliases normalize; malformed 200 bodies fail closed. Watch detection: periodic settled-head re-check (GitHub does not bump PR updated_at on check activity), ci_failure subscription gate, startup_failure conclusion, fail-closed state reads, array-shape guard, tmp cleanup on failed writes, rate-limit fast-skip so the watch cannot pin the notification lane. Worker hygiene: channel-turn _meta classification with worker authorization, wake registration detach after the retried prompt settles, response-body cancellation and body-read deadlines; the restore route distinguishes permanent workspace absence (workspace_not_found 404) from reversible draining states. Docs: both design languages record the new decisions (private-only watch, bounded backpressure + dead-letter) and corrected semantics; user doc cost claims qualified. ~90 new regression tests pin each fix witness. * fix(channels): repair bound-delivery build, error taxonomy, and watch dedupe A second undirected + adversarial audit of the bound-delivery fix commit found a broken build and 33 further confirmed findings. This closes them all in one pass: - build: transitionInboundTask's Pick omitted deliveredAt/deferCount/ backpressure, failing the github package's tsc (vitest masked it). - cold start: gate the comment lane on the first successful snapshot (boundSnapshotSeen), not complete-only, so a permanent partial no longer stalls the lane forever. - private-only watch: route watch envelopes by identity so a delivery-cleared watch task can never leak to the public path; retire watch events symmetrically on unbind; restrict terminal-task replacement to watch keys so a cancelled comment is never revived after a crash. - error taxonomy: superseded/archived sessions are gone, the worktree-reset barrier and cold-partial absence are backpressure, on both the direct prompt and wake paths — permanent states fall back, reversible states defer instead of burning the retry budget. - cross-workspace: the worker token is a process-level relay credential admitted for any served (primary/trusted) workspace, restoring channel-turn classification for cross-workspace bound delivery. - snapshot authority: flag an untrusted primary as partial; log per-workspace scan failures. - watch robustness: fail fast on secondary rate limits; pin ci_failure once-per-head against fired-ring eviction (ciFiredSha); roll back a claim whose pending->fired save fails; reconcile pending events independently of the binding fetch; surface watch-cap overflow. - dead-letter: surface the payload and stop claiming success on write failure. - restore contract: migrate unknown-cwd tests to 404 workspace_not_found and pin reversible states (draining/transitioning/blocked) out of it. Adds 38 regression and test-gap tests; each code fix mutation-verified red without its test. Both packages type-check; design docs (EN/zh-CN) and the user doc synced. * fix(channels): harden bound-delivery snapshot authority, recovery ordering, and watch fail-fast Round-3 undirected + adversarial audit found further defects; all high-confidence ones are closed here. - snapshot authority: a bound-session Gone evicts the dead session from the binding maps before the refresh, so a permanent partial snapshot cannot retain it by recency and shadow a live follower; a Gone from the re-delivery hop falls through to the public/retire contract instead of consuming the failure budget; a binding refresh that has never succeeded logs a distinct, throttled, actionable stall message (the gate stays fail-closed). - recovery ordering: a bound task's deliveredAt acceptance mark is persisted before the cross-lane pending-deliveries read, so a corrupt or unreadable file cannot leave the task running without the mark and replay the injection; watch state drops malformed per-entry values (keeping healthy entries) instead of throwing every poll. - error taxonomy: a post-wake retry 404 is reversible backpressure (the owner may be draining; the classification keys on "the wake ran", not the detach return), not Gone; the interrupted-worktree 409s (worktree_marker_missing / worktree_reset_interrupted) are gone for delivery (a resume retry never converges); an authoritative Gone evicts the cached wake cwd. - watch robustness: fail fast on any rate limit (GitHub does not always send retry-after) and permanent per-PR 404/410/451, not just primary limits. - GHEC data-residency: map api.<tenant>.ghe.com to the <tenant>.ghe.com web host so bound delivery works there. - docs: PR breaking-change notes, dead-letter filename, Files-affected table, ci_failure SHA-revisit semantics (both design docs, EN + zh-CN), plus two test-harness isolation fixes (shuffle-safe). Adds regression + test-gap tests; every code fix mutation-verified red without its test. Deferred with disclosure: a low-confidence pre-existing sidecar enrichment silent failure in the shared session-list surface. * docs(channels): note the post-wake 404 exception in the accessor comment Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): close comment-round findings on absence, idempotency, and watch isolation An incomplete binding scan never establishes absence: empty-partial cold answers defer the lane for a bounded number of polls, and unmatched events defer recoverably (dead-letter at the horizon) until a complete snapshot lands. Scanned sessions with no bindings emit tombstone rows through the route, projection, and accessor so an observed unbind stops private delivery even on partial answers; unreadable sidecars flag partial. Prompt admission gains an idempotencyKey dedupe ring so a retry after a lost acceptance mark or a lost 202 re-injects nothing; the accessor sends the durable task id on every POST. Cross-workspace channel classification now requires the explicit bound-relay marker, scoping the process-level worker credential to its reason. 429 and other 5xx join the backpressure taxonomy. Watch hardening: reconciliation failure gates watch-task recovery, shared credential/rate-limit failures abort the whole pass (401 included), the pending CI re-check is interval-bounded, a failing Checks endpoint no longer suppresses the other detections, and pending envelopes validate with the task store's own validators. Bound prompts surface both truncation levels and the comment author; a failed dead-letter write keeps the task. Docs (user, protocol, design EN/ZH) and Web Shell i18n synced; every fix mutation-verified. Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): make the aggregate per-comment display cut visible Real-scenario verification of the truncation fixes surfaced a third silent-tail surface in the same flow: the aggregate lane caps each comment at 400 display characters with no marker, and the comment is already recorded in dispatchedComments — a long comment's tail (possibly its actionable last line) vanished from every surface while the bullet read complete. The cut now carries an explicit [comment truncated: N more characters] marker, alongside the existing prompt-level and batch-level notices. Regression test added and mutation-verified; user doc and design docs (EN/ZH) synced. Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): close round-1 audit findings on event loss and admission Undirected + adversarial audit of the merged diff found four event-loss paths and an admission-contract outlier; all fixed with regression tests and mutation witnesses: - The aggregate lane recorded dispatchedComments keys BEFORE the task claim; an unwritable task store plus the unconditional end-of-poll cursor save persisted the keys with no task, silently consuming the comments forever. Keys now land via applyTaskDedupe only after the task write succeeds. - The idempotency key is now the event identity (source chat + message id), not the task id: a crash after task removal but before the cursor save re-creates the task, and a task-scoped key let the daemon admit the same event twice. - A persistently failing Checks endpoint degraded the pending re-check interval to one doomed request per PR per poll; failed attempts now advance the interval clock, and the undefined-ci branch is bounded too. - Pruning a watch entry whose earlier transition still has a surviving task reset the episode counter; a rebind then collided keys and silently consumed the new transition into the old envelope. Prune now skips entries with recoverable watch tasks (and skips entirely when the task store is unreadable). - sendPrompt's unknown/closing admission rejections threw as rejected promises, letting the prompt route return a phantom 202 and the ring remember a non-admission; they now throw synchronously like the sibling admission paths, and the route forgets the key on any deferred SessionNotFoundError as defense in depth. Also: bounded the accessor wake-cwd cache under permanent partial, honest relabel of the routing-gate regression test, field-level body assertions, Web Shell coverage for the two new descriptor fields (EN/zh), descriptor reasonFilter description synced to the checkbox contract, and the protocol doc's ring bounds completed (1024-session cap and its lifetime exception). Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): close round-2 audit findings on watch key identity and admission The round-1 event key still collided across PRs (one repo, one session, parallel episode counters) and across episode-counter resets (prune or corruption recovery plus rebind), letting the daemon's long-lived idempotency ring silently swallow new transitions. Watch keys now bake in the PR number and detection timestamp; ci_failure keeps once-per-head via PR+SHA prefix matching in the fired ring and pending queue. Over-length event keys hash deterministically instead of earning a route 400 that burns the retry budget. The prompt route forgets the idempotency key on any pre-admission deferred rejection, keyed off the admission signal rather than the error type, covering the standalone working-directory RequestError. Permanent partial snapshots can no longer grow binding retention or watch entries without bound (1024 cap, oldest-first, surfaced on stderr). Design docs (EN/ZH) and contract comments are synced, including the ring-lifetime exception at the 1024-session cap. Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): close round-3 audit and maintainer review findings The public-routing gate trusted a historical complete snapshot: under a later partial answer an unmatched PR event could take the irreversible public path while a live follower hid in an unlisted workspace. Absence is now authoritative only on a current complete successful snapshot, or per PR when every previous non-channel binder is listed and excludes it (a listed session's PR set is complete, so its tombstone stays authoritative). Never-known PRs, omitted or retention-evicted binders, and failing refreshes defer as bounded backpressure; the new clauses are PR-scoped so issue events and channel-only bindings keep flowing publicly. The aggregate batch idempotency key identified only the first comment, so a superset batch re-listed after the cursor-save crash window deduped into the original admission and permanently consumed the new members. The batch message id is now a content hash of the sorted member identities; identical batches still dedupe across the acceptance crash window. Partial retention rebuilt only the old winners, so a listed tombstone on the newest follower emptied the routing map despite a retained alternate; the rebuild now iterates the retained session rows. Watch-entry cap eviction gained the prune's retained-binding and in-flight-task protections (an evicted entry re-baselines silently, so the earlier re-fire claim was wrong and is corrected). Disabling watch subscriptions no longer clears the reconciliation guard while unreconciled pendings exist, and the per-entry pending queue is capped at 64 with oldest overflow retired to the dead-letter. Cross-instance watch delivery is documented as per-subscription at-least-once, and the admission residual (202 before admission completes) is stated precisely in the route and contract comments. Design decisions 10/11 (EN/ZH) are synced. Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): close round-4 audit findings on absence authority and republication A partial binding snapshot can never prove a PR unbound: an unscannable workspace may hold a never-observed follower, and a listed tombstone proves only that the listed session unbound. The round-3 per-PR authority mechanism granted public routing when every observed binder was listed and excluding the PR, which a probe showed misroutes a same-poll comment irreversibly. Remove the mechanism; unmatched PR events now defer as bounded backpressure until a complete snapshot lands, while listed tombstones still stop private delivery and watching immediately. The partial/failed clauses stay PR-scoped so issue events keep flowing once the cold-start gate opens. Close a pre-existing public-reply double-publish window: a reply could land (publication audit posted), the task be removed, and the cursor save lost; the re-listed comment was claimed and published again because only recovery checked the audit. The dispatch path now honors the same audit record before claiming, fail-closed on an unreadable audit, and records the dedupe keys so the cursor advances. The worker accessor flagged a listed session partial only when ALL its PR entries failed validation; any dropped entry now degrades the answer so a partial projection never masquerades as complete. The round-3 cap-eviction task protection gains a non-vacuous witness (the protected entry moved outside the retained candidate range). Bound preflight rejections and the disabled-subscription guard now surface accurate stderr, and the protocol doc states the 202-before-admission residual. Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): close round-5 audit findings on audit fail-closed and wake classification The round-4 claim-time publication audit check claimed fail-closed but only skipped the single notification: with no task claimed, the end-of-poll cursor commit (mark-read included) advanced past the skipped event, so a persistently unreadable audit silently consumed every new public event instead of retrying. An unreadable audit now freezes the whole poll's cursor via a dedicated flag, and the next poll retries the read and the events. The audit consumption verdict treated any posting/posted/suppressed line as consumed regardless of a later terminal failed, so a reply that never landed (posting then failed) plus a lost cursor save permanently swallowed the event. The verdict is now the LAST outcome per source key: a terminal failed keeps the event retryable; a bare posting (crash mid-post) still consumes at-most-once. The gone-session public fallback bypassed the absence gate: under a partial scan an unscannable workspace may still follow the PR, making the public reply irreversible. The fallback now defers as backpressure until a complete snapshot establishes absence (delivery cleared so recovery re-resolves without a doomed POST); deliverToBoundSession returns a settled flag so runInboundTask's success path cannot retire a task the fallback deferred. The wake/resume path classified 429 and non-503 5xx as delivery failures, burning the attempt budget into an unrecoverable dead-letter on a transient resume rate limit or spawn jitter; they are backpressure now, aligned with the POST path. The watch drain stops at the first failed delivery to preserve per-PR ordering, and the worker flags ANY dropped session row partial, symmetric with the PR-entry rule. Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): close round-6 audit findings on freeze bypass and ordering The round-5 audit freeze only froze the cursor commit, but the direct lane and the first-contact body feed recorded their dedupe keys unconditionally after dispatch; the run loop's unconditional cursor save then persisted the keys, so a transient unreadable audit permanently filtered the re-listed events — the same silent consumption the freeze was meant to prevent, revived through two lanes. Both lanes now honor the dispatch refusal before recording, matching the comment lane. The audit consumption verdict treated every terminal failed as a provable no-write, but ambiguous errors (502/timeout) may have landed the comment; unlocking those keys let a crash window double-publish an irreversible public reply. Ambiguous failures now audit as failed_ambiguous and stay consumed (at-most-once); only definite no-write failures (secondary rate limit with remaining=0) unlock retry. The per-PR drain ordering held only within a single pass: the watch pass re-drains after reconcile and the next poll's reconcile runs before recovery, so a later event could still arrive before its unsettled predecessor. The drain now skips the whole entry while an earlier watch transition of the same PR has an unsettled task (a task whose key equals the pending head is that event's own crash-window claim and drains normally). A claim persistence failure propagates out of the drain so reconcile fails and the recovery guard holds instead of delivering an accepted task whose pending record is unreconciled. Disabling watch subscriptions with legacy unreconciled pendings parked the accepted tasks as backpressure so they stop freezing the shared notification cursor channel-wide; an unusable watch state container is surfaced on stderr like the corruption path instead of wiping silently. Co-authored-by: Qwen-Coder <[email protected]> * test(channels): cover the audit-freeze recovery with deliverToBoundSessions off The maintainer's P2 reproduction exercised the unreadable-audit window with deliverToBoundSessions both enabled and disabled; parameterize the R5-A1 freeze/recovery test over both modes so the public-only path is pinned too. Co-authored-by: Qwen-Coder <[email protected]> * test(channels): mock pulls.get in the direct-lane freeze test processDirectLane fetches PR meta before dispatch; without an explicit pulls.get mock the fetch throws under isolated runs, the per-notification catch skips the event, and the test never reaches the freeze guard — passing only by mock leakage from earlier tests in a full-suite run and failing (vacuously) in isolation. The mock makes the witness genuine in both run modes; the guard-off mutation now kills it isolated too. Co-authored-by: Qwen-Coder <[email protected]> * test(channels): pin the reader's failed_ambiguous consumption verdict The writer-side failed_ambiguous tests only read the audit file; no test drove the reader's consumption of an ambiguous verdict, so deleting the reader's failed_ambiguous branch kept the suite green. The distinguishing sequence is posting -> definite failed -> failed_ambiguous: the ambiguous retry must supersede the stale definite-failed verdict and stay consumed, or the double-publish window reopens. The new test seeds that sequence (plus a definite-failed retryable contrast) and drives dispatch; the reader-branch mutation now kills it. Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): let the notification cursor flow while watch reconcile is blocked Co-authored-by: Qwen-Coder <[email protected]> * test(channels): prime the comment lane in the direct-lane freeze test Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): decouple watch retires from the public reply store and sanitize dead-letter dumps Co-authored-by: Qwen-Coder <[email protected]> * fix(serve): throw synchronously on the standalone admission guard so relays never consume a phantom 202 Co-authored-by: Qwen-Coder <[email protected]> * test(serve): harden deferred-forget retries and align admission contract comments Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): protect the dispatched-comment ring from frozen-cursor eviction and sanitize error diagnostics Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): remove the hard-cap comment refusal that silently consumed events; harden the cursor ring Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): prune the comment-time map every poll, not only on dispatch Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): iterate live followers in the gone-fallback and prune the time map in finally Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): exempt issue events from the whole bound-absence gate The per-event absence gate's !boundSnapshotTrusted clause was not PR-scoped, so under a permanent partial binding scan (a multi-page scan, a long-lived non-active workspace, an untrusted primary, or a corrupt sidecar) a complete snapshot never lands and unmatched issue events deferred to the 240-deferral horizon and dead-lettered without a public reply — even though bindings are PR-only and an issue's absence is provable without any snapshot. PR-scope the whole gate, cold start included; issue events route publicly at once. The labeling assumption is verified against 50 real GitHub notifications (zero subject-type/URL mismatches) plus GitHub's exclusive issue/PR number space. Sync the design EN+ZH (gone-fallback iteration, the finally prune timing, the whole-gate issue exemption) and the user doc, and align the stale R13-M1 test comment with the finally semantics. * docs(channels): qualify the issue exemption and document the audit write trade-off Round-15 audit follow-up, documentation only: The user-doc sentence added in round 14 overstated the issue exemption ("never held by the binding scan ... at once"): round 14 PR-scoped only the per-event absence gate, while the lane-level cold-start gate still holds every notification (issue events included) until the first successful binding lookup, and the empty-partial gate holds the lane up to 30 polls. Qualify the sentence to the per-event gate, state the lane-level hold explicitly, and tighten the design's stale-bindings shorthand to match (EN + ZH). Also document the publication audit's WRITE semantics in key decision 10 (EN + ZH): the write is best-effort (fail-open) so an unwritable audit store does not break the public-reply contract — a deliberate trade-off pinned by the "keeps successful publication when its audit write fails" test. Under total storage failure no durable consumption intent can be recorded, so a failure spanning the posting record, the POST, and the task removal can let recovery re-publish; the read path stays fail-closed. Notes the fail-closed write-ahead alternative and its availability cost. * fix(channels): tolerate .diff/.patch binding URLs and pin the aggregate sort parsePrBindingUrl rejected GitHub's /pull/N.diff and /pull/N.patch spellings while the refresh sweep's pullRequestKey tolerates them. The bind route accepts any http(s) URL without normalizing the path, so a manually pasted .diff/.patch binding persisted and was then skipped by the routing-map build, the gone-fallback, and the PR watch — silently degrading that PR to public replies and never watching it. Align the suffix alternation with pullRequestKey; the existing number-match guard still rejects mismatched url/number bindings. Add parameterized .diff / .patch regression tests. Also pin the aggregate content hash's order-independence: the existing R3-B1 test only fed ascending members, so removing the implementation's sort survived. Add an out-of-order batch test asserting the key equals the sorted batch's key. Documentation: key decision 11 (EN + ZH) still carried round 14's overstatement that issue events are exempt from the "ENTIRE absence gate, cold start included"; round 15 fixed the user doc and the stale-bindings paragraph but missed this numbered decision. Qualify it to the per-event absence gate (including its !boundSnapshotTrusted cold-start clause) and state explicitly that the separate lane-level cold-start gate still holds issue events until the first successful lookup. * docs(channels): document the hidden-catalog delivery limitation Round-17 audit follow-up, documentation only (EN + ZH): Document the bindings snapshot's hidden-catalog exclusion as a deliberate design limitation: the machine-owned multi-agent task sessions (agent-host / agent) are excluded by the session-list path without setting partial, and they are not interactive delivery targets, so a PR bound only to such a session takes the public channel path. The public reply is safe; the private-delivery contract does not extend to those sessions. Making the snapshot authoritative over them or gating the write path is left to the multi-agent subsystem's design. Also finish the per-event qualification sweep: the absence bullet still said issue events are exempt from "the whole gate"; qualify it to "the whole per-event gate" to match key decision 11 and the sibling surfaces. * fix(channels): address review round — wake-cache presence, watch 403 isolation, aggregate ack Resolve the review-bot findings (one Critical) plus the doudouOUC doc gap, each pinned by a regression test and an isolated mutation witness: - R2-1 (Critical): the cold-cache wake branch read the target's cwd from the FIFO-capped hint cache, so a listed, live, dormant session beyond the 1024-row cap on a complete snapshot was misreported as gone and its event posted publicly. Read presence from the fresh full snapshot instead; the cap stays a bound (fast-path hint only) and the Gone verdict is unchanged. A present session is now woken even on a partial answer (presence is authoritative); only an absent session defers there. - R1-7: a failing refresh after one successful empty partial stalled the comment lane forever (the bounded escape only advanced on the success path) and hid behind a misleading "keeping previous map" log. Advance the same escape and key the distinct stall log off the no-authoritative-map state. - R1-11: a per-repo permission 403 aborted the whole watch pass, starving every other watched PR. Abort on a 403 only when provably a shared rate limit; otherwise isolate it per PR. - R2-2: the aggregate batch's non-numeric hash messageId silently disabled the eyes reaction. Record the batch's first comment as a bounded reaction anchor resolved by the prompt-start/end overrides. - R2-18: export PR_WATCH_EVENTS and build both the validator set and the UI option list from it; add the four localized option labels (EN + ZH). - doudouOUC: document the >100-session permanent-partial steady state and add a throttled diagnostic naming the listed session count. Test gaps closed: prsReadFailed production setters (R2-3), both idempotency FIFO bounds (R2-4), the real-fetch comment-time capture (R2-5), and the editor field descriptions (R2-6). Documentation: name the third relay _meta credential key (R2-7), complete the Files-affect inventory (R2-8), and qualify the unconditional acceptance criteria to the two FIFO caps and the restart recovery (R2-9), mirrored EN + ZH. --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
dff7deb19c |
docs(release): explain hosted-runner queueing for publish (#16194)
* docs(release): explain hosted-runner queueing for publish * docs(release): link the current release pool record |
||
|
|
02b8b0108c |
feat(web-shell): show a Channel event timeline (#16106)
* feat(web-shell): show a Channel event timeline Channels reported what they did only through stderr, the publication audit, and the inbound task store, none of which the daemon could read. An operator wondering why a GitHub comment got no reply had to read the daemon log. Every Channel now appends one bounded journal per instance: the inbound messages it accepted, the ones it skipped and the machine reason for each skip, publication outcomes, inbound task transitions, and poll results. The journal holds routing decisions and identifiers only, never message text; it rotates at 1 MiB to a single backup, and a write failure is swallowed into one stderr line so diagnostics cannot break message handling. The daemon serves the journal through GET /workspace/channels/events and its workspace-qualified form, with limit, type, channel, and since filters, a 32-file and 2 MiB read bound, and the existing trust and client checks. The channel parameter repeats once per instance name because a Channel name may legally contain a comma. The Web Shell gains a Channel events page and sidebar entry that render the cross-Channel timeline for the resolved workspace, filtered by Channel and event type and refreshed every five seconds, gated by channel_management. Docs: the design in English and Chinese, the serve protocol, and the Channel user guide. Channel tests isolate QWEN_HOME so journals never land in a real home directory. * fix(web-shell): scope Channel events to the selected workspace Review found four correctness gaps in the event timeline. The page read the provider's daemon-wide primary cwd, so a Channel started in a registered secondary workspace could never be inspected. It now offers the same workspace selection as the management page and passes the selected cwd to both hooks; changing the workspace resets the Channel filter. The poll loop captured the adapter's detail before the poll ran, so the first success had no counters and every later one carried the previous poll's counters against the current timestamp. The field is now cleared before the poll and consumed after it settles, on both the success and failure paths. The 32-file read cap was applied before the Channel filter, which made an explicitly requested, quieter Channel unreachable once busier peers had newer files. Candidate files are now narrowed by the requested identities first. Channel identities were truncated to 80 code points while the management API accepts 255 UTF-8 bytes, so longer legal names never matched their own filter and names sharing a prefix became indistinguishable. The identity now keeps its full legal length; only the UI shortens it for display. Each fix has a regression test that was mutation-checked against the previous behavior. Design (both languages) and the Channel user guide now describe the workspace selection, the poll-detail timing, the filter-before-cap ordering, and the identity limit. * fix(web-shell): mask Channel events cached for another query The workspace selector changes the query without remounting the hook, and the underlying resource keeps its previous data while a request is in flight or after it fails. Selecting a second workspace therefore showed the first workspace's rows under the new selector until the new request succeeded, and kept them when it failed — including their Channel names in the filter list. Each snapshot is now tagged with the query that produced it (workspace, Channel, types, limit) and mismatched data is masked, the same way the Channel management hook already does. Two hook-level regressions drive the real hook and resource with a pending and a rejected second-workspace request; both fail without the tag. * fix(web-shell): keep Channel event reads off the daemon's critical path A verification round measured the journal read at the documented cap: one page poll took ~860 ms of synchronous I/O, an unrelated /health request waited ~790 ms behind it, and four concurrent reads serialized to 3.4 s. The route runs on the daemon that also serves sessions, so a diagnostic page must not be able to stall it. The read is now asynchronous and chunked (64 KiB awaits) under tighter bounds: 256 KiB per journal file, 4 MiB per request, 32 files, freshest first. On the same 32-file / 32 MiB fixture the read is ~175 ms, a bystander /health stays at 6 ms p50 against a 2 ms baseline, and four concurrent reads finish in ~630 ms. Two reporting gaps go with it. Anything the bounds clip — dropped files or an exhausted byte budget — now sets `truncated`, so the page cannot show a timeline that silently omits whole Channels. And a scope directory that cannot be read propagates instead of returning an empty list, so the route answers 5xx rather than claiming the workspace's Channels did nothing; a missing directory still means "nothing written yet". The page also stops polling while its tab is hidden and reloads when it becomes visible, so a forgotten tab no longer reads journals every 5 s. Tests pin the two behaviors the wire depended on but the suite did not: reading events that survive only in the rotated file, and clamping a limit above the maximum. Both new mutations are killed, as are the truncation, unreadable-directory, and hidden-tab guards; a bystander-timer test pins that the read yields to the event loop. * fix(web-shell): report a clipped journal tail as truncated The per-file read window introduced with the asynchronous read is 256 KiB, but a journal grows to 1 MiB before it rotates. `readTail` skipped the head of a taller file without telling anyone, so a single busy Channel lost its own older events while the response still claimed `truncated: false` — the exact completeness contract the previous commit set out to fix. A 1 MiB journal of 1020 records answered 255 events with `truncated: false` at `limit=500`. `readTail` now reports whether it started mid-file and the reader folds that into `truncated`. The request budget is also spent in whole tail windows instead of a final partial one, so its flag keeps the precise meaning "whole journals were left out". Three tests close the gaps this exposed: a journal taller than the tail window must report truncation even when the returned rows fit the limit; the budget fixture now uses twenty full-window journals whose combined rows stay under the record limit, so only the budget can explain the flag; and a journal file that cannot be read must fail loudly, like the directory case already does. All four previously surviving mutants — tail flag, budget flag, budget debit, swallowed file-read errors — are now killed. * fix(web-shell): bound Channel event read retention The events read parsed and retained every matching record across all journals before applying `limit`, so an all-Channels poll on a busy workspace held far more than the rows it returns. Trim the accumulator to the newest `limit` once it passes four times that. A dropped record is older than every kept one and the sort is stable, so the answer does not change: a 60-scenario differential run against the previous behaviour produced byte-identical output. Trimming counts as hitting the record limit, so it sets `truncated` too. Closes the remaining non-blocking review items as well. The provider workspace action declares the full query type it already forwards, so the interface no longer suggests that path cannot filter by Channel. The new UI strings use the established 频道 label rather than introducing a second word for Channel beside the existing sidebar entry; the design docs keep 渠道, which is what every other zh-CN design uses. The docs now state the page's own request size (200) apart from the route default (100), and the `truncated` comments describe every reason it can be set instead of only the record limit. The NUL query-key separator is documented as collision-free: instance names reject control characters, journal names are sanitized, and a path cannot contain NUL. * fix(channels): read a journal that fits the budget and keep aligned records Reserving a full 256 KiB window per journal left a small journal unread when the remainder was under one window, even when it covered that journal many times over — coverage lost in exchange for a flag that was already truthful. Reserve what the journal actually needs instead, using the size the enumeration already stats, so the budget flag still means "whole journals were left out" and a 200-byte journal is no longer skipped beside seventeen full ones. A tail window that landed exactly on a line boundary also dropped one complete record, because the first line was discarded whenever the window started mid-file. Read one leading byte and drop that line only when the byte is not a newline. Folding the byte into the first chunk keeps the read to one awaited call per chunk, which is what lets the daemon serve other requests between them — now pinned by a test that compares the event-loop turns a tall read takes against a short one over the same six journals. A synchronous chunk read fails it. Also records why an unparseable instant cannot satisfy `since` while an unfiltered read still returns it, and pins both halves. * fix(channels): collapse repeated reason-filter skips into a counted row A real end-to-end run — the actual GithubAdapter over HTTP against a local GitHub-compatible API, a real agent turn, the page in a browser — exposed an amplification the seeded-journal runs could not. The poll cursor does not advance while an inbound task is recoverable, so a task that failed and is still retryable makes the provider return the same notifications on every poll, and filtered notifications are skipped before any dedupe. Each poll therefore re-journaled the whole batch: with a 2 s poll and 300 filtered notifications the journal rotated and pushed the accepted/posted rows out of the page's 200-row window. Journal at most 20 reason_filter rows per poll and collapse the remainder into one row carrying detail.filtered, which the page renders as a count in both languages. The budget is per poll, so a genuinely new batch is still described in full. Measured against the real adapter: 15 polls served 4,500 notifications and wrote 315 rows instead of 4,500, without rotating. Against a stub that honours both `since` and GitHub's read-state semantics, the redelivery stops once the batch is marked read and a new batch gets a fresh budget. The design docs record the mechanism precisely, including that an in-flight turn cannot cause it because the poll loop awaits pollOnce. * fix(web-shell): keep one Channel event polling loop across visibility restores Restoring tab visibility while a journal request was still pending started a second run. Clearing the timer handle cannot cancel an in-flight run, so both runs settled, both called schedule(), and only the last timer handle was kept — the orphan kept polling forever. Ordinary tab switches on a slow response therefore multiplied journal reads permanently: one overlapping hide/show cycle left two requests per interval, three cycles left four. Guard the cycle with an in-flight flag so a restore during a pending request lets that request finish and reschedule instead of starting a second loop. Both read paths are time-bounded (the SDK's 30 s fetch default and the action timeout), so the guard cannot stall polling. The regression test holds every request pending, overlaps three hide/show cycles, and asserts one request per interval afterwards; removing the guard reproduces the reported four calls. |
||
|
|
ae7901552e |
fix(core): settle the loop reminder delivery at the model-acceptance boundary (#15559)
* fix(core): settle the loop reminder delivery at the model-acceptance boundary takeLoopReminder() cleared the pending map and logged the 'reminded' telemetry while the ToolResult request was only being assembled. Between the take and turn.run(), the send still awaits microcompaction and memory recall and yields pending goal events, so a consumer that stops the generator there loses the streak's warning (dropPendingReminders only unmarks still-pending keys) while telemetry credits a delivery the model never saw. The delivery is now only prepared at the take: the client commits it when the first model-accepted event arrives and abandons it — restoring the pending entries — in the send's finally when the request never reached the model. A full reset drops a prepared delivery so a late abandon cannot leak a stale warning into a new prompt. The agent-core round loop keeps its take-time accounting with an explicit commit since it has no consumer-visible suspension between the take and the model call. Fixes #15537 * fix(core): defer the agent-core loop reminder commit to model acceptance * fix(core): drop the ineffective pre-round reminder abandons The three abandonLoopReminderDelivery() calls at the abort/maxTurns/time checks could not change anything: each is followed by a break out of the round loop, the detector is method-local, and abandon only moves entries between two private maps without logging. They made the take-site comment and the maxTurns control read as if they were what keeps `reminded` truthful, so drop them and say what the control actually pins — no round crosses a model-acceptance boundary, so nothing commits. Also, from the same review round: - abandon's doc block named the wrong function. A retry drops the streak through resetToolCallCount(), which deletes the three hard-coded keys; dropPendingReminders() cannot reach entries already taken out of the pending map. - client.test.ts asserted not.toHaveBeenCalled() immediately after mockClear(), which cannot fail. Compare the call count with the value captured before the send instead, so the "suspended before turn.run" premise is actually pinned. - cover the multi-entry path: commit credits every entry of a delivery and abandon restores all of them, not just the first. - telemetry.md and both languages of the loop-protection design doc now say `reminded` is logged at model acceptance, and the "sent, never accepted, credited on a later send" case is added to the enumeration of not-counted cases. --------- Co-authored-by: suzhiguo7 <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
6e49388c4a |
docs(serve): refresh daemon usage guidance (#16056)
Replace obsolete alpha-era deployment and persistence limits with current behavior. Document authenticated background startup, token sidecar lifecycle, and service-manager choices. Co-authored-by: verify <verify@local> |
||
|
|
6bef5a01c6 |
feat(web-shell): add "Add to Split View" to session menus (R1 fixes for #14878) (#15956)
* feat(web-shell): add sessions to split view from the sidebar * fix(web-shell): validate split targets and retain pending additions * fix(web-shell): report transient split-classification failures per session (#14775) * fix(web-shell): harden the first split open: race, per-id memo, pending mirror, failure notice (#14775) * fix(web-shell): disable no-op split adds, clarify the capacity hint, document split (#14775) * fix(web-shell): fix split classification type errors (#14775) * test(web-shell): fix split pending-navigation and archived-offer test refs (#14775) * style(web-shell): prettier-format the split classification memo type (#14775) * fix(web-shell): adapt controlled pane reveal to prop-derived pane set The main merge (#14577) derives paneIds from the sessionIds prop in controlled mode, so the reveal effect could no longer sync local state (setPaneIds does not exist) and its render-time ref check was dead: the ref is current by the time the effect runs. Track the previous commit in an effect-time ref instead. Also restore the test-block closer the merge dropped from SplitView.test.tsx. * Fix split-persist e2e spec: assert disabled state for already-open session (bot R2 blocker) The first smoke test clicked 'Add to Split View' on Session A after it was already a split pane. The R1-10 gate disables that menu item with the title 'This session is already open in Split View' (sidebar splitAlreadyOpen), so Playwright retried a click on a disabled item. Replace the click with toBeDisabled() + title assertions, reusing the shape of the six-pane spec in the same file. The no-op guarantees (pane count, draft, DOM persistence) are kept as-is. --------- Co-authored-by: luozongmin <[email protected]> Co-authored-by: Сергей Панченко <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
86f1338b86 |
feat(serve): batch workspace session live-state snapshots (#12513)
* feat(serve): batch workspace session live-state snapshots * test(serve): include batch live-state capability in integration baseline * fix(serve): close review gaps in the batch session live-state route (#12513) Index POST /sessions/live-state in the REST reference's persisted-catalog row so the docs contract sees the protocol section, give the batch route the single-workspace route's server-side records (routing-failure warn on untrusted members, daemonLog error on unexpected bridge failures), make the mid-batch disconnect guard observable with an AbortController and a macrotask yield between members, and move the duplicated member-failure, generation-currency, and bound literals into workspace-route-runtime for both plural batch routes. Six tests pin the new witnesses. Co-authored-by: Qwen-Coder <[email protected]> * fix(serve): align live-state scope docs and pin batch route guards (#12513) Co-authored-by: Qwen-Coder <[email protected]> * test(cli): pin grouped REST doc rows against bare SDK names and column shifts Co-authored-by: Qwen-Coder <[email protected]> * test(cli): fail closed on grouped REST doc rows and ambiguous SDK qualifiers Co-authored-by: Qwen-Coder <[email protected]> * test(serve): pin documented live-state batch bounds against silent drift (#12513) Co-authored-by: Qwen-Coder <[email protected]> * test(cli): isolate the suite from the operator's settings home (#12513) The autofix verification gate runs the packages/cli suite with the host's real HOME on a machine shared with other jobs. A malformed $HOME/.qwen/settings.json there (observed content: "X") makes every operator-settings reader fail closed at once — readOperatorSettingsScopes is designed to refuse a corrupted policy file — and one such file failed 408 tests across 15 files in the measured gate rejection. Pin a private empty QWEN_HOME per test-file process in the suite setup when the environment did not select one, following the file's existing doctrine for operator-owned environment. Tests that exercise QWEN_HOME set, stub, or delete it themselves. settings.test.ts keys every user-settings and .env path off a mocked os.homedir, so it now deletes an ambient QWEN_HOME explicitly; with any ambient value set, 13 of its tests fail regardless of this change. * test(cli): harden the QWEN_HOME suite pin and batch maxima test (#12513) - Pin on truthiness so an ambient empty QWEN_HOME no longer falls through to $HOME/.qwen, matching every production reader. - Sweep the pinned home from afterAll instead of an 'exit' listener that vitest workers never fire (51 leaked dirs on src/config -> 0). - Restore QWEN_DEBUG_LOG_FILE around loadCliConfig tests so the debug logger's fire-and-forget writes stop racing the sweep. - Witness that the suite never resolves settings from the ambient $HOME/.qwen, and assert member shape in the 20-selector maxima half so twenty failed members cannot read as twenty snapshots. * test(cli): keep AppContainer store work off the pinned per-file QWEN_HOME (#12513) * test(managed-agent): settle fresh group leaders before age-judged sweeps (#12513) A just-forked process can read in ps with procps's negative-elapsed wraparound (e.g. 441077234-00:18:40), which parsePsElapsed deliberately leaves undatable. An undatable leader judges every worker/group identity 'unknown', so an unwitnessed sweep conservatively holds the live process and rejects unproven — flaking the real-process sweep tests within milliseconds of the spawn. settledGroupTable now also waits for the leader's row to be datable, and the four sweep sites that judged a fresh process without the guard use it. * fix(cli): keep the QWEN_HOME test pin unconditional and alive for the process lifetime (#12513) The conditional pin honored an ambient QWEN_HOME, so agent-gate vitest runs read and wrote the operator's live settings home; the teardown sweep then deleted the live pin while un-awaited extension-store work was still reading it, turning Test (ubuntu-latest) red on ENOENT. Pin unconditionally, keep the pin for the process lifetime, reap stale pins by age at setup, await the floated extensions-list handler, and add teardown, pin, reaper, and memory-only-contract witnesses. Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): keep the QWEN_HOME teardown witness inside test-setup.ts (#12513) The previous commit registered the pin teardown witness as a second setupFiles entry in packages/cli/vitest.config.ts, which deterministic verification rejects as CI/verification machinery outside the PR footprint. Revert vitest.config.ts and fold the witness into test-setup.ts itself: vitest runs root-level afterAll hooks in reverse registration order, so the hook registered immediately after the pin runs last among this file's root hooks and still turns every file red if a teardown sweep of the live pin is reintroduced below it (mutation-probed). The separate witness setup file is deleted. Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> * test(integration): raise the installed distribution case ceiling The tui-parity installed-distribution darwin-x64 leg failed on the npx-lifecycle-enabled no-relaunch-fallback case with "Test timed out in 120000ms" (run 40537979533, job 122316223603). The five preceding cases averaged ~117s on that host, right at the ceiling: startup alone can spend the whole 90s ready-prompt budget there (#15677) and the /about retry window is another 90s, so 120s cannot contain a healthy case. Raise the per-case ceiling to 240s and keep the budget helper's note in sync. --------- Co-authored-by: 西岐 <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: qwen-code-ci-bot <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: qwen-code-autofix <[email protected]> Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost> Co-authored-by: qwen-code-autofix[bot] <qwen-code-autofix[bot]@users.noreply.github.com> Co-authored-by: Qwen Autofix <[email protected]> |
||
|
|
dba7d065ce |
feat(web-shell): expose archived export and restore inline (#15872)
* feat(web-shell): expose archived export and restore inline * test(web-shell): use inline restore in standalone E2E |
||
|
|
0976826090 |
fix(web-shell): align no-workspace category header with peers (#15897)
* fix(web-shell): align no-workspace category header with peers * test(web-shell): preserve archived no-workspace header presentation * docs(web-shell): show sidebar category header comparison |
||
|
|
a1f567a64f |
feat(web-shell): extend one-click update coverage and surface manual steps (#15849)
* feat(web-shell): extend one-click update coverage and surface manual steps * fix(web-shell): keep More menu open for the manual update panel * fix(web-shell): shape-neutral update guidance and drawer overlay exemptions |
||
|
|
64e3aa7a18 |
fix(web-shell): standardize workspace and session display copy (#15315)
* fix(web-shell): standardize workspace and session display copy * test(web-shell): sync workspace menu text expectations * docs(web-shell): clarify the scope of display terminology * docs(web-shell): align remaining display-label references * test(web-shell): match the renamed new-session action --------- Co-authored-by: 大知 <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
3131fcc230 |
feat(web-shell): allow embedding hosts to pick workspace directories (#15296)
* feat(web-shell): allow host workspace directory pickers * docs(web-shell): add host directory picker demo screenshots * fix(web-shell): preserve focus across host picker handoff --------- Co-authored-by: Jiankai Xiao <[email protected]> |
||
|
|
0ecdf0342d |
fix(serve): revalidate skill snapshots in the background (#14890)
* fix(serve): revalidate skill snapshots in the background * test(serve): include snapshot metadata in skills route response * fix(serve): preserve foreground skill observations * fix(serve): publish valid slow skill observations * fix(serve): retain skill snapshots when observations fail * fix(serve): report failed skill catalogs consistently (#14890) Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): trace discarded warm skills errors and pin observation age (#14890) * fix(cli): tighten skill snapshot cache and settings lock scopes Co-authored-by: Qwen-Coder <[email protected]> * fix(serve): correct preheat contract and skills cache invalidation (#14890) * fix(serve): evict invalidated skills enumeration and correct route docs (#14890) Co-authored-by: Qwen-Coder <[email protected]> * fix(serve): preserve skill toggle order and retained catalog fallback Co-authored-by: Qwen-Coder <[email protected]> * fix(serve): require an enumerated skill catalog for deletion (#14890) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: claudeethan <[email protected]> Co-authored-by: Ethan Ye <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
9a41afe0d4 |
feat: add persistent session Workflow mode (DW-04A) (#15422)
* feat(core): persist session Workflow mode and dispatch reminders * feat(sdk): expose session Workflow mode through native and daemon controls * feat(cli): add Workflow mode controls and end-to-end coverage * fix(cli): preserve pasted text when recalling prompt history * fix(sdk): complete Workflow mode routing and owner-safe replies * test: verify Workflow mode recording and Mod permission boundaries * test(sdk): select the secondary workspace ACP mount * test(cli): assert headless Workflow mode eligibility * fix(workflow): enforce owner trust for prompt mode writes * fix(workflow): keep daemon write policy out of MCP metadata * fix(workflow): preserve retry and restore state across lifecycle failures * fix(workflow): align ACP retry types with shared loop * test(workflow): isolate ACP cadence from followup suggestions * fix(sdk): close query after initialization failure * fix(cli): serialize workflow mode writes with session switches * fix(sdk): keep mode errors async and reject pending reinitialization * fix(cli): filter workflow mode from SSH raw session state * fix(cli): preserve workflow mode startup failures * fix(acp): refresh restored session config options * fix(core): rearm workflow notices after cancellation * fix(core): rearm workflow notices after failed switches --------- Co-authored-by: qqqys <[email protected]> |
||
|
|
6f53133d6d |
feat(web-shell): stream resumable BTW answers (#15709)
Co-authored-by: 钉萁 <[email protected]> |
||
|
|
a92b4efe72 |
feat(web-shell): show /autofix-pr watches and stop them from the Web Shell (#15668)
* feat(web-shell): show /autofix-pr watches and stop them from the Web Shell The daemon serves the /autofix-pr watches of a workspace's clone, as `qwen autofix-pr status --no-work` lists them: GET /autofix-pr/watches for the primary workspace and /workspaces/:workspace/autofix-pr/watches for a registered, trusted one, advertised as `autofix_pr_watches`. It finds the clone's main worktree with `git worktree list` and reads the watches' files only; runStatus takes that root and no session instead of the daemon's own directory and environment. The Web Shell shows each watch on the card of its scheduled task and on a new PR watches page in the sidebar: the PR, where the watch is (watching, fixing in the background, a fix left behind, not checked recently, the PR merged or closed, stopped), its pushes, its last check and round, and the items waiting for a person. For a watch run by a conversation of this Web Shell, Open session goes there and Stop watch sends `/autofix-pr stop <n>` to it, as a scheduled task's manual run sends its prompt. Docs: design Section 6.15 (EN and zh-CN), the user guide, and the serve protocol. * fix(web-shell): run no git for the watch listing, and match the gate's stale rule - The daemon's listing runs no git command but the one that finds the clone: runStatus takes `git: false`, which skips the gitfile and submodule checks as well as the worktree scan, and the view leaves out the worktree path, gitTrusted, worktreeWork and pushTarget. It names the clone (`root`). - State files, gate cursors and fix markers are read without following a link and without blocking, judged by what was opened: a FIFO put in their place can no longer hold the daemon, or a helper. - A task is stale as its gate judges it: the watch ended, records no task where the task has an id, or records another one, a task with no id included. - The PR watches page reads a locked workspace by its own route, finds a watch's task in any workspace it lists, lists a clone's watch once, and sends a watch no gate checks to a session here rather than a terminal. - The rail offers the page only when the daemon lists watches, and the view goes back to the chat when it stops doing so. - Docs follow. * test(web-shell): drive the PR watches page and a watch's task card in Chromium Against the mock daemon: the sidebar opens the page, each watch shows where it is, a watch run elsewhere or by nothing says where to act, and Stop watch sends /autofix-pr stop <n> to the session that owns it. The scheduled-task card of the watch shows the same state. * fix(web-shell): restore a watch's session in its own workspace, and fence the listing - Open session and Stop watch, and a scheduled task's run and history, restore the session in the workspace of the task bound to it: from another workspace's conversation the daemon refused the restore. The PR watches page finds that task in whichever workspace holds it. - The watch listing checks its runtime generation before and after it asks git: a workspace removed or replaced meanwhile lists nothing and answers 503 workspace_runtime_unavailable, on both routes. A failure is logged by the daemon. - A task left behind by a take-over is shown stale on its card, as its gate stops it. - The integration test's baseline capability list names autofix_pr_watches. * fix(web-shell): name the one workspace of a single-project daemon for a watch's session With one ordinary workspace the PR watches page and the scheduled-tasks page recorded no workspace for a task, so Open session, Stop watch and a task's Run now and history, started from a standalone or Live conversation, restored the session in that context rather than the workspace. Both pages now name the primary workspace when it is the only one; its route stays the unqualified one. * test(web-shell): capture the PR watches page and a watch's task card in the visual preview No visual scenario answered GET /autofix-pr/watches, so the before/after preview could not see either surface. The scenario serves three watches (fixing in the background with an item for a person, run from a terminal, merged with nothing running it) and the watch's scheduled task, against the fixed capture clock. |
||
|
|
eea44208b8 |
ci: run every suite but CLI and core in the side test lane (#15570)
* ci: run every suite but CLI and core in the side test lane
The Linux test job's main lane ran the CLI suite and then, one after
another, core, about twenty smaller workspace suites and scripts/tests,
while the side lane (web-shell, then the standalone regression) went idle
long before the CLI suite finished. The main lane now runs only the CLI
and core suites; the side lane runs every other workspace's test:ci, the
standalone regression and scripts/tests, each at half the Vitest worker
cap. Every side-lane suite runs even after another fails.
* ci: print the side lane of a test step killed before its replay
The lane script replays the side lane's log only after the main lane
ends, so a step timeout or cancel lost every suite but the CLI and core
ones. The script now writes that log to SIDE_LANE_LOG when the caller
names one; ci.yml names it under runner.temp, outside the TMPDIR the step
removes, and a failure()/cancelled() step prints whatever is left of it.
The lane test also checks that the main lane's output is present before
it checks its order against the replay, and a comment that still placed
scripts/tests in the main lane now names the side lane.
* ci: keep the lane script's log out of a nested run
scripts/tests runs `npm run test:scripts:unit`, which runs the lane script
again, inside the step that names SIDE_LANE_LOG. The nested run inherited
that name and truncated, replayed and removed the outer side lane's log, so
the outer replay had nothing to print. The script now unsets the variable
once it has read it, and the lane test drives a nested run with a named log
to pin that: the nested run must not see the caller's log, and the outer
side lane's own output must still reach its replay.
The print-step test also pins the `if [ -f "${side_log}" ]` guard around the
cat, which is what keeps that step quiet on an ordinary red test job.
|
||
|
|
7b69fccd74 |
feat(tools): let an explicitly configured search backend name its host (#15638)
`classifyDashScopeBaseUrl` accepted a fixed list of DashScope-family suffixes, so an organization that routes model traffic through its own gateway had no configuration-only way to point the built-in `web_search` backend at that gateway: the tool stayed off with an `unknown-host` notice, and the alternatives were patching the source or dropping the built-in tool for an MCP search provider (#15299). Add `tools.webSearch.allowedHosts` / `WEB_SEARCH_ALLOWED_HOSTS`: extra suffixes matched exactly like the built-in ones (exact host, or any subdomain of a listed suffix). Empty by default, so behaviour is unchanged for anyone who does not set it, and the `https:` requirement still runs first because the side request carries a bearer key. The list reaches only the two places where the user named the backend: the `WEB_SEARCH_BASE_URL` path and the `modelProviders` path. Automatic derivation keeps the built-in list in both of its host tests — the entry usability check and the auto-eligible host check — so a host nobody declared is never adopted implicitly and never receives the main provider's key. Entries are trimmed, lower-cased, de-duplicated, and entries without a dot are ignored, so a typo narrows the list instead of widening it (a bare `com` would otherwise accept every `.com` host). Both rejection notices now name the setting, which is what a user hitting the gate needs. |
||
|
|
98312b31cc |
feat(daemon): add cached workspace artifact queries and batch removal (#15594)
* feat(daemon): add cached workspace artifact queries and batch removal * fix(daemon): Share artifact cache with single-session queries * feat(daemon): Add artifact library session metadata and streaming --------- Co-authored-by: 丁炳智 <[email protected]> |
||
|
|
ad8a542013 |
feat(web-shell): manage extra workspace directories from settings (#15393)
* feat(web-shell): manage extra workspace directories from settings
The CLI's `/directory add` extends a session's workspace context with extra
directories, persists them at workspace scope, and reloads memory. Web Shell
had no equivalent: the settings panel never rendered
`context.includeDirectories`, and no daemon route exposed it.
Add a workspace-scoped REST surface backed by a new ACP control method. The
handler validates the path, applies the delta to the workspace config and every
live session, persists `context.includeDirectories`, refreshes hierarchical
memory when configured, and notifies the model. The Web Shell exposes the list
under Settings -> Workspace and reuses the existing directory dialog for path
input, autocomplete, and the native picker.
The route is trust-gated. SSH, managed, standalone, and experimental
paired-engine daemons do not advertise or serve it. The primary directory
cannot be removed; a session that cd'd into an extra directory is skipped with
a warning, and a directory that disappeared from disk can still be removed from
the list.
* fix(ci): align capability baselines and keep settings keys out of the transcript bundle
`workspace_directories` is a new registered and conditional capability, so the
ordered baseline lists in the unit and integration suites need it at the
registry position, and the conditional drift test needs a predicate branch.
The Web Shell category label no longer uses a `settings.category.*` i18n key.
The transcript build shares the i18n dictionary and deliberately carries no
`settings.` literals; the section title already exists as a non-settings key.
* fix(cli): keep workspace directory edits consistent with persisted settings
Three review findings on the workspace-directory mutation:
- Re-read the workspace settings layer before deriving the next list. The ACP
child's LoadedSettings snapshot can lag a write made by another owner, so an
add or remove could overwrite or miss entries already saved on disk.
- Persist with `throwOnWriteFailure` and before touching live roots. A refused
write now fails the mutation instead of reporting `changed: true` against an
unchanged file.
- Resolve entries the way runtime configuration does (`~`, relative paths
against the workspace root, then realpath) for display and matching, while
persisting the original spelling. A relative entry such as `../first` is now
removable from the UI, and removal still matches the persisted entry.
* fix(cli): resolve workspace directory entries like runtime settings
The second review round found two remaining P2s and one test-contract gap:
- Read, match, and remove entries from the settings layer with environment
variables resolved (`workspace.settings`), so a `$VAR` entry is listed as the
path sessions use and removal clears that path from live contexts. Persist
the original spelling from `originalSettings`.
- Refuse adding the workspace's primary directory (`primary_directory`); it is
always part of the workspace and previously persisted as an extra entry that
DELETE then refused as `initial_directory`.
- Accept relative and `~` removal paths in the handler, not only in the route,
and test the downstream handler with the relative spelling. The route test's
bridge mock had masked that the handler still required an absolute path.
* test(cli): canonicalize temp roots in workspace directory tests
The route and handler fixtures composed expected paths from the raw
`os.tmpdir()` spelling. On macOS that is `/var/...` while the implementation
resolves entries through `realpath`, which returns `/private/var/...`, so the
three new cells failed only on macOS.
Realpath the temp root once at setup in both files, so fixture paths and the
implementation's resolved output share the same spelling. No production code
changes.
* fix(cli): list workspace directories through the selected runtime environment
A directory variable defined only in the workspace's `.env` reaches the ACP
child and its settings but not the daemon process. The GET route read settings
with `skipLoadEnvironment`, so it interpolated against the daemon's environment
and listed the unresolved placeholder while live sessions used the real
directory; deleting that displayed path answered 200 with `changed: false`.
Read the workspace scope through a settings snapshot built from the selected
runtime's environment (and its trust), matching what the mutation handler and
its sessions resolve. Cover a variable that exists only in the runtime overlay.
* fix(cli): drop the daemon-environment fallback from the directories list
The process.env guard in `packages/cli/src/serve/**` only permits registered
accesses. Reading `process.env` as the parent-process fallback for the
directories list tripped it and failed the Test lane.
Only a runtime overlay carries a workspace `.env`, so the overlay environment
is the whole source: a runtime without one resolves the persisted list against
an empty environment, which keeps placeholders rather than silently borrowing
the daemon's process-wide variables.
* fix(web-shell): close the four Critical review findings
- SSH detection used a `ssh://` cwd prefix, but registration rewrites an SSH
workspace to a local mirror directory before it is served, so the gate never
fired and the section rendered against a boundary that answers 501. The route
now tests `routeFileSystemFactory.sshWorkspace` (as `capabilities.ts` does)
and the section tests the workspace entry's `ssh` marker.
- The route mapped every unclassified bridge failure to a flat 500. The first
mutation on a freshly opened workspace spawns a control channel, and that
spawn times out or is refused while the child starts; those carry none of the
feature's error kinds. The shared mapper is now the fallback, so a retryable
state keeps its 503/504 + Retry-After semantics.
- The section did not serialize mutations: Add was gated only on the initial
load, so an add could race a pending remove over the same settings
read-modify-write and silently drop one change. One gate now covers both
directions, with a ref so it holds before the next render.
- Browse rendered unconditionally, dropping the `native_directory_picker` gate
the same dialog gets in App; a headless daemon serves this section but answers
the picker with 501. Browse now follows the capability.
Tests pin each: the SSH route case uses the production shape (local mirror cwd +
runtime marker), the section case uses the capability marker, the fallback case
runs the real shared mapper, and a deferred remove asserts Add is disabled.
* fix(ci): type the shared-mapper mock and split the home-root trust gate
The prior commit typed the test harness's `sendBridgeError` option as a loose
`(res: unknown, ...)` function, which `tsc --build` rejects when it is handed
the real `SendBridgeError`: the whole `prepare` build failed, so every CI job
that installs dependencies went red. Type the option as `SendBridgeError`
instead.
Also separate two states the handler conflated: `workspaceSettingsActive` is
false both for an untrusted workspace and for a trusted workspace rooted at
`$HOME`, and only the first is a trust failure. Trust is now read from
`config.isTrustedFolder()`; a home-rooted workspace answers
`directory_management_unsupported` rather than an `untrusted_workspace` code no
grant can clear. `makeConfig` gains the `isTrustedFolder` the handler now calls.
The section seeds the dialog with the workspace root so browse mode lists
folders on open, and its placeholder no longer advertises an `ssh://` spelling
this route rejects.
* fix(web-shell): gate the settings category on the SSH marker in App
The Settings root still decided whether to supply the Workspace directories
section from `workspaceCwd.startsWith('ssh://')`. Registration rewrites an SSH
workspace to a local mirror directory, so that test never fires: for a
registered SSH workspace App passed a truthy element, SettingsMessage created
the category, and the child section then rendered nothing — a visible, selected
category with empty content and no list request.
Decide it from the selected workspace entry's `ssh` marker, the same signal the
section uses, so App passes `undefined` and the category is omitted.
* fix(cli): pair workspace directory entries by tail, not by index
`workspaceFile.settings` is the merged layer, and `context.includeDirectories`
merges with `MergeStrategy.CONCAT`, so a user-scope value lands ahead of the
workspace entries. The handler paired that merged array with the
workspace-scope array by index, so once a user scope also set the key, a
workspace entry resolved to an unrelated user path: the panel showed the wrong
directory and removal left the real root live in every session.
The workspace entries sit at the END of the merged array, so take that tail
instead. Each workspace entry then resolves to itself, and a `${VAR}` entry
still matches the path sessions use.
The regression case seeds a user-scope prefix ahead of one workspace entry and
asserts the removal clears it from both live contexts; index pairing fails it.
|
||
|
|
3eff5f7793 |
feat(web-shell): create workspaces from a Git URL (#15361)
* feat(web-shell): create workspaces from a Git URL
The Add-workspace dialog could only register an existing directory or
create an empty scratch directory, so starting from a repository meant
leaving the browser, cloning on the daemon host, and registering the
path afterwards.
Add a Git source that starts an asynchronous clone job, reports progress,
and supports cancellation. A successful clone is registered through the
existing workspace route, so capacity, nesting, persistence, and the
daemon's trust policy stay in one place.
* fix(cli): classify git_workspace_clone in the capability parity matrix
The parity matrix landed on main after this branch, and every capability
tag needs a row. Managed workspace creation only creates empty
directories, so record the clone surface as an explicit Managed gap
rather than claiming parity.
* fix(cli): reserve active clone targets before overlapping work
An outer clone's failure/cancel cleanup removes its target recursively,
so a nested clone or workspace registration must not start underneath a
running job. Reserve the canonical target for the job lifetime, reject
overlapping clone and registration paths, and release the reservation
only after cleanup settles.
Also resolve the success path before publishing `succeeded`, so a reader
never observes a terminal snapshot without the `cwd` it registers.
* fix(web-shell): keep clone polling recoverable
A failed poll left the last known status running, so the panel offered
Cancel instead of the recheck that can recover the existing job. Prefer
the unknown-state action.
Reset the unmounted guard in the effect setup: StrictMode's development
replay otherwise leaves it set and discards every later poll, including a
successful clone.
* feat(web-shell): reuse the directory path as the clone parent
When the source switches to Git, treat the directory path as the clone
parent and append the repository name from the URL. A path whose basename
already matches stays as-is, and a manually edited or browsed target is
never overwritten by later URL edits.
* feat(web-shell): suggest folders for the clone target
Headless daemons do not provide a native directory picker, so the clone
target had to be typed in full. Reuse the directory suggestion list:
selecting an existing folder uses it as the parent and appends the
repository name, while a manually edited target still wins.
* fix(cli): keep clone progress monotonic
git restarts the percentage for some clone phases (remote counting,
then receiving), so the Web Shell progress bar could jump backwards.
Clamp the reported percent to the running maximum; the status message
still follows the current git line.
Also pin the clone admission guards left unpinned by review: reserved
roots, unknown request keys, promotion of a workspace under an active
clone target, file URLs that name a host, percentages above 100, and
the dialog fallback when the daemon capability disappears.
* fix(cli): reserve the clone target before claiming it
The claim created the target with mkdir, then awaited the existence check
and the final realpath, and only reserved the canonical path afterwards.
A concurrent ordinary registration could admit the freshly created
directory in that window and then lose it to the clone's cancel/failure
cleanup.
Reserve the canonical target inside claimCloneTarget before mkdir, share
the reservation with registration, and release it on every claim failure
and on post-claim admission failures. Add a regression test that overlaps
the registration with the blocked claim and a unit test that asserts the
reservation exists before the directory does.
Also update the serve integration capability envelope for the
host-conditional git_workspace_clone tag, mirroring the daemon's probe.
* fix(cli): register git_workspace_clone in serve capability guards
Test (ubuntu) failed five capability-registry guards because the new
git_workspace_clone tag was not registered alongside the existing
conditional features:
- document it in the qwen serve protocol table;
- allow the clone spawner's process-environment copy and SystemRoot
lookup in the serve process.env guard;
- add it to EXPECTED_REGISTERED_FEATURES and the conditional-feature
drift-insurance branch.
Verified: capabilities-docs-contract, process-env-guard and server
suite 1399/1399; typecheck, ESLint, Prettier clean.
* fix(cli): keep clone targets away from reserved and Live paths
A clone target that contains the reserved Conversations root was accepted
when that root did not exist yet: creating the target made the root's
parent part of the clone, and the Live runtime could then be published
inside it. Because the Live exemption skipped the "canonical inside
target" direction for active clone targets, cancelling or failing the
clone deleted the registered Conversations workspace.
Reject clone targets that contain a reserved root, even before the root
exists, and apply strict two-way nesting to active clone targets in the
owned-runtime admission regardless of provenance.
Verified: workspace-git-clone + workspace-management 196/196;
typecheck, ESLint, Prettier clean.
* fix(cli): resolve reserved roots through existing ancestors
The reserved-root containment check fell back to the unresolved spelling
when the Conversations root did not exist yet. A symlinked ancestor such
as ~/Documents then made the canonical clone target and the lexical
reserved path disagree: the clone was accepted, the Live runtime created
the root inside it, and cancelling the clone left ConversationWorkspace
with a cached identity for a directory that no longer existed.
Resolve each path through its nearest existing ancestor and append the
missing suffix before comparing, so a clone target containing a missing
reserved root is rejected before mkdir even when ancestors are symlinks.
Verified: workspace-git-clone + workspace-management 197/197;
typecheck, ESLint, Prettier clean.
* fix(cli): cover case aliases and dangling links in reserved roots
The reserved-root guard compared the reconstructed missing suffix with
case-sensitive isWithinRoot, so a case-variant ancestor such as
`Documents/qwen code` did not match `Documents/Qwen Code/Conversations`
on a case-insensitive filesystem. It also walked past a dangling symlink
ancestor, so a clone target that was the link's missing destination
could later make the link valid inside the clone's cleanup scope.
Compare canonical paths case-insensitively after resolving through the
nearest existing ancestor, and follow dangling symlinks while
reconstructing the missing suffix. Add regressions for both aliases.
Verified: workspace-git-clone + workspace-management 199/199;
typecheck, ESLint, Prettier clean.
* fix(cli): harden git clone credentials, config, and admission
- Strip URL credentials before git's argv and supply them through a
command-line credential helper backed by the child environment, so the
secret no longer appears in /proc/<pid>/cmdline or .git/config.
- Disable global/system git config for the clone child so a repository's
.gitattributes cannot run host-configured filter commands during checkout.
- Keep the first fatal/error stderr line for clone failures instead of the
trailing SSH boilerplate.
- Only treat Receiving objects/Resolving deltas as progress phases, so
remote counting cannot pin the bar at 100.
- Strip OSC/C1 and all other control characters from clone messages; add
the missing Windows taskkill 'error' listener.
- Require operator authority for clone cancellation; reject clone targets
inside the managed scratch root or equal to a registered/in-flight
workspace; keep case-insensitive containment scoped to clone targets.
Verified: workspace-git-clone + workspace-management 205/205;
typecheck, ESLint, Prettier clean.
* fix(web-shell): gate the clone flow during submit and IME input
- Disable the post-clone Back button while registration is in flight and
guard backToForm, so it cannot reset a submitting flag owned by onAdd.
- Ignore clone-target key handling while an IME composition is active.
- Dismiss the clone-target suggestion list when the git form is submitted,
so its Escape interceptor cannot stay armed over the progress view.
Verified: AddWorkspaceDialog 57/57; Prettier and ESLint clean.
* docs: align git clone guarantees with credential and config isolation
- Document the `{ kind: "git" }` form and its 202 job response in the
`POST /workspaces` reference.
- State that URL credentials are moved out of git argv and the stored remote
URL, and that global/system git config is disabled for the clone child so
committed filters cannot run host commands during checkout.
- Mirror both changes in the Chinese design document.
* fix(cli): use bracket access for clone credential env
The strict index signature on NodeJS.ProcessEnv rejects dot access to
QWEN_GIT_USERNAME/QWEN_GIT_PASSWORD; all CI jobs that build the CLI
failed at tsc with TS4111 before running any test.
* fix(web-shell): cancel the running clone from the left action
- Route the progress panel's left Cancel through cancelClone while the job
is running, so it stops the daemon job instead of only closing the dialog.
- Clear the pending poll timer at pollCloneJob entry and before cancel, and
guard registration per job id, so a terminal snapshot cannot register the
same clone twice.
Verified: AddWorkspaceDialog 57/57; Prettier and ESLint clean.
* fix(web-shell): harden git clone env and clone-job dialog states
Pin LC_ALL/LANG for the clone child and sweep host Git config channels (GIT_CONFIG_*, GIT_ASKPASS, GIT_SSH*, GIT_PROXY_COMMAND) so a localized git still reports parseable progress and a browser-chosen URL cannot borrow host credentials. Decode URL credentials defensively, reject control characters, and print the inline credential helper with printf so dash does not mangle tokens.
In the add-workspace dialog, tear the clone state down when the capability disappears, report a pruned clone job instead of offering recheck forever, honour a cancel that loses the race with a finished clone, and close instead of cancelling an unconfirmed job. Correct the host-authentication prose in the design docs, README and URL hint to match what the isolated clone child actually supports.
* fix(cli): resolve relative symlinks physically and keep cancel intent
- Resolve a relative symlink target from its physical parent before appending
the missing suffix, so a dangling relative Documents link cannot hide the
reserved Conversations root from clone admission.
- Keep the per-job cancellation intent across overlapping poll responses, so
a second succeeded poll cannot auto-register after Cancel. Clear it only on
an explicit Back/new submission.
Verified: route + clone 209/209; AddWorkspaceDialog 61/61; typecheck with a
6 GB heap; Prettier and ESLint clean.
---------
Co-authored-by: verify <verify@local>
|
||
|
|
1df5031e70 |
perf(ci): overlap web-shell and standalone tests with the CLI suite (#15438)
* perf(ci): overlap web-shell and standalone tests with the CLI suite The Linux Test job ran every workspace suite, the standalone runtime regression and scripts/tests one after another. The CLI suite keeps its workers busy for 13-20 minutes, while web-shell's suite spends most of its run on App.test.tsx alone and the standalone regression starts one runtime process at a time, so those two added their mostly idle minutes straight to the job. Run them in a side lane beside the other workspaces instead. .github/scripts/ci/run-unit-test-lanes.sh starts web-shell and the standalone regression in the background, runs every other workspace's test:ci (listed with npm pkg get, in npm's workspace order) and then scripts/tests, and replays the side lane's log after the main lane so the two never interleave. The step runs the script under the same setpriv wrapper, so every test command still runs without the DAC override capabilities. test:scripts now reaches its Vitest run through a new test:scripts:unit script with a trailing --, so the main lane can run it without the standalone regression while test:ci, test:ci:workspaces and test:scripts run what they ran before. * fix(ci): name the cause when the lane script cannot start a lane - Fail with an ::error:: line when mktemp cannot create the side lane's log file, instead of running the main lane alone and dying in tail. - Check the exit status of `npm pkg get` and of the parser. A failing `npm pkg get --json` prints an error object, which used to parse as an empty listing and report "Found no workspaces". - Note in the header that the replay puts side-lane failures after the main lane's in main-failure-signature.mjs's title and search markers. - Lane test: clear the caller's NODE_OPTIONS, assert the main lane's output is present before checking its order, and cover the failed listing and the missing temp dir. - Cross-check npm's workspace resolution against the selector in scripts/workspaces.js. * fix(ci): halve web-shell's worker cap beside the main lane Each Vitest run applies VITEST_MAX_FORKS / VITEST_MAX_THREADS on its own, so running two lanes at once doubled the step's worker cap, and on the hosted runners, where no cap is set, put two all-CPUs pools on 4 vCPU. The lane script now runs web-shell at half the cap, at least 1, and at 2 where the cap is unset; the main lane keeps the full cap. - Refuse a workspace name with whitespace in it: the main lane's arguments are read one per line, so a line break would split a name into other arguments, flags among them. - Lane test: web-shell and the main lane's suites now wait for each other, so a side lane serialized before or after the main lane fails; record each call's worker caps; run the script against npm's real listing and compare its main lane with the selector the release pins use; cover a name with a line break in it. - ci.yml: the standalone runtime cache comment names the lane script's side lane, not test:scripts, as what runs the regression in this job. - ci-variables.md: say the cap applies per Vitest run and what the two-lane step runs at its peak. - release-workflow.test.js: follow test:scripts through test:scripts:unit to vitest; #15102 pinned the shape before the split. |