mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
351b5fc51a013b4de2062327525f4f371d1d2ae5
9706
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
351b5fc51a |
ci(lint): stop new cli imports from the core package root (#11798)
Importing a value from the @qwen-code/qwen-code-core root evaluates all of core in every test whose import graph reaches the importing file, which is where most cli test time goes (#10908). The existing rule stops at packages/core/src, so cli kept adding root imports: 446 production files at #10957's merge, 465 eight days later. Enable @typescript-eslint/no-restricted-imports for cli production files with type-only imports allowed, reusing the barrel specifier set from no-core-root-barrel-import. The 372 files that value-import the root today are listed in eslint.legacy-core-barrel-imports.mjs; the list only shrinks. The new file is registered wherever eslint.legacy-filenames.mjs is: the lint-gate freshness list, the autofix sensitive-file classifier, and the hygiene fix agent's deny list. ESLint and Prettier (--experimental-cli) were run on the changed files, and the new block was exercised through ESLint's API against the real eslint.config.js. The scripts/tests vitest suites were not run locally. Refs #10908 |
||
|
|
f71a4e797e |
chore(pnpm): scope build approvals, pin the ACP zod peer, add a freshness lane (#11797)
Follow-ups left open when #11625 merged: its three unresolved review threads, the deferred finding in #11681, and #10524. - allowBuilds decisions now honour the whole key the way pnpm does: a bare name covers every version, an exact `name@version` (or a `||` union) covers only those, and a range or source-like key decides nothing. Reducing a key to its name let an approval scoped to one version cover the next (R1-10). - Pin `@agentclientprotocol/sdk>zod` to 3.25.76 so a pnpm worktree runs the zod the npm tree installs. Under zod 4 the SDK's own `instanceof z.ZodError` test stops matching and a malformed request answers -32602 instead of -32603; both versions are npm-locked, so the agreement gate cannot see the split (R1-1). - Cover web-shell's shipped client sources with the undeclared-import rule; it is the one published package whose sources are not under src/. Measured cost today: zero violations (R2-6). - Add a path-filtered workflow that regenerates pnpm-lock.yaml with `corepack pnpm import` and fails when the committed file differs, which is the freshness half set membership cannot cover (#11681). - setup-worktree: accept only an executable file on PATH, so a corepack left without its exec bit reports the actionable error instead of EACCES (#10524). - Name the esbuild divergence an accepted parity gap, and record what `fsevents: false` costs a macOS worktree. |
||
|
|
32a25efa6e |
fix(cli): resolve ACP core settings against the active target dir (#11647)
* fix(cli): resolve ACP core settings against the active target dir The `qwen/settings/getCore` and `qwen/settings/setCoreValue` handlers in `extMethodInternal` fell back to `process.cwd()` when no explicit `cwd` was passed. In a worktree-backed ACP session the daemon runs from the project root, so workspace-scoped settings were written to `<projectRoot>/.qwen/settings.json` instead of the worktree's own `.qwen/settings.json`. Fall back to `config.getTargetDir()` instead, which `relocateWorkingDirectory()` keeps pinned to the active session workspace (including worktrees). An explicit `cwd` still wins. Fixes #8138 Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-issue-patrol/jmtwqd66dxl * fix(cli): resolve ACP settings from the requesting session's target dir The `qwen/settings/*` + `qwen/permissions/*` handlers resolved their workspace root from `process.cwd()` or the daemon's bootstrap `this.config.getTargetDir()`, both of which stay pinned to the boot workspace. A worktree session relocates *its own* Config, so workspace-scoped writes landed in `<projectRoot>/.qwen/settings.json` instead of the worktree's own `.qwen/settings.json` (#8138). Resolve the workspace root from `session.getConfig().getTargetDir()` (falling back to the bootstrap Config when the client sends no `sessionId`) and apply it uniformly across the settings/permissions surface. An explicit `cwd` still wins. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtwvd7nnxt * fix(cli): consolidate duplicate @qwen-code/qwen-code-core imports Merge ExtensionManager and the type-only Config/GoalSnapshotV2 into the single existing value import so import/no-duplicates passes under --max-warnings 0. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmtwy83e5xx * fix(cli): resolve getMemoryPaths against the active session target dir Align qwen/settings/getMemoryPaths with its getMemory/setMemory siblings by resolving through settingsCwdFor, so the project memory file points at the requesting session's worktree rather than the daemon boot dir. Correct the settingsCwdFor doc comment, which overstated the no-sessionId fallback as preserving process.cwd() behaviour. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxadc55yg * fix(acp): fail loud on unresolvable sessionId, drop per-request env injection settingsCwdFor previously treated an unresolvable sessionId as absent, silently retargeting every qwen/settings/* and qwen/permissions/* read and write to the daemon's bootstrap workspace (a workspace the caller never selected, with its own trust decision). Reject an unresolvable id with errorKind session_not_found instead, keeping the no-sessionId fallback that the workspace-service setRules path depends on. The twelve rewired handlers also called loadSettings without options, so a session-scoped read injected the foreign workspace's .env into the daemon's process-wide process.env. Route them through a shared loadRequestSettings that sets skipLoadEnvironment (and states consumeCorruptionEnvVars explicitly, since an options object replaces loadSettings' = true default). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxixz7myu * fix(cli): scope ACP settings adoption and permission sync to the session workspace Resolve the requesting session's stable workspace root (its bound storage) instead of the live cwd, so session/cd no longer repoints the twelve settings handlers at a subdirectory holding no .qwen/settings.json (R2-2). Only publish a freshly loaded LoadedSettings into the process-wide this.settings cache when it was loaded for the daemon's own workspace, so a bare sessionId read cannot repoint the daemon-global consumers (R2-3). Scope syncLivePermissionManagers fan-out to the workspace a workspace-scoped setRules write landed in, instead of broadcasting the delta to every live session (R2-5). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxn8asoz1 * fix(cli): honor explicit cwd and reject malformed sessionId in ACP settings resolver Reorder `settingsCwdFor` so an explicit request `cwd` wins before the `sessionId` lookup, fixing the R2-1 regression where a request that names its workspace is rejected over a field the resolver never uses. Distinguish a present-but-malformed `sessionId` (number/object/empty string) from an absent one so it fails with `invalidParams` instead of silently retargeting the bootstrap workspace (R3-3). Restore the lost fallback-branch pin and add the both-fields and malformed-sessionId cases the review asked for (R3-4). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxrimarz8 * fix(cli): name and pin the session workspace root, and cover the permission fan-out filter Extract sessionWorkspaceRoot(config) to give the admission-bound storage root (storage.getProjectRoot()) a name distinct from the live getTargetDir() and Config.getProjectRoot(), and route settingsCwdFor and the workspace-scoped permission fan-out through it. This is a pure refactor of the two inline reads. Add two regression cases that pin the fan-out filter against the two mutants that previously survived green: a user-scoped setRules delta must still reach every live session across workspaces (the Workspace guard must stay conditional on scope), and the requesting session must survive its own filter after a session/cd moved its live cwd into a subdirectory (the filter must key on the stable root, not getTargetDir()). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmty039h3zl * fix(cli): keep MCP/hook/extension writes on the workspace-global coordinate The MCP status/apply routes (buildManagedWorkspaceMcpStatus, reloadWorkspaceMcpDiscovery) and the hooks/extensions status routes resolve only the bootstrap workspace. Resolving sessionId on the write side made a handler answer "saved" for a server no route would ever list or apply, so setMcpServer, removeMcpServer, setHook, removeHook and setExtensionSetting now resolve only an explicit cwd (R3-2, option b). Session-aware resolution stays where write and read-back share the coordinate (core settings, memory, permissions). Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): canonicalize workspace path spellings in permission fan-out syncLivePermissionManagers and adoptRequestSettings compared workspace coordinates with path.resolve only, so a grant revoked under a different spelling of the same directory (symlinked /tmp vs /private/tmp, or a different case) never reached the live session's permission manager while the caller was told it saved — fail-open. Canonicalize both sides with realpathSync.native (path.resolve fallback) plus a darwin/win32 case-fold. Also treat a present-but-empty cwd as absent in the five workspace-global settings handlers (|| instead of ??), matching the session-aware handlers. Co-authored-by: Qwen-Coder <[email protected]> * style(cli): apply prettier formatting to the ACP agent files The Prettier lane flags both files touched by this PR (a JSDoc continuation line and an over-width path.join call), which fails Lint & Static before any later step runs. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmu0kyvtn3f * fix(cli): rely on realpath casing instead of folding case in workspace comparison realpathSync.native() already returns the on-disk casing on macOS and Windows, so the toLowerCase() on top was redundant on case-insensitive volumes and actively wrong on case-sensitive ones: proj and PROJ are genuinely different workspaces there, and folding authorised one under a rule written for the other. Also pin the removal direction of the cross-spelling fan-out and clean up the test symlink. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
6d7b5e19fc |
fix(cli): treat write-side PTY EIO as a benign teardown race (#11788)
isExpectedPtyRaceError only tolerated the read side of the pty teardown race, so a `write EIO` fell through to writeStderrLine + process.exit(1) and killed the TUI. That is exactly what Ink's throttled log write raises once the terminal is closed or detached: node delivers it asynchronously out of the stream write callback, so it reaches the process-level uncaughtException handler as `Error: write EIO` with code EIO. Direction does not change the class of the race, so the EIO branch now matches the write side using the same shape it already used for the read side. Everything else about the classifier is untouched: the read-side cases, ioctl EBADF and the resize-after-exit message stay benign, and any non-pty error (including an EIO whose message names neither read nor write) is still reported to stderr and still fatal. Reproduced against a real pty (python pty.openpty master, node child with stdout/stderr on the slave, then close the master): with origin/main the child dies with exit code 1 on the first `write EIO`; with this change the identical error is classified benign and the child keeps running. EAGAIN is deliberately left read-only. Flooding a pty whose master is never drained never surfaced a `write EAGAIN` here — libuv queues tty writes rather than reporting backpressure to JS — so a write-side EAGAIN branch would be dead code. Refs #11783 Patrol-Run: qwen-issue-patrol/jmu04jbjw2s Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
60be57673d |
fix(goal): keep a full-claim checkpoint replay from spending a stall, and tidy batching follow-ups (#11796)
- A restore replay was exempt from the stall breaker only when it failed. One that came back with a full claim list on an overflowing window still added a stall, so a session restored one stall short of the limit was stopped at activation before any turn of its own ran. The success arm now keeps the restored streak too, while still recording the diagnostic and writing the checkpoint. - goals.md says the timeout bounds each model call, not the whole check, and that a batched check's wall time is charged to goalMaxActiveMinutes. - The restore-replay tests share one crash-state harness, the batchSizes helper states the split rule the code evaluates, and a test pins that a pause between batches sends no further batch. |
||
|
|
1291d2a68d |
fix(dingtalk): remove background agent headings (#11800)
Co-authored-by: qqqys <[email protected]> |
||
|
|
f9534f4395 |
fix(web-shell): make the cockpit visual capture deterministic (#11786)
Two independent sources of run-to-run nondeterminism landed on session-workflow-cockpit-*, and neither was visible to the suite. The artifact dock's 200ms open slide ran live during captures because the suite renders under prefers-reduced-motion: no-preference, so the CSS opt-out the app already ships never applied. Measured at the mutation that mounts the dock: artifactPanelDockOpen is running at currentTime 0 with the dock at width 0 and the divider at x 1276, settling at width 504 / x 772. No assertion in any screenshot spec gates on the dock, so whether a capture lands inside those 200ms is down to how long the pre-capture waits happened to take. The cockpit also focuses its own back button on mount, and whether Chrome paints the :focus-visible ring for a programmatic focus is a heuristic. Over five captures of one unchanged tree the ring appeared in three of the five light renders and three of the five dark ones; run1 vs run2..5 through web-shell-visuals-compose.mjs scored CHANGED 0.05% in 4 of 4 pairs, with all 499 differing pixels inside the ring's own box and the button's border and label byte-identical. Force reduced motion through use.contextOptions and give the harness a clearFocus that the cockpit spec calls before its capture. Two details that are easy to get wrong and are now pinned by visual-capture-contracts.test.ts: - reducedMotion is a browser.newContext() option, not one of the runner's own use options. Written as a bare sibling of viewport it is silently dropped: project.use.reducedMotion reads back as 'reduce' while the page still reports no-preference (measured on @playwright/test 1.61.1). - clearFocus stays per-scenario. Called from captureScreenshot it moved 11 of 68 views, slash-menu-dark by 30.1%, because that menu is open precisely because the composer has focus. After both: 0 of 4 compose pairs changed, dark is byte-identical across 5 runs, and the full 4-spec render moves 1 of 68 views against the pre-fix render -- session-workflow-cockpit-dark at 0.05%, which is the ring going away. Patrol-Run: qwen-issue-patrol/jmtzy3u652k Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
7e0beb9d18 |
fix(skills): re-apply a Skill's side effects when a session is resumed (#11280)
* fix(skills): re-apply a Skill's side effects when a session is resumed A resumed session restored a Skill's instructions without restoring the state that enforces them: the restore path only rebuilt the dedup sets, while session hooks and allowedTools allow rules live in per-process memory. So --continue / --resume came back with the Skill's body in the model's context and its PreToolUse gate gone, silently. Each restored Skill now goes through the same shared side-effect helper both live invocation paths use. Two refusals carry over from those paths, so a resume never re-arms what a fresh invocation would refuse today: a Skill disabled since it was invoked, and a conditional (paths:) Skill whose activation has not fired in this session. Folder trust is re-checked inside the helper, and both registrations dedup, so the replay cannot stack duplicates. Bookkeeping stays unconditional, since the body really is in the restored context. The paths that still do not restore now say why in the debug log — at warn when the Skill declares hooks:, matching the existing rule — instead of falling through silently. A Skill started with /<skill-name> leaves no tool-call record to key on and is documented as a limitation. Fixes #11180 * fix(skills): scope a restored Skill's grants to its session and name what resume declines Review round 1 on #11280. A skill's `allowedTools` were granted as process-wide session allow rules that no session transition clears, while its hooks die with the session they were registered for. Re-applying them on resume made that gap reachable from a failed `/resume`: the rollback returns the user to the old session with the abandoned one's grants still live. Grants made for a skill now carry the session they were made for, and the permission manager filters on it alongside the existing folder-trust check, so `/clear` and a session swap drop them the way they drop the skill body. The dedup contract widens the same way trust gating does. Restore also declined more cases than it explained, and explained one it got wrong: a same-session re-invocation records the dedup message rather than a body, and every such response was reported as an edited SKILL.md — a steady-state warning pointing at a file that never changed. An already restored skill is now a no-op, and the mismatch reason is chosen by whether the recorded output is a skill body at all. A skill hidden from the model since it was invoked is no longer re-armed: frontmatter is not part of the recorded body, so that change is invisible to the match. Also: collect the cached skills once instead of twice per resume, pin the two `await`s and the mismatch log with tests that red without them, scope the integration test's send wait to the request carrying its prompt, and correct the skills doc — resume re-applies both halves, a frontmatter-only edit is restored, and the cases that log elsewhere or not at all are named. * fix(skills): single-source the skill availability rule resume re-checks Review round 2 on #11280. Resume decided whether to re-arm a restored skill's `allowedTools` and `hooks:` from its own copy of the model-facing availability rule, which the availability filter also hand-rolls. Two copies of one rule drift, and this pair already had: the filter's visibility condition was missing from the resume copy until last round. Both now ask `skillModelInvocationBlock`, which returns which condition failed so the resume path can still name it in the log — the reason that branch exists. Every condition is read live off `Config` / `SkillManager` rather than off the snapshots an unsequenced `refreshSkills()` commits, and `pendingConditionalSkillNames` keeps its own filter, which is this trio minus the activation check. The session scoping added last round was pinned only against a stubbed `PermissionManagerConfig`, leaving the wiring it depends on — that the id tagged onto the grant is the one `startNewSession` replaces — untested. A `config.test.ts` case now drives a real `Config`, a real `PermissionManager` and `applySkillSideEffects` across a session boundary, and reds if either the scoping or the id passed through `applySkillSideEffects` is removed. Both `PreToolUse`-gate integration suites drove copies of one fixture, already diverged in the place that decides their assertions: only the resume suite's gate wrote the hit counter, and nothing at the parity call site said why. The skill, the gate script (counter always written), the fake-model env stubs, the launch args, the poll helper and the exit sequence move to `integration-tests/helpers/gated-skill-fixture.ts`. The two fake models stay separate: parity dispatches on a request counter, resume on the last user message, because its second session replays the first one's turns. * fix(skills): scope the AUTO stash, key resume declines on the record Round 2 review follow-ups. R1-1 (Critical): `addSessionAllowRule`'s AUTO-mode stash branch deduped on `raw` and returned before the live re-point, so a dangerous `allowedTools` grant stashed in session A kept A's id; leaving AUTO re-attached that same object and the session filter dropped it for good. It now updates the stashed entry in place, never pushing it into `sessionRules.allow` while AUTO is on. R2-3: `activeSessionAllowRules()` was the only reader applying the session predicate. `getAllowRawStrings()` (the `StartSessionEvent` at a session boundary) now goes through it, and the AUTO strip candidates go through the new session-only half — not the full predicate, so a mid-AUTO trust grant cannot activate an unstripped dangerous rule. R1-3: `skillModelInvocationBlock` checks `disableModelInvocation` before `isSkillActive`. Such a skill never enters the activation registry, so `inactive` is permanently unsatisfiable and masked the cause that explains the decline. R2-4: the decline log is deferred to after the history scan and emitted only for skills nothing later restored, so a stale first pair no longer warns "your gate is gone" about a gate the same loop arms. R2-7: `logSkillNotRestored` keys on the record rather than the declaration. A non-body record (a recorded refusal) logs at debug saying nothing was ever armed; the warn level now covers `allowedTools` as well as `hooks:` (nine bundled skills declare the former and none the latter); and each reason names the route that works — `/<skill-name>` for inactive and hidden, `/skills` first for disabled. R1-5, R2-1, R2-6: the docs bullet, the `sessionId` JSDoc and the `SkillCommandLoader` assertion comment now describe what the code does — a recorded-response mismatch covers a moved skill directory as well as an edited body, no in-tree caller produces an unscoped session grant, and the "Exactly `{ trustGated: true }`" pin covers both keys. R2-2, R2-8: the shared fixture derives `SKILL_DESCRIPTION` from its prefix, names each generated on-disk path once, is imported by the parity suite instead of re-spelling `gated-skill`, and emits the description as a quoted YAML scalar — a bare one with a colon-space flattens the `hooks:` block into `{"PreToolUse":"","- matcher":"Shell"}`, which `parseHooksConfig` discards, leaving the gate silently absent. * fix(skills): hold the AUTO and trust invariants at the read side on resume Round-3 review findings on #11280, fixed at the shared cause rather than per instance. `sessionRules.allow` keeps a previous session's grants as inert entries and nothing ever removes one, so every path that reads the array had to re-derive "is this rule mine?" — and each derived it differently: - The AUTO strip takes candidates from the session-scoped half, so a grant tagged with another session was neither stripped nor stashed, and the strip's latch never re-scans. Re-entering that session id under a latched AUTO made the rule live again with the classifier bypassed. Enforce the AUTO half in `activeSessionAllowRules()`, the predicate `evaluate()` decides on, where session provenance cannot defeat it. - `restoreDangerousRules()` re-attached the stash by concatenation, seating two entries with the same `raw` once a cycle spanned a session swap. Merge by `raw`, keeping the stashed object so the next strip can still remove it by identity. - The live dedup re-pointed a kept entry across a session boundary while keeping the dead session's `trustGated`, so a previous session's ungated grant de-gated a later project skill's repository-controlled one and the folder-trust gate failed open. Take the arrival's gating on a cross-scope re-point, widen only within one scope, and route the AUTO stash branch through the same rule instead of re-pointing the session alone. On the resume path: - A skill body the scheduler truncated before recording starts with the truncation sentinel, not the body prefix, so it was classified as "not this skill's body" and declined at `debug` under a sentence asserting nothing was ever armed — false for a record whose `execute()` had already applied the side effects. Classify it as the truncated body it is. The decline stands: the record can no longer be compared against SKILL.md. - Declines were keyed last-write-wins, so a later refusal pair overwrote a body-shaped decline and discarded the one actionable signal. Rank them. - The body-mismatch arm hardcoded `reinvoke`, which `validateToolParams` refuses whenever a block condition co-occurs — and it does by construction for a `paths:` skill. Consult the block for the remedy. - Remedies named `/<skill-name>` unconditionally, but `user-invocable: false` drops the command from every dispatch accessor. Branch on it. Also corrects the in-code rationale for the warn/debug split: `warn` and `debug` share one sink gated only on `QWEN_DEBUG_LOG_FILE`, so the level buys grep and triage inside the debug log, not operator visibility. Every behavioural fix is pinned by a test verified to red when the fix is removed. * fix(skills): read unmatched resume records as uncorroborated bodies Addresses the round-4 review of the resume re-arming path. - An unmatched recorded response is now reported as a declined body (warn, with a route) unless it is one of the strings execute() returns in place of a body. The persisted-output stub, the budget-exhausted stub and the spill-failure fallback no longer land in the arm that claims nothing was armed; none of them is re-armed. - The restore lookup keys on the exact skill name, as the live path does, so two skills whose names differ only by case no longer collapse into one entry. - A declined project skill in an untrusted folder names folder trust as the precondition, and a disabled user-invocable: false skill is no longer sent to /skills. - Pin the stash path's cross-session trust narrowing and the merge's object replacement in permission-manager tests, and correct the merge comment's stated reason. * refactor(skills): simplify resume re-arming - Drop session-scoped allow rules. Config.startNewSession now clears session allow rules (including the AUTO stash) when the session changes, and a resumed session re-arms its own skills during initialize(). This removes the sessionId tag and its AUTO-mode merge and trust re-pointing logic. - Replace the decline classification, remedy text and warn/debug ranking with one factual warn line for a skipped skill that declares hooks or allowedTools. - Trim comments and the docs bullet. * fix(skills): do not re-arm Skills recorded inside exec output An exec script can print the nested-results line itself, so it now restores the Skill body only. The docs now state which skips log and that the match covers the body only. --------- Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
06d65463ad |
fix(web-shell): move source download to the header (#11641)
* fix(web-shell): move source download to the header * test(web-shell): require a single source download action |
||
|
|
c26a8fd004 |
feat(core): make the web_search budget configurable and bound the extractor fallback (#11692)
* feat(core): make the web_search budget configurable and bound the extractor fallback The side request ran under a fixed 60s budget set when the tool was opt-in and rarely opened pages. With web_extractor on and the tool on by default, live searches take 13-107s, so the budget is hit routinely and the partial path hands the model a raw page dump instead of an answer. Add tools.webSearch.timeoutMs (WEB_SEARCH_TIMEOUT_MS), default 120s and at most 600s, and drive the client timeout, the abort signal and the timeout message from it. Salvaged extractor text now carries a label saying it is raw page content rather than the agent's narration, and is cut to 6000 characters. Also pin two properties the citation policy relies on: evidence bullets are bare URLs and nothing else, and the tool description and the per-result footer derive from one rule list so they cannot drift. The docs and settings schema named qwen3.6-plus as the automatic search model after it changed to qwen3.8-flash. * docs(cli): state per-tool execution cap precedence over the web search budget (#11692) Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): reject out-of-range tools.webSearch.timeoutMs at write time (#11692) Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): qualify the web search timeout partial-result promise (#11692) Co-authored-by: Qwen-Coder <[email protected]> * test(cli): add web search timeout constants to exhaustive core mocks (#11692) Co-authored-by: Qwen-Coder <[email protected]> * test(core): cover cancellation winning over partial-result salvage (#11692) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Qwen Autofix <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: qwen-code-ci-bot <[email protected]> |
||
|
|
42b7091d1a |
fix(cli): reap running monitors when the process dies on an uncaught exception (#11742)
* fix(cli): reap running monitors when the process dies on an uncaught exception Monitors spawn detached so the tool can group-kill them, which also means they outlive the CLI when it dies: the crashed session can never consume their terminal events, and the in-memory registry gives a resumed session no reattach path (#11732). Abort all running monitors from the uncaught-exception handler so the existing abort path group-kills them before the process exits. * fix(cli): record monitor reap failures in the crash debug log Review feedback: a throwing abortAll would be swallowed by the catch and the debug lines are written before the reap runs, so the one distinguishing fact (the reap failed and monitors still leaked) was dropped. Hoist logPath and append a MONITOR_REAP_FAILED line best-effort. * fix(cli): record the reap outcome synchronously and scope the reap comment - Snapshot the running count before abortAll and append a synchronous [MONITOR_REAP] reaped=N line on success, so a crash log can tell "reap skipped it" from "signalled but ignored SIGTERM" (R1-2). - Narrow the reap comment: the handler covers the Config it was installed with; per-session ACP registries need process-wide tracking, which stays follow-up work (R1-1). Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
ef0b8a6baf |
fix(ci): raise the build heap cap from 3072 to 4096 MB (#11781)
main alone peaks at ~3.14 GB RSS building packages/cli under the 3072 MB --max-old-space-size cap, so whether tsc --build survives is decided by runner load: under CPU contention GC throughput drops and the build dies with 'Ineffective mark-compacts near heap limit'. Three OOM failures on three distinct runners versus green builds of the identical commit on quieter ones were observed on a single PR head. 4096 gives ~30% headroom over the measured footprint; worst-case RSS (~4.3 GB) stays safe on GitHub-hosted 16 GB runners and the ECS fleet. The test lanes keep 3072 — per-package vitest heaps are far smaller and have no OOM evidence. Fixes #11780 |
||
|
|
8520dfc1d9 |
docs: translate permission card and browser notification designs (#11775)
Co-authored-by: probe <probe@local> |
||
|
|
0fedceba20 |
fix(review): document the deadline's pause ceilings and improve capture-local errors (#11779)
Three follow-ups to #11686, from its sandboxed verification report and the audits of this change. The plan-recorded wall is elapsed time and the next reverse-audit round is priced at the costliest admission-to-admission span, so a pause is charged twice: it spends the wall and, while it is the longest gap, prices the next round. A new Review Deadline section in the user docs gives the ceilings: under the default reserve a review can pause after its first audit pair (rounds 1 and 2 launch together) for at most about 3h20m / 5h20m / 7h20m on the 8h / 12h / 16h walls, (wall - 80m) / 2 on any wall of 4h or more, less by half of any time before the pair launched. It also says a stopped review composes and reports what it proved with the verdict capped, that running /review again follows the incremental-review rules rather than continuing the audit, and that setting the wall through the environment applies to every review in the session. DESIGN.md states the same rule, including the gate's price floors. The soft-deadline paragraph under Headless runs and the --deadline help point to the section, and the help now names what the environment epoch wins over. Tests drive the real gate to pin the ceilings, the later-launch discount and the costliest-span case. capture-local keeps #11686's one-line error contract (a usage error exits 2, anything else 1) and now prints the error's stack after that line under --debug, so an internal fault can be located without turning operator errors back into crash banners. Only the flag does this; debug variables in the environment do not. The handler also rules --out before the tree is captured, as fetch-diff and issue-context do: a directory --out used to run the whole capture and then fail with EISDIR, and a blank one wrote the plan to a whitespace-named file. The shared assertWritableOutPath now names the mistake for a non-string --out: "--out must be given once, as a file path" for a repeated flag (an array), "--out must name a file path" for --no-out, instead of `out.trim is not a function`. fetch-diff and issue-context pass --out through instead of String()-ing it, which wrote a repeated flag to one comma path and --no-out to a file named `false`; both now exit 2 before fetching. |
||
|
|
faa395885e |
feat(web-shell): Improve context overview and add manual compression (#11700)
* feat(web-shell): Improve context overview and add manual compression * fix(web-shell): Keep context compression state consistent * fix(web-shell): Clarify context compression state hints * fix(web-shell): Preserve cold-session context usage * fix(web-shell): Guard context refresh and compression lifecycle |
||
|
|
6a0806faf3 |
fix(core): let the producer's own budget decide shell output size (#11727)
* fix(core): let the producer's own budget decide shell output size Shell truncates at its declared budget (30k by default) and keeps a head-and-tail preview so the trailing exit and error summary survives. The scheduler then re-bounded that body against the global threshold plus headroom (~28k by default), which sits BELOW the tool's budget — so output in that window was reduced to a short head instead, losing the trailing status that larger output keeps. Producers now report `outputBudgetApplied` when they have sized a body against their own budget, including when nothing was cut, and the generic gate stands down for it. Spawn failures, background launches, and separately built error messages are unmarked and keep the gate. Per-tool, combined, and aggregate batch budgets are unchanged. Also caps the attribution warning's exception text, which is appended outside the truncation envelope and was the one unbounded input the gate had been covering on the failure paths. * fix(core): bound the marked timeout detail and reserve appended Shell metadata Review follow-up on the output-budget marker: the scheduler's timeout branch stood the generic gate down for a marked body with no bound behind it, and Shell set the marker before appending the long-run advisory and attribution warning, so the assembled string could cross the declared budget after being marked. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): clamp the reserved Shell body budget so the marked pass cannot stand down * chore: trigger GitHub pull-ref resync (empty) * fix(core): share the truncation persistence tri-state and unify Shell metadata appends Co-authored-by: Qwen-Coder <[email protected]> * fix(core): cap the Shell metadata reservation at half the explicit threshold At a sub-advisory explicit truncateToolOutputThreshold the reserved long-run advisory consumed the whole body budget, leaving a 1-char head-and-tail preview that dropped the trailing exit-code line. Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Qwen-Coder <[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]> |
||
|
|
01aa4f267f |
feat(web-shell): make Session Workflow dependencies navigable and quiet its chrome (#10938)
* fix(web-shell): keep the auto-sized dialog's gutter on a phone The `auto` size floors the panel at `min-w-[min(100%,560px)]` so a small plan graph does not collapse to a narrow column. tailwind-merge keeps that class alongside the base ceiling `max-w-[calc(100%-2rem)]` — different modifier groups — and below the `sm:` breakpoint min-width wins over max-width. On a 390px viewport the Plan & tasks dialog therefore rendered its rounded, ringed panel flush to both screen edges, while every fixed size (sm/md/lg/xl) kept the 1rem gutter at that same width. Floor the panel at the gutter the ceiling already reserves. * fix(web-shell): bind the DAG lane reservation to the constant that places lanes Lane spacing was stated twice in two languages: `EDGE_LANE_HEIGHT` in TS, which places each layer-spanning return lane at `lane * EDGE_LANE_HEIGHT`, and a literal `9px` in `.dagCanvas`'s bottom padding, which reserves room for them. The two are coupled only through the `--plan-edge-lanes` inline style, so raising the constant alone leaves the reservation short; with enough spanning edges the clamp pins the outer lanes together and layer-spanning edges overlap again — the exact defect the lanes exist to fix. Nothing would have caught it: the lane test mocks `offsetHeight` directly. TS now publishes the pitch as `--plan-edge-lane-height` beside the lane count, and the stylesheet reads it. Also give the graph a phone: three 240px lanes plus two 64px gutters are ~850px, so the canvas scrolled in both axes at once and the horizontal scroll hid the layer the vertical scroll was looking for. Below 720px the lane and gutter narrow. Layer semantics and the rect-measured edge routing are untouched, so this changes what fits, not what is drawn. The new stylesheet guard pins both, in the shape the other `.css.test.ts` files use, since jsdom does not compute the cascade. One test-only addition on the same component: the spanning-edge test fixed the edge's arrival column (`H 876`) but not its arrival row, and nothing else in the file asserted a spanning edge's `endY`. Substituting the return lane's `routeY` for `endY` in the path tail draws the edge ~170px below the target node while every existing assertion still passes. Pinned corner-agnostically. Refs #10866. * test(sdk): pin the skills entry in the transcript compactor keep-set `compactTaskExecutionOutput` keeps an allowlist of `task_execution` fields when it drops the bulky ones, and every neighbouring entry — `executionSummary`, `tokenCount` — is already pinned by the reducer tests. `skills` was not: removing it from the keep-set left the whole suite green, and a session restored from a persisted transcript would silently lose the skill list the live run recorded. Mutation-checked: deleting `'skills'` from the keep-set fails exactly this assertion (1 failed | 327 passed); restored, 328/328. * feat(web-shell): make workflow dependencies navigable, and let the surface state each thing once Two related passes over the Session Workflow inspector and cockpit. ## Dependencies become navigation A step's dependencies rendered as `todo.blockedBy!.join(', ')` — a run of raw Todo ids, in both the graph node and the inspector detail. An id is an address, not a label, so following an edge meant reading `step-3, step-7` and then finding those rows by eye, in a panel whose entire subject is which step waits on which. Each reference is now the step it points at — the step number the list and the graph both already show, plus its title — as a control that selects that step. The dependency list becomes the graph's navigation. Recent activity was sliced to 6 while the count beside it reported the true total, so the panel contradicted itself and the remainder was unreachable. The cap stays as a preview and gains a control that opens the rest. ## The surface states each thing once The panel had accumulated chrome that competed with its content: - Section labels were tracked-out uppercase at 10-11px, so every fixed label shouted over the step title and status it introduced. They are labels, not headlines: sentence case at a muted weight. - The cockpit spent the accent colour on a fixed "Dependency graph" eyebrow above the session title — making the one blue thing on the page the one thing that never changes, while the graph below uses that same accent to mean "this node is selected". The label moves to the subtitle line and the accent is left to mean selection. - Completed-over-total sat in a tinted pill directly above the progress bar that draws the same ratio, beside the status word that names the same state — three surfaces for one fact. The count keeps the tone as ink and drops the pill. - Runtime, tool calls, tokens and artifact kind/status were glued into single strings with middle dots, which wrapped as a unit and read out as one run of text. They are separate facts, so they are separate elements. The identity line keeps the mono face only on the short session id, which is an address the user copies or matches against a daemon log. No token or theme values change; this is the existing system applied more sparingly. Both behaviour changes are mutation-checked: reverting the dependency list to the joined id string, and restoring the silent cap, each fail exactly their new test. Refs #10866. * docs(core): say why the Todo dependency semantics are not behind the flag `todo_write`'s omitted-`blockedBy` preservation, and the `blockedBy` schema text that invites the omission, both reach sessions with Session Workflow disabled. That was a deliberate choice when the behaviour landed, but nothing in the file said so, so it reads as a missed gate — the `hasActivePlan` derivation sat bare and the only nearby comment explains the dangling-edge filter, not the gating. Record the decision and its reasoning: these are plan data-model semantics rather than presentation, and gating them on a visualization switch would give the tool two dialects, storing different plans for the same call depending on whether anyone is looking at the graph. Record the blast radius in the same place, because "reaches every session" overstates it. The merge changes an update only when the previous plan still has an unfinished item, the same id carried a non-empty `blockedBy`, and this call omits it. Plans that never used dependencies are untouched (`undefined?.filter()` is `undefined`), callers that always send the full list are unaffected, and `[]` stays the explicit clear. What does reach every session is the schema text, which is why it is named as widening this branch's exposure rather than as an independent change. Comment-only; no behaviour, no signatures. * feat(web-shell): lead the plan DAG with the step, not its status Read against the three questions a plan graph is opened with — what is running, what is blocked on what, what needs me — the node answered none of them first. **Reading order was inverted.** The node led with a status glyph, then the raw Todo id, then the status word; `nodeContent`, the only element that says what the step *is*, came third. At graph scale a reader scanned `● step-3 Blocked` before "Compare findings". An id is an address and the least useful thing at that scale, so it leaves the node face entirely — it stays in the detail panel, where it is actually copied. Content leads, at 13px/500, behind the step number the inspector list and dependency chips already use, so the three surfaces address a step identically instead of three different ways. **Status was stated three or four times per node** — glyph, status word, border tint, plus an attention badge. It is now stated once, as a 3px left rule, and only when it is worth saying: waiting is the resting state of most nodes in a real plan and carries no rule. The stylesheet already made this argument for `blocked`; extending it consistently is what stops the canvas reading as a wall of tinted boxes and gives the one running node something to stand out against. Attention folds into the same rule and outranks the status tone, because it is the state the reader is being asked to act on. Status is colour, which assistive tech cannot read, so it stays in the accessibility tree as words. **Time was invisible** in an execution trace. The node face now carries the agent count and the summed agent runtime for that step — the "is this alive or hung" signal, which is the first thing asked of a live run. Two facts with real spacing, not one middle-dot-joined string that wraps as a unit and reads out as a single run of text. **Dependencies were stated twice**, as a drawn edge and again as `Depends on step-3, step-7`. The row is now rendered only when no edge states it — above `MAX_RENDERED_PLAN_EDGES` the graph draws none, and there the row is the dependency's only statement, so deleting it outright would have lost information in exactly the case that needs it most. It renders as the same number-and-title chips the inspector uses. The input port dot goes with them: the left edge now carries the status rule, and an incoming edge already terminates in an arrowhead at the node — that arrowhead is the input marker. The output port stays, since outgoing edges leave their source unmarked. Both port dots also drop `--agent-blue-500`; every node with an edge had one, so a permanently blue dot across most of the graph left selection no colour of its own. Edge routing, the layering algorithm, the return lanes and the measure pass are untouched. This is the node face and the colour rule. Three geometry tests identified a node by its first `<span>` — the Todo id. That is now the accessible status word, so they bind to `data-plan-node-id` instead, which is the stable hook and was always the better one. Every behavioural change is mutation-checked: ungating the dependency row, deleting the accessible status word, and dropping the elapsed each fail exactly their own assertion. Design plan, including the two revisions the review pass forced, is in docs/plans/2026-09-04-session-workflow-dag-design.md. Refs #10866. * fix(web-shell): drop the unused dependencyIdsByTodo destructure ESLint flags the graph destructure binding as never used; the memo still computes and returns the map for other consumers. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmtlr9x6ugb * fix(web-shell): stop the activity grid from swallowing the show-all control Round-1 review findings. **Blocker.** `.showAllActivity` was a single class (0,1,0), but the button is a direct child of `.activityList`, whose base rule `.activityList > button` (0,1,1) declares the four-column activity grid and wins. The label — a bare text run that Grid wraps into an anonymous item — auto-placed into the 52px first column and wrapped to three or four lines with three empty columns beside it, and the intended padding lost too. Hover only looked right by coincidence, because the row's own hover set the same background. This is the collision handled two rules earlier for `.metrics`, missed here. Both are now pinned by the stylesheet guard so neither can regress to a bare class, and the fix resets `display` and `grid-template-columns` explicitly rather than relying on source order. **The graph's step-details panel still joined raw ids.** The node face's chips cannot be controls — that row lives inside the node's own `<button>` and a nested button is invalid HTML — but this panel sits outside it, so its references become real controls that select the step they name, which is what makes a dependency list navigation rather than text. `Depends on` and `Unblocks` both. **The narrow-viewport claim was wider than the fix.** Three 168px lanes plus two 32px gutters is ~588px, which removes horizontal scrolling between there and the 720px breakpoint but still scrolls on a 390px phone. Added a second step below 480px that gets a three-layer plan under a phone viewport, and said plainly in the comment that deeper plans still scroll — reusing the flat layout stays with #10866. The guard test is renamed to what it pins: jsdom computes no layout, so it can assert the two steps exist and narrow, not an outcome. **The lane pitch had a second copy.** The `var()` fallback restated `9px` next to the `.dagCanvas` default, so "TS owns the pitch" held only when the graph has dependencies. The default is now `0px` — the zero-lane case collapses the reservation regardless — and the guard rejects the fallback copy too. **Comment antecedent.** "the prompt change" in todoWrite named nothing a later reader could find; it now quotes the schema description it means and says why a tool schema reaches every session. Refs #10866. * fix(web-shell): keep the node's non-colour status channel, and stop clipping its port Two defects from my own review of the DAG pass, both in the class the round-1 review already caught once: invisible to jsdom, invisible to a passing unit run, and only findable by reading the stylesheet. **The status glyph was an accessibility channel, not decoration.** Making the left rule the single carrier of status dropped the glyph — but the constant it came from says exactly why it exists: "Status is carried by a glyph as well as a colour so the graph survives colour-blindness, high-contrast mode, and a greyscale screenshot." Colour plus a screen-reader-only word leaves a sighted colour-blind reader, high-contrast mode, and any greyscale capture unable to tell running from completed from needs-attention. "One status, one place" was the wrong rule; the right one is one carrier of the status *colour*, plus a redundant non-colour channel. The glyph returns for every status, in the meta line so the step's content still leads, and muted so the rule stays the only thing carrying the tone. Pinned per status, and pinned as distinct from each other, so the shape channel cannot silently collapse. **The status rule clipped the outgoing port.** Drawing it as `.node::before` needed `overflow: hidden` for the node's 8px radius to apply to it — which also clips the port dot at `right: -4px`, which is deliberately outside the box. It is a `border-left` instead: border-radius clips it natively, no overflow, and a transparent rule paints the node's own background so a resting node reads as 3px of extra left padding with no width difference between states. The stylesheet guard now rejects the overflow, the pseudo-element and a missing port offset together, since the three only fail as a set. Also drops `.nodeGlyph`'s dead sibling state: the rule is back in use, so nothing here is unused CSS. The design plan records both as corrections rather than folding them in, so a later reader sees which of its rules were wrong and why. Refs #10866. * fix(web-shell): put dependency truncation where it can actually fire Round-2 review findings that survive at this head; the round's three blockers (the node's clipped port, the show-all specificity collision, and the step-details panel's raw ids) were already fixed in the two commits after the head it reviewed. **The chips' `text-overflow: ellipsis` was dead.** It was declared on the `inline-flex` chip itself, but `text-overflow` applies to a block container's own inline content — the title sat in an anonymous flex item, so a long dependency title was clipped hard with no ellipsis. The title now has its own span carrying `overflow`, `min-width: 0` and the ellipsis, in all three places that render a reference (the node face's read-only chips and both directions of the step-details panel's links), and the stylesheet guard pins that it lives on the title rather than the box. **The comment above `.node` still described the old status behaviour** — it said status drives the accent that drives the border, which this branch stopped doing two commits ago. Rewritten to describe the border/rule split, keeping the hazard it originally recorded: with status off the border, a selected completed or blocked node can no longer pick up a second border in a clashing hue. **The compaction test's comment overclaimed.** It said every other keep-set entry is pinned by that test; it pins `skills`. Narrowed to what it does. Two coverage gaps the same round named: - The inspector test exercised only the upstream direction. Upstream and downstream render through one helper but read from different sources (`blockedBy` versus the projection's `dependentsByTodo`), so covering one did not cover the other; the downstream link is now rendered, asserted and clicked. - `planExecution.agentCount`'s singular and plural branches are separate formatter paths, and only the singular one was exercised — `1 agents` would have shipped green. Both are now pinned. Refs #10866. * fix(web-shell): drop ghost dependency controls and mark them interactive The step-details dependency buttons mapped raw selectedTodo.blockedBy, so an unresolvable step id rendered as a clickable "? <id>" control; clicking it selected a ghost id, emptied selectedTodo, and hid the details panel mid-navigation. Render from the same filtered projection the edges draw from (dependencyIdsByTodo: unknown ids, duplicates, and self-references dropped) and hide the section when nothing resolvable remains. Also add the data-plan-interactive marker to both step-details dependency button groups. The host keyboard handlers in ToolApproval and TasksStatusMessage isolate plan controls through that marker, and these were the only interactive controls in the view without it. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtmourmvi4 * fix(web-shell): let dependency titles keep their colour, and attention its ring The muted `> span` rules match every direct-child span of the chip and link — now including the title span the truncation fix added — so every dependency title rendered muted while the chip's own `color: var(--foreground)` applied to no rendered text. The title now states its own colour on the title rule, where two classes outspecific the one-class-plus-element mute. The attention rule sat before the blocked/ready rule at equal specificity, so a blocked node that also needs attention wore the muted ring instead of the attention tone. Attention is declared after the status tones so it outranks them, as its comment says. Both shapes are pinned at the source in PlanExecutionView.css.test.ts. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtn1pqbuir * fix(web-shell): dedup repeated blockedBy ids before rendering references The node-face chip row and the inspector's upstream list mapped raw model-authored blockedBy, so a repeated id emitted duplicate React keys and a duplicate chip or link. Every other blockedBy consumer already dedups with new Set, and the step-details panel reads the deduped topology projection; both surfaces now match. The dense-plan test feeds a repeated id and pins the single chip, and a new inspector test pins the single upstream link. Also pin the styles.dependencyTitle wiring on the chip and link title spans in the existing render tests: the truncation rule targets that class, so dropping the className would re-clip titles without an ellipsis while every text assertion stayed green. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtn1pqbuir * fix(web-shell): state the 480px step's true plan width in the comment The comment claimed the 480px step gets a three-layer plan under a phone viewport, but its own pinned values sum to ~396px (3×116px lanes, 2×18px gaps, 2×6px canvas padding) — still ~6px wider than the 390px phone the sibling comment names as the reference. Restate the comment with the true figure: fits phones wider than ~396px, a 390px phone still scrolls the last ~6px. Comment only; the pinned values and the stylesheet guard are unchanged. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtnaadh9j8 * docs(plans): mark the session-workflow DAG design implemented, resolve its open questions The doc shipped in this PR declared "Status: proposal, not implemented" while the same PR ships the implementation, and left two open questions the code had already answered. Mark it implemented and record the decisions where they were made: the elapsed count renders on the node face (meta line, when greater than zero), and attention folds into the left rule via data-attention, declared after every status tone so it wins the cascade. The verification note stays open — it is about process, not a design decision. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtnaadh9j8 * test(web-shell): pin the declarations the plan guards actually rest on The truncation guard now pins `overflow: hidden` beside `text-overflow: ellipsis` — the former is what makes the latter take effect, and `overflow` is not inherited from the chip. The two phone-width steps pin the lane width on its owner selector and add the paired gap/padding narrowing the fit rests on; dropping any of them restored the clipped-title or two-axis-scroll defect while the suite stayed green. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtngpuscjl * fix(web-shell): drop self-blocks in the inspector, keep status words beside attention The inspector's upstream dependency filter kept self-references: a todo listing its own id in blockedBy rendered a "Blocked by" chip pointing at itself, while the projection's dependentsByTodo builder and the DAG both drop self-blocks. Apply the same rule so the inspector agrees with every sibling surface. The node face's sr-only span also announced attention instead of the status word; an attention node lost its status from the accessibility tree while the aria-hidden glyph still carried it for sighted users. Announce both: the status word first, attention appended. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtnn5c4cjw * test(web-shell): witness the downstream click, attention word and cascade order - click the downstream 'Unblocks' reference in the step-details panel and assert the named node gains aria-pressed, so a dropped onClick in the selectedDependents block turns red - render a node into attention and assert its sr-only span carries the status word beside "Needs attention"; pin the inspector's self-block exclusion beside the dedup test - extend the cascade-order guard to all six status selectors, not just blocked, so regrouping any status tone below the attention rule fails Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtnn5c4cjw * test(web-shell): pin the plural agent count and the attention attribute - link two root Agent executions to one node and assert the face renders '2 agents', so an EN template that always emits `${count} agent` turns red; the count-1 side stays pinned by the existing assertion pair - assert the attention fixture's article carries data-attention='true', the hook the `.node[data-attention='true']` colour rule selects on; the 'Needs attention' word was pinned at |
||
|
|
6a211b4652 | chore(release): bump node repl to 0.1.4 (#11771) cua-driver-rs-v0.20.6 | ||
|
|
eb780062ca |
feat(review): record a default deadline in the plan so every run has a wall (#11686)
* feat(review): record a default deadline in the plan so every run has a wall The reverse-audit loop's deadline gate only ever read QWEN_REVIEW_DEADLINE_EPOCH, which CI exports and nothing else does, so a local run whose loop had stopped converging had no bound but the round cap — and the cap counts rounds, not time. The capture commands (fetch-pr, capture-local, plan-diff) now record a wall in the plan: `--deadline <minutes>`, or by default 8h on a 3A diff, 12h on a 3B one and 16h when huge; `--deadline none` records nothing. It is stored as a duration from the attempt's start (`deadlineSeconds`) with its source (`deadlineSource`), because `--resume` never rewrites the plan; the readers add it to the current attempt's start — the run-session ledger's entry for this session, else the plan's mtime — so a resume from a new session renews it. Precedence in the gates is env > plan wall > none. A plan-sourced wall keeps a third of itself as the tail reserve, capped at 4800s and floored at the compose floor. The default is a liveness bound, not a cost calibration, and it does not flip the huge tier's round reduction (5 -> 3): only an explicit clock — the env epoch or the flag — does, so a local huge review keeps its five rounds. The defaults are sized above a healthy run at the cap from the repository's own CI review runs: un-gated 3A runs post at 106-293 min, posted 3B runs past ~4.5h are all gate-stopped between rounds 3 and 5 (two more killed at 6h with nothing posted), and huge runs reach the 6h wall — roughly twice each tier's CI budget. A malformed, blank or non-string --deadline is a usage error refused before any fetch or capture work; a --deadline on a resumed run is reported as ignored. Malformed env values fall through to the plan wall; malformed plan fields fail open. Docs (SKILL.md, DESIGN.md, code-review.md, settings.md) describe the precedence, the defaults and the measurements. * fix(review): refuse a --deadline that cannot admit round 1, and settle the triage notes - `parseDeadlineOption` now takes the environment and refuses, up front, a wall shorter than the round-1 estimate plus the reserve it would keep (`RESERVE_ENV` when set, else `planReserveSeconds`) — `--deadline 10` used to parse cleanly and then refuse everything it was asked to build. - `admitReverseAuditRound` carries the parsed report to the gate instead of re-reading the plan from disk on every admission; both callers held it. - The reserve floor wording says what holds at the boundary: a floored plan wall's reserve EQUALS the compose floor, and the round gate still fires first because it prices the round on top — pinned by a test. - DESIGN.md's operator-ceiling passage no longer says a larger ceiling "has no expression": `--deadline` states one, and the tier still reads only whether an explicit clock exists (#9245). The new section names the wall's audience (unattended runs). * fix(review): price the --deadline floor at a convergence, and settle the review's twenty threads Two Criticals and eighteen suggestions from the deep review of #11686: - The up-front `--deadline` floor admitted exactly the wall its own message named as sufficient (50 min captured cleanly, then round 1 was refused one second later) and priced one round where the loop's convergence pair can cost two. It now prices two rounds at the estimate plus the reserve the wall implies, refuses at or below that (at or under 90 minutes with the default reserve), and is env-free — the same ruling in every shell — so the writer tests no longer depend on `QWEN_REVIEW_DEADLINE_RESERVE_SECONDS` being unset, and a CI-clocked capture is not aborted over a bound the env epoch would have overridden. - `planReserveSeconds` floors at the EFFECTIVE compose floor, so an operator raising `QWEN_REVIEW_DEADLINE_COMPOSE_FLOOR_SECONDS` cannot make the round gate admit a round whose verification the verify gate would refuse. - The `/review` parser consumes `--deadline` with its value and warns, so `/review --deadline 90` no longer retargets the review to PR #90. - The resume note reads the plan and says whether a wall is in force. - check-coverage exempts a round-cap not-built gap when the plan's wall has closed: the round-1 rebuild it would prescribe cannot run. - `ResolvedDeadline` drops its two unread fields; `hasReviewDeadline` is the one definition of "explicit". - Docs and glosses: the non-converging-run wording in three places, the huge-tier predicate at seven stale sites, the DESIGN.md citation that no longer existed, the multiplier and admission-horizon numbers, the tier-priced-at-capture and `--deadline none`-under-env caveats, and the three qualifications the user doc's wall paragraph lacked (idle time is charged, the floor is arithmetic only, the workflow allowance is env-only). - Tests: the lease and gh legs of the early-refusal order, the default wall written under an env clock, a real short flag wall for the reserve-scaling case, the effective-floor ordering, and the closed-wall exemption. * fix(review): settle the second review round on the deadline floor and parser arm - The too-short refusal names two true numbers: what the rejected wall would need (its own reserve, which grows with it) and the shortest wall the rule admits, found as a fixed point (`shortestDeadlineMinutes`, 91 under the default rule) instead of a number derived from the input. - `captureDeadline` prices the flag a second time with the shell's reserve and compose-floor overrides — the gate's own expression — and refuses a wall the gate would refuse at zero elapsed; skipped under an env epoch, which makes the flag inert. `parseDeadlineOption` stays env-free. - The `/review` parser consumes the token after `--deadline` only when it can be a deadline (minutes or `none`), so a PR URL stays the target, and the warning names the consumed token instead of predicting the wall. - coverage's closed-wall exemption gains its open-wall oracle; the gate's comment states the open-span worst case an idle gap is charged as. - Docs: the round-cap reader's band in both directions (honoured inside [3, tier], lowered under a clock, replaced outside), `qwen review run`'s kill as the second env-only reader, the multiplier's provenance (measured for 3A, projected for 3B and huge), the flag's second bar under overrides, and the test comments' boundaries. * fix(review): settle four reverse-audit rounds on the deadline flag and parser arm Findings from four rounds of independent reverse audit, each with executed probes, on the earlier review fixes: - `validateDeadlineFlag(env, raw)` is the one capture-time check — grammar, the env-free convergence floor, then the same floor priced with the shell's reserve and compose-floor overrides (the gate's own expression), skipped under an env epoch. The three capture commands run it up front, before any fetch, lease or planning work; the second bar used to reach `captureDeadline` only, after that work was spent. - The refusal message names which bar refused, formats the reserve so the minutes add up (floored to a tenth), and cites the shortest wall the shell accepts under BOTH bars (`shortestDeadlineMinutes`, now a fixed point over the combined rule, `null` when an override puts every wall under a day out of reach). A lowered reserve no longer sends the caller to a wall the default rule then refuses; under an epoch the figure is the default rule's. - `fetch-pr --resume` is held only to the grammar and the default rule up front (the flag is ignored on a resumed plan); a resume that falls through to a fresh review meets the shell bar before the stale worktree is destroyed and before the fallthrough is announced. - The `/review` parser routes a non-deadline value after `--deadline` through the same disposal pool as an invalid `--effort` value, with the `=` form mirroring the siblings (PR-shaped → pool, else reported), and its warnings say what happened to the value. - check-coverage prices the round-cap exemption's rebuild the way the gate prices the plain `--round 1` build (from the round stamps, one auditor), not at a flat constant. - The resume note formats minutes, distinguishes a flag wall from a default one, and mentions an exported epoch. - Tests isolate `QWEN_REVIEW_DEADLINE_EPOCH` where the shell bar is under test (this repository's own review job exports it), pin the fallthrough order, the combined-rule figure, the `=` form, and put the stamped compose fixture on the plain pricing branch with ten seconds of slack. * fix(review): settle six audit rounds on the resumed wall, the FIX waiver and the capture handlers Round pricing and the resume note: - describeResumedWall describes what the gates will do, never what to do instead: the orchestrator reads that line, and SKILL.md forbids it to drop --resume or add --deadline. Each gate is asked its own question (a zero compose floor refuses nothing; a reserve override below the floor names the verify refusal even where rounds are admitted), the start is named as the ledger entry or the plan's capture, minutes are pluralised. - The round pricers read only this attempt's stamps (attemptStamps): a round-cap resume keeps the dead attempt's stamps, whose open span crossed the death gap and refused a rebuild on a wall the resume had just renewed. The one-stamp-per-round guard is per attempt for the same reason. - cheapestRebuildAdmissionSeconds prices every round the stamps could make a `--round <k>` rebuild (each stamped round and the no-exclusion case) and takes the least; the compose-time waiver and the note both use it, so a FIX the gate would admit for some k is never waived. The FIX waiver, explained: - check-coverage reports the FIXes it withholds (`waived`), compose-review prints them as NOTE: lines beside the FIX lines and carries them on the composed verdict (`waivedFixes`; save-artifact defaults the field, run passes it through). The `[unverified]` relaunch FIX answers to the same verify floor. Notes are in minutes, like every other line on that channel. The capture commands: - capture-local's handler keeps plan-diff's contract: a usage error is one stderr line and exit 2, anything else exit 1 — never a crash banner for a malformed --deadline. The help text mentions --resume only on fetch-pr and says the grammar and the default rule still apply on a resume. A usage error echoes an over-long or non-string value bounded, in code points. - fetch-pr's ledger note rides only where a plan wall would date from it. Docs: DESIGN.md names the last admission each default allows; SKILL.md marks the resume note as information and the withheld FIXes as NOTE: lines; code-review.md carries the operator's remedies (a resume from a new session first) and the withholding rule. Tests pin every arm above, isolated from the ambient CI epoch; a mutation matrix over the new arms is listed in the PR. |
||
|
|
00180c5360 |
feat: agent board — share work across independently started agents (#9402)
* feat(core): add ask and decision as board items
Stage 1 of docs/plans/2026-08-18-peer-session-collaboration.md. The board
holds task today; these are its two peers, under the same lock and
atomic-write discipline.
An ask is a question with terminal states — answered, declined, timeout — so
a sender always learns which and can wait, reroute, or escalate. That is what
a plain message cannot offer, and it is why the design has no general-purpose
message.
Timeout settles lazily. expiresAt is written at creation and any reader past
the deadline reports timeout; no sweeper walks the directory. A fetch-based
system has no daemon guaranteed to be running, so the only process that can be
relied on is the one currently asking. The deadline is re-checked under the
lock, since answering a lapsed ask would silently resurrect it.
A decision is something awaiting human authority — approval, acceptance,
adjudication unified, because each needs authority and no agent has more of it
than another. It has no expiry on purpose: silent expiry converts "nobody
looked" into "the system decided", which is the one authority nothing but a
human may hold.
Items land at ~/.qwen/boards/{board}/, the layout 2.8 settles, so they never
need the migration the existing split roots do.
board-lock.ts extracts the two-tier lock rather than adding a third copy of
it; folding tasks.ts and mailbox.ts onto it is a follow-up, kept out so this
change does not also rewrite those paths.
Refs #8724
* feat(cli): add the board command surface and fleet up
Stage 2 — the step where the requirement is actually met. With a command
line over the board, a Codex, a shell script, a scheduled job and a Qwen
session participate on identical terms, because running a command is the one
thing all of them can do.
qwen board show | watch | task | claim | done | ask | answer | decline
| raise | resolve
Verbs are flat rather than nested under a noun — board claim t-3, not board
task claim t-3 — because the id prefix already carries the noun. Two levels
instead of three matters for a surface an agent types every turn.
board watch polls rather than watching the directory: fs.watch differs across
platforms and network filesystems, and a directory this small is not the
bottleneck. The panel leads with what needs a human, then what is blocked on
a peer, then work in flight — a panel that leads with "3 agents working" is a
process monitor, this one leads with what will not move until someone acts.
ask --wait is bounded (30s default) with distinct exit codes, since a foreign
agent running it is blocking its own turn.
fleet up writes no terminal code. tmux already gives panes, per-pane cwd,
keyboard switching, zoom, detach and a server that outlives the client, and
core already wraps its CLI. The command sequences those calls and adds the
one thing tmux cannot know about: a pane showing the board. Agents start via
the command passed at split time, so no keystroke can land before a shell is
ready. --with runs any other binary in its own pane, which is how a foreign
agent joins.
board-tasks.ts is deliberately not agents/team/tasks.ts: that module is Agent
Team's in-session list with dependency edges and an in-process change emitter
its scheduler subscribes to, keyed under a different storage root. Keeping
them apart lets this ship without rewriting that scheduler.
Refs #8724
* feat(core): make a session board-aware when QWEN_BOARD is set
Without this the feature does not work at all. fleet up starts each pane with
QWEN_BOARD in its environment, but a plain session ignored it — never
claiming a task, never looking for a question addressed to it, never raising
anything for the user. Panes of agents that cannot see each other, and a board
pane that stays empty.
The section is about when to reach for the board, not a command reference. A
model given verbs without boundaries uses ask for things it could read itself,
or narrates progress instead of moving a task. So it states that completing a
task is how you report, that an ask is for what only another participant can
answer, and that anything needing authority is a decision no agent resolves —
including itself.
It also pins the boundaries the design depends on and that prose would drift
away from: no general-purpose message, a named owner is a proposal rather than
an assignment, and your own output is not visible to anyone else.
Absent unless QWEN_BOARD is set, so the common single-agent prompt pays
nothing for it.
The CLI now re-exports the env var names from core instead of redeclaring
them. Two copies of a constant that must agree is the same failure mode this
whole design exists to remove.
Refs #8724
* docs(users): document the agent board
Written for someone who wants several agents working side by side, so it opens
with the command that produces that and a picture of the result rather than
with the model.
Three things it states plainly because they are the parts that surprise
people: the panel is ordered by what needs you first and hides anything
settled; --wait always settles, so a caller never hangs; and nothing is pushed
to any participant, which is exactly why a tool we did not write can take
part.
The limits section is honest about the two that matter — participation is
cooperative and cannot be enforced on an agent we did not write, and a named
owner is a proposal rather than an assignment.
* fix: repair the blockers a direction audit found
The audit found the command surface was unreachable and the layout broken,
which between them meant nothing in this PR could actually be exercised.
Blocker: qwen board and qwen fleet were registered but not in
parseArguments' exit list, so every invocation printed its output and then
booted a full interactive Qwen session on the same stdin. In a fleet pane
that is a TUI takeover; from a tool call it is a hung turn. It also made
run()'s exit codes meaningless, so the branching the ask contract promises
did not work.
Layout: tmux -l sizes the *new* pane, and the new pane was the board — so the
board took 70% and sat on the right, the opposite of both the intent and the
comment. Splitting N ways by hand also computed -l 100% for the last pane,
which tmux rejects, so the default --agents 2 failed on a half-built session.
Now the window's own pane is respawned as the board and main-vertical does
the arithmetic. Running inside tmux also used the configured session name
rather than the one the user is actually in, which fails whenever those
differ.
Ownership: claiming a completed task silently reopened and reassigned it, and
anyone could complete work someone else was mid-way through. Completion now
requires ownership; notes and hand-offs stay open to anyone, since those are
how a stuck board gets unstuck and neither destroys work.
Identity: the participant fallback included the pid, so every invocation was
a different participant — claim and done on one task came from two identities
and an ask could never be addressed back. Stable per-user now.
Also: --ttl on ask, since a recipient that only looks between long turns
could not answer within the fixed 15 minutes; --wait reads one item instead
of listing the board every 500ms; and the asks header no longer claims the
timeout is persisted, because it never was and persisting it would mean a
read path that writes.
* feat(cli): hand the protocol to agents we did not write
The audit was right that --with was hollow: it set QWEN_BOARD in a foreign
pane, but a Codex never reads that variable and nothing we control can inject
into its prompt. The env var alone made heterogeneous participation look
supported while leaving the operator to reconstruct the protocol by hand —
the exact thing an access layer exists to avoid.
qwen board protocol prints the same instructions a Qwen session gets, filled
in with the real board and participant name, in a form meant to be pasted.
--with panes now run it before exec'ing the command, so the text is already
in the pane the agent lives in.
This does not make participation enforceable, and the docs say so. It makes
it possible without a human reading our source.
Also adds show --mine, which narrows tasks and asks to one participant while
deliberately keeping decisions: those are the human's, and a view that hides
them cannot tell the user what is blocking the board.
* fix(cli): repair fleet layout targeting and the type error the audit caught
The second audit found this branch did not typecheck: fleet.ts passed
tmuxListPanes' result — already TmuxPaneInfo[] — back into
parseTmuxListPanes. Same failure mode as the round before: a path written
against an imagined API and never exercised.
Worse was what it did when it ran. Inside tmux, the code asked tmux for "the
current window" after creating one. For a non-attached command client that
resolves from the inherited $TMUX_PANE — the window that invoked us — so
fleet up respawned its own pane and killed itself mid-build. tmuxNewWindow
now returns the new window's id via -P -F, which is the only thing that can
name the window we just made.
Also from the audit: the layout ran out of rows past three or four panes
because every split halved the same pane, so it now rebalances after each;
re-running outside tmux reused an existing window and destroyed the earlier
board, so it always makes a fresh one; tmux failures after verifyTmux escaped
as a raw stack trace through the yargs handler, now contained to one line;
and show --mine filtered only the human render, making the flag a no-op for
--json, which is the branch a foreign agent uses.
The --with pane no longer prints the protocol before exec'ing: agent TUIs
enter the alternate screen and discard it, so the banner was never readable.
The operator is told once, after the layout is up, to paste qwen board
protocol in. The docs said the environment variables were enough for a
foreign tool to join, which was never true; they now say what actually works,
and record --ttl, --mine and the running-session limitation.
* feat(cli): let an already-running session join a board with /board
The last requirement still unmet. Board awareness came only from QWEN_BOARD,
which fleet up sets in each pane's environment — and an environment is fixed
at launch. A session that was already deep in a problem when coordination
started could never participate, which is precisely the case this design
exists for.
resolveBoardPromptContext now checks a runtime context before the
environment, and /board sets it and refreshes the system instruction, so the
board section is present from the next turn. getMainSessionSystemInstruction
rebuilds the base prompt on every refresh, so no cache had to be invalidated.
/board reports where you are
/board <name> [as <who>]
/board off
The point is that the joining session keeps its context. Briefing a fresh
teammate is what spawning already does; bringing in an agent that has been
working for an hour is what this is for.
Docs record it, and the limits section now says what is actually true — a
running session can join, but nothing is pushed to it, so it sees an item the
next time it looks.
* feat(cli): add board prune, and stop overstating decision authority
prune was specified in the design and referenced by a comment in asks.ts for
a command that did not exist, so settled items accumulated forever. It is
manual rather than scheduled: deleting a record another participant may be
mid-read on is a concurrency problem worth not having, and a fetch-based
system has no daemon that could be trusted to sweep. It takes the item lock
so a concurrent settle cannot be lost between the read and the unlink.
decisions.ts claimed no agent-visible tool routes to resolveDecision. That was
false — qwen board resolve is on the same CLI the prompt teaches agents to
use, so anything with a shell could settle its own decision. Enforcing it
needs a surface the agent panes do not carry; until then the code, and now the
user docs, say "by agreement" rather than pretending it is an invariant.
Two smaller repairs from the audit: the lock map grew one mutex per touched
path for the process lifetime, which a long-lived board watch would accumulate
indefinitely, and a file removed between taking the lock and reading it
surfaced as a raw ENOENT instead of the not-found the onMissing contract
promises — reachable now that prune exists.
Also names the board pane, which showed the hostname before.
* feat: register participants so peers can be addressed by name
Until now the only participants visible were those who had already owned a
task or been party to an ask. An agent that joined a minute ago could not be
addressed, because nothing knew its name — and `board ask <name>` had no way
to learn one, so callers were left guessing.
participants/{name}.json records who is on a board. It does not duplicate the
machine-wide session registry: that one is keyed by pid and answers what is
alive, this one is keyed by declared name and answers who is here. Liveness is
read from the former, so a record never has to be heartbeated — a crashed
agent stops being listed because its pid is gone.
A name held by a live process is suffixed rather than refused: a caller made
to retry with a different name will either pick badly or give up. A name whose
holder is gone is reclaimed, or a crash loop would exhaust every reasonable
one.
Panes join before the agent starts, and /board joins on the way in, rather
than instructing the model to run it. A participant nobody registered cannot
be addressed, which is too load-bearing to leave to whether an agent follows
its prompt.
kind — interactive | daemon | spawned | foreign — is the field that separates
an agent a leader started, which exists to do what it is given, from an
independent one with its own work, for which an assignment is a proposal.
* feat(cli): poll the board so an idle session stays responsive
A session mid-turn reads the board at its turn boundary, but one sitting idle
at a prompt has no boundary — so an ask addressed to it would sit untouched
until its TTL lapsed. That gap is the strongest argument for push, and it does
not need push to close.
useBoardPending polls every five seconds: below what a person notices, and one
readdir plus a few small reads, the same order as sessions ps. The session
still chose the moment and nothing can arrive unbidden, so none of the
machinery a delivery path would need comes with it. Decisions lead the
indicator and carry the mark — an ask blocks one peer, a decision blocks
everyone until a person acts.
Deliberately not wired into Footer.tsx. That is a 320-line ternary chain whose
rendered output I cannot see from here, and blind edits to unverifiable code
produced both of this branch's earlier blockers. The hook and its formatter
are tested; wiring is one branch in leftBottomContent and wants a visual
check.
An unreadable board — removed, permissions changed — drops the indicator
rather than interrupting the session.
* feat(cli): show what the board is waiting on in the footer
Completes the idle path. The hook polled but nothing rendered it, so a session
sitting at a prompt still had no reason to look.
It goes in rightItems, next to sandbox and safe-mode, rather than as a branch
in leftBottomContent: the indicator is additive and should not displace the
shortcut hint. Warning-coloured because everything it counts is something
waiting on someone, and decisions lead — an ask blocks one peer, a decision
blocks everyone until a person acts.
The previous commit left this out for fear of editing a component whose render
could not be seen from here. That was the right caution but the wrong
conclusion: Footer.test.tsx renders with ink-testing-library, so the wiring is
asserted rather than eyeballed. Three cases — no board, quiet board, counts
present — and the existing 34 still pass.
* refactor(core): fold the three copies of the file lock onto one factory
tasks.ts, mailbox.ts and board-lock.ts each carried the same two-tier
discipline — an in-process mutex per path so local writers do not stampede the
OS lock, wrapping a proper-lockfile cross-process lock. mailbox.ts's version
was already commented as mirroring tasks.ts's.
They are not identically tuned, and flattening that would have been the wrong
kind of consolidation: mailbox retries 10 where the task board retries 30, and
logs a compromised lock at debug rather than warn. An inbox has one writer per
message; the task board has up to MAX_TEAMMATES contending for the same
first-pending file, which is what the deeper budget and the jittered backoff
are for. So createItemLock takes the budget and the handler, and each caller
keeps its own.
onMissing is now optional and rethrows when absent, which is what mailbox
expects — its callers treat a vanished inbox as a real error, while the task
board and the board items treat it as not-found.
disposeInboxLocks survives as a thin wrapper over the factory's dispose: the
mutex map moved inside, so eviction had to move with it.
Verified: 301 tests across src/agents/team pass, including tasks, mailbox and
the 53-case coordination harness.
* feat: task dependencies and the deadlock the design promised
Closes the last open item. Two things the design sold and the code did not
have.
BoardTaskRecord gains blocks/blockedBy, written on both sides by
blockBoardTask. Both halves are stored rather than derived because a reader
holds one task at a time — answering "what is this waiting on" from a one-sided
edge would mean scanning every other task. A blocker that no longer exists
counts as satisfied, so a pruned task cannot wedge the board forever.
findDeadlocks is the part 3.3 called the thing no single session can see about
itself: from inside, A is simply waiting, and so is B. On the board those are
two open asks pointing at each other, so the cycle is data rather than an
inference from two stalled sessions. It renders above the asks it is made of,
because a pair that cannot resolve itself needs a person and nothing else
surfaces it.
Only the two-party case. Longer cycles exist but are rare enough that
reporting them costs more confusion than it saves, and a pair is what someone
can act on immediately.
Verified: 305 tests across src/agents/team, 12 in the board CLI, core
typechecks clean.
* fix(core): type sharp by its module shape so the package builds
packages/core does not compile against its own declared dependency range.
image-view.ts imports SharpConstructor from sharp, and no such export exists
in ^0.34.5 — resolved here to exactly 0.34.5, whose types are export = sharp,
a function merged with a namespace and describing that shape under no name.
typeof import('sharp') is what the file actually needs: callable for
sharp(bytes, …) and carrying the namespace members the resize path reads
(sharp.kernel.lanczos3).
Found while trying to verify the board work end to end from build output — the
build failed before reaching it, and would fail the same way for anyone
installing to the manifest. Carried here rather than opened separately because
it is a prerequisite for building this PR at all. packages/cli still fails on
nine errors in acp-bridge and channels/{dingtalk,feishu}, which are untouched
by this branch.
Verified: packages/core builds clean, image-view tests pass.
* fix: stabilize agent board fleet startup
* chore(cli): use standard command output streams
* feat(cli): make decision authority structural rather than a request
The last thing in this design that was asserted but not held. decision exists
because approval, acceptance and adjudication need authority no agent has —
and that was a sentence in the prompt, on a command line agents use for
everything else. Anything with a shell could approve its own request.
qwen board resolve now refuses from a non-interactive shell, which is what an
agent's tool call is: its stdout is captured, a person's terminal is not.
A barrier, not a proof. --force bypasses it for scripting, and an agent that
allocated a pty would pass. Both are acceptable, because the threat is a model
taking the path of least resistance rather than one working to defeat a check.
What changes is that settling your own request is no longer the easiest thing
available.
The check lives in the CLI rather than resolveDecision because only the caller
can see whether a person is there. decisions.ts, the board prompt and the user
docs now all say the same thing, where before the code claimed an invariant it
did not have.
Verified: 13 board CLI tests, 25 across board-prompt and board-items.
* fix(core): restore a callable sharp type in image-view
`(typeof import('sharp'))['default']` aliases the module-namespace
shape, which is not callable, so the `sharp(bytes, ...)` call sites
fail to build. sharp is `export = sharp` (a function merged with a
namespace), so `typeof import('sharp')` is already the callable
function+namespace type.
* fix(board): guard os.userInfo() in the participant-name fallback
os.userInfo() throws SystemError when the effective uid has no passwd
entry (containers, root). Fall back to USER/LOGNAME before a generic
name so the zero-config default still resolves.
* fix(board): never prune on an unparseable settle timestamp
A truthy non-numeric timestamp made NaN, and
is false, so the age gate skipped its and unlinked the file. Resolve
the timestamp to a finite millisecond value first and skip anything that does
not parse.
* fix(board): sanitize record-controlled text before terminal rendering
renderBoard interpolated task/ask/decision/participant fields raw into
the terminal, so a participant-written ANSI/C0 sequence would be
interpreted by the terminal. Route every record-controlled field through
sanitizeTerminalText.
* fix(board): parse the block command and reject NaN numeric options
- "block <id> --on <blocker>" parsed --on and <blocker> as demanded
positionals, so the block command could never run; --on is an option
now.
- yargs type:number delivers NaN for non-numeric input; route
older-than/interval/timeout/ttl through finiteNumber so a poisoned
number fails with one line instead of deleting everything (prune) or
never firing (wait).
* fix(board): reject a NaN --agents in fleet up
yargs type:number yields NaN for non-numeric input, and the loop
condition i <= Math.max(0, NaN) is always false, silently spawning
zero agents. Reject non-finite --agents with one clear error.
* fix(board): keep the collision-suffixed name inside SAFE_NAME
A 64-char base plus a numeric suffix exceeded the 64-char limit, so the
claimed name produced a record no reader could address. Truncate the base
to leave room for the suffix.
* fix(board): move the mutex-eviction check outside runExclusive
The eviction ran inside the runExclusive callback, where isLocked() is
always true because the caller itself holds the mutex, so the delete was
unreachable and the map grew without bound. Run it in a .finally() after
runExclusive releases the mutex.
* fix(board): prune asks that settled as timeout
* fix(board): age timeout asks by their expiresAt in prune
* fix(board): carry --board/--as in the protocol command examples
The emitted instruction text gave commands without --board/--as, so a
recipient running in a different directory or as a different user would
target the wrong board or identity. Inject the flags into every command.
* fix(core): restore a callable sharp type in image-view
sharp 0.35 ships dual type entries: the ESM `.d.mts` declares
`export default sharp`, while the CJS `.d.cts` uses `export = sharp`.
packages/core is ESM, so `typeof import('sharp')` resolves to the
non-callable module namespace; index `['default']` to reach the callable
function+namespace type that the resize path (sharp.kernel) needs.
* fix(board): resolve the tsc errors blocking the board build
- pruneCollection redeclared `raw` in one block; rename the settle
timestamp to `timestamp` so the file contents read keeps `raw`.
- access USER/LOGNAME through the ProcessEnv index signature
(noPropertyAccessFromIndexSignature).
- supply the missing participantCount on the deadlock renderBoard call.
* fix(board): satisfy ESLint for the board files
- board.ts: route the non-interactive resolve refusal through
process.stderr.write instead of console.error (no-console)
- render.ts: merge the two @qwen-code/qwen-code-core type imports
(import/no-duplicates)
- rename boardCommand.ts / useBoardPending.ts to kebab-case
(check-file/filename-naming-convention) and update their imports
* fix(board): move board exports off the core barrel to keep iconv-lite out of the ACP closure
The board/fleet `export *` lines on the core barrel let the ACP agent
runtime (which imports the barrel) pull the board's dependency chain —
sync-file-encoding → iconvHelper → iconv-lite encoding tables — into its
static startup closure, tripping the serve fast-path bundle check. Re-export
the board surface from a dedicated `@qwen-code/qwen-code-core/board`
subpath and point the CLI importers at it; the barrel no longer carries the
board, so the ACP closure stays clean.
* fix(board): restore CI coverage
* fix(i18n): register board command description
* fix(board): narrow the collaboration MVP
* fix(board): harden persisted record boundaries
* test(board): satisfy terminal sanitizer lint
* fix(board): close round-4 findings — keep emit multiline, guard renders, fix dead fixtures
* docs: include agent board collaboration design
* refactor(board): narrow the collaboration surface
* fix(board): keep piped --json intact and validate --timeout before writing
`parseArguments` calls `process.exit` as soon as a board handler resolves.
`emit` wrote to stdout without waiting for the flush, so a `--json` snapshot
larger than the pipe buffer was truncated mid-record while the command still
exited 0. Waiting for the write callback also means a reader that closes early
has to be handled, since a closed pipe is reported both to the callback and as
a stream error event.
`ask --wait` validated `--timeout` only after `createAsk` had already
persisted the ask, so a rejected timeout left an orphaned open ask on the
board. The local wait is now validated before anything is written, matching
how `--ttl` already behaved.
* fix(board): report pruned items by id rather than filename
`prune --json` listed `a-<uuid>.json` / `t-<uuid>.json` while every other
machine-readable board surface identifies items by bare id, so a script
joining prune output against ids from `show --json` matched nothing.
* docs(board): clarify collaboration delivery sequence
* fix(core): accept settled timeout state in board ask records
parseAsk rejected state 'timeout' although AskState declares it and
settleAsk produces it, so a timeout record written to disk by a foreign
runtime sharing the board was silently dropped from listings and treated
as missing by getAsk/answerAsk/declineAsk. Accept it with the shape
settleAsk emits (settledAt present, no answer or reason payload).
Co-authored-by: Qwen-Coder <[email protected]>
* test(core): cover ask TTL transition, self-ask guard, and task pruning
Adds a dedicated asks suite: the expired-open auto-transition to timeout
that the CLI exit code 3 relies on, the createAsk from===to refusal,
foreign timeout records (valid and malformed), and parse-level rejections
exercised through getAsk/listAsks. Also pins pruneBoardTasks, which only
prunes completed tasks keyed on updatedAt — a path the pruneAsks tests do
not share.
Co-authored-by: Qwen-Coder <[email protected]>
* fix(core): bound board read fan-out and fold board name case
Two storage-layer fixes from the second-pass review, plus the two tests
that pin guarantees the suite did not pin, and one interop doc line.
listBoardTasks/listAsks opened one fd per record through a single
Promise.all, so peak fds grew linearly with the entry count and a board
larger than a low hard RLIMIT_NOFILE (docker --ulimit nofile=, systemd
LimitNOFILE=, CI runners) failed the whole listing with EMFILE and no
partial results. Read in batches of 32 instead; the trailing createdAt
sort keeps the result order unchanged.
Board names reach the filesystem verbatim, and APFS/NTFS fold case while
ext4 does not, so Sprint and sprint were one board on macOS and two on
Linux. Fold case where the directory name is derived, so one name means
one board on every filesystem.
board-items.test.ts: the prune re-check test mutated the record after a
single setImmediate, which lands before prune's readdir returns, so a
read-before-lock implementation still passed. The test now holds the
cross-process lock itself and waits until prune is provably blocked on
it before reopening the ask.
board-tasks.test.ts: adds the contended-claim test (deleting the
"already claimed by" guard turns 1 winner into 8) and a listing test
over more records than one read batch.
agent-board.md: an expired ask stays state "open" with settledAt null on
disk because timeout is derived on read, so a foreign reader must apply
now >= expiresAt itself.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtzb8sj51m
* fix(ci): cover the ./board core export in the capture loader
Merging main brought scripts/tests/text-capture-core-loader-sync.test.js, which requires the skill-review-harness loader's inline named map to carry one entry per named key of core's exports map. This PR adds ./board (packages/core/src/board.ts, dist/src/board.js), so the map needs ['board', 'board.ts'] -- a missing entry whose specifier has no mirrored src file falls through to the exports map and the capture dies with ERR_MODULE_NOT_FOUND on a fresh clone.
Also format packages/core/src/agents/team/asks.test.ts. Step 8 of Lint & Static (Check lint gate freshness) had been failing in ~24s on every prior run of this branch, which short-circuited step 26 (Run Prettier); once the gate could run, prettier flagged this file's multi-line import, which fits on one line. Formatting only, no assertion touched.
Verified: vitest run --config ./scripts/tests/vitest.config.ts on text-capture-core-loader-sync.test.js and integration-tsconfig-core-paths-sync.test.js = 2 files / 4 tests passed; prettier --check clean on both changed files.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmtzb8sj51m
---------
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
|
||
|
|
9d69c91a64 |
fix(web-shell): pin es2021 floor across both build configs and harden terminal tests (#11758)
- Resolve the build-config probe through web-shell's own Vite 5 instead of the root-hoisted Vite 7, parameterize it over vite.config.ts and vite.lib.config.ts, and pin build.target itself (R1-1, R1-2). - Share WEB_SHELL_BUILD_TARGET between the app and lib configs so the published lib bundle gets the same es2021 floor (R1-2). - Pin the localized protocol-mismatch notice and the releaseRequested flag in the mismatch test (R1-3, R1-4). - Drop the duplicated node-pty pin count owned by conpty-host.test.ts (R1-5). Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
e6647b3a3b |
fix(cua): bound observations and reduce model round trips (#11735)
* fix(cua): retain bounded observation baselines and cap text * perf(cua): reduce avoidable computer-use round trips * perf(node-repl): avoid repeated runtime rules in tool discovery * fix(cua): recognize attached sheets in exact-target validation * fix(cua): expose known refusal categories in app errors * perf(cua): Combine macOS bootstrap with initial app observation * perf(cua): Prefer exact selection for field replacement * perf(cua): Lead field edits with verified text selection * chore(cua): reconcile local optimizations with updated App routing |
||
|
|
5f57c78b06 |
feat(cua): add app-bound actions and compact native observations (#11705)
* feat(cua): add app-bound actions and compact native observations * fix(cua): complete native app input and AX alignment * feat(cua): add macOS text operations and platform skills * fix(cua): preserve app observation context * fix(cua): reduce app action recovery failures * fix(cua): align macOS app targeting with native service |
||
|
|
2eaed75010 |
fix(cua): address tool failures and efficiency in OSWorld runs (#11683)
* fix(cua): address tool failures and efficiency in OSWorld runs * docs(cua): consolidate efficiency bugfix design notes |
||
|
|
f1d5a64bbf |
fix(core): strip AppImage PYTHONHOME/PYTHONPATH from stdio MCP children (#11763)
* fix(core): strip AppImage PYTHONHOME/PYTHONPATH from stdio MCP children The Desktop AppImage exports its bundled Python's PYTHONHOME/PYTHONPATH globally; a stdio MCP server's own interpreter then resolves its standard library against the AppImage mount and crashes at startup (#11718). Strip both from the inherited environment when spawning stdio MCP servers under the desktop shell only: the CLI leaves a user's own Python setup untouched, and an explicit per-server env entry still wins. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): match the desktop marker read used elsewhere ('1') Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
79dabcb832 |
fix(web-shell): keep mid-turn messages the daemon rejects at idle (#11289)
A mid-turn message the daemon refuses with `session_idle` is not a failure: the session can take an ordinary prompt instead. The web shell fell back to that route but then had no way to tell what the daemon did with the message, so it surfaced a rejection toast for a message that had run, or echoed it twice, or dropped it in silence — and a queue row could be left that the user could neither see nor remove. The fallback now confirms the resubmission against a pending-prompt snapshot instead of trusting the activity mirror, which another client's queued prompt can lag, and a row the daemon refused at idle carries that provenance through a hold and the drain so a later release confirms the same way. Echoes are sourced from the payload the client actually holds — a row bound by the daemon's id, a stash keyed by it, or the started event's own rendering — and every route that echoes leaves a record that outlives the settle, so no submit body appends the same message twice. A start that arrives while a removal is in flight parks, and every removal-failure arm replays it, so a message the daemon really ran is not lost because the user's cancel lost the race; it replays only this client's own messages, and never as the daemon's `[image]` placeholder. Attribution of a suppressed start to a row requires an event this client owns, because a rendering is not an identity. A row the drain has stamped but not yet released cannot own a snapshot entry or a started event, so it is out of every match count: it can no longer be spliced or bound to a prompt it is not, which read as a cancellation and bailed without posting, reporting or restoring anything. A cancellation that could not be confirmed is recorded and applied by the next snapshot that can prove something, and an owner change issues those removals rather than wiping them. Also documents the mid-turn resend rule after a refused close: waiting for the promotion is the default and is loss-free, and forcing the drain means releasing every still-queued id before re-posting any, because no DELETE verdict on its own establishes delivery. Co-authored-by: probe <probe@local> |
||
|
|
b3975b7568 |
fix(cli): prevent dialog clipping in short terminals (#9040)
* fix(cli): prevent dialog clipping in short terminals * fix(cli): address dialog height review feedback * test(cli): pin dialog height budget branches and document locked-skill collapse * fix(cli): make dialog height budgets width-aware Cap MultiSelect labels to the render width and truncate counted single-line texts so the height budget holds at any terminal width; give the interactive unlocked list first claim on flexible rows and let the locked block collapse to (or drop) its hint instead of overflowing. * fix(cli): harden dialog height budgets * fix(cli): shed dialog chrome below the height-budget floor Mirrors the /statusline compact path in /skills: budgets at or below the fixed frame drop border, paddingY, and footer so the always-rendered list row fits. Truncates every remaining counted text (translated titles, Type to search, Preview) at any width, folds \v/\f in status line fields, and notes hidden locked skills in the subtitle when no locked row fits (round-5 review). * test(cli): pin round-5 budget fixes Covers the compact-frame budgets (11/10/8), the locked-only query at a frame-floor budget, the ca-locale 26-column title, the subtitle locked count at residual === 1, \v/\f folding, and diverges the two width sources in the DialogManager wiring test (round-5 review). * fix(cli): translate the locked-skills subtitle badge * fix(cli): shed skills dialog chrome to the bare list at tiny budgets Round-7 R7-1: the /skills compact frame still floored at 6 rows (title + subtitle + search + list margin + the forced list row), so height budgets <= 5 overflowed and overflow="hidden" clipped the interactive list itself. Add a bare tier that sheds the remaining chrome at budgets <= 5 so only the list area renders, mirroring the /statusline compact path. The hidden search row also bypasses and suppresses query mutation so a retained query cannot filter invisibly. * fix(cli): simplify short skills dialog handling * test(cli): pin review round-9 suggestions for dialog height fix - assert absence of (+N locked) badge in unconstrained all-locked dialog - cover \r in status-line control-character collapse cases - use feminine participles in fr/ca locked-count badge translations * fix(cli): address dialog height review feedback * fix(cli): preserve dialog search and label behavior * test(cli): restore skills dialog config mock * fix(cli): preserve locked skill search within height budgets * docs: record PR 9040 review and verification follow-ups --------- Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
271c968095 |
fix(dingtalk): preserve callback text (#11757)
Co-authored-by: qqqys <[email protected]> |
||
|
|
ee1ebcc167 |
fix(dingtalk): handle replied rich-text media (#11759)
Co-authored-by: qqqys <[email protected]> |
||
|
|
35a9c5c62d |
perf(web-shell): load metadata on demand and reuse capability checks (#11644)
* perf(web-shell): load metadata on demand and reuse capability checks * fix(web-shell): address demand-loading review regressions * fix(web-shell): address demand-loading review suggestions Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): pass explicit initial value to git poll target ref React 19 typings require useRef to receive an initial value; the zero-argument call failed tsc -p tsconfig.lib.json (TS2554) and rejected the previous round's build. Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): restore slash input after pre-admission failure --------- 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]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
e6e075996a |
fix(cli): break Ink useBoxMetrics commit-phase setState loop (React #185) (#11565)
* fix(cli): break Ink useBoxMetrics commit-phase setState loop (React #185) * fix(cli): scope the ink useBoxMetrics loop guard per instance The guard added to the vendored Ink patch kept its 32-entry budget in module scope, so every useBoxMetrics instance shared one budget, and the budget counted invocations rather than oscillation cycles. With 40 boxes mounting in a single commit, instances past the 32nd invocation never measured: hasMeasured stayed false for good, because no later commit re-fires their layout listener. Hold the budget in a ref so each instance refills on its own. Cancel the pending budget-refill timer on unmount. Vitest fakes setImmediate by default and vi.useRealTimers() discards a pending faked immediate, so the stored handle stayed non-null after the timer was gone and the counter never refilled. Drop the functional same-value bail for setHasMeasured. The useState setter already bails for an identical primitive, and the functional form cannot be eagerly bailed - React has to schedule the render to evaluate the updater. Reverted to upstream's setHasMeasured(Boolean(ref.current)). Regenerate patches/ink+7.0.3.patch with patch-package instead of hand-appending the hunk, so it lands in path order with an index line and function-context headers. The regeneration also reformats the pre-existing hunks (index hashes, hunk-context labels, offsets); that churn is cosmetic, and the only content change outside the new hunk is one "\ No newline at end of file" marker. Adds packages/cli/src/ui/use-box-metrics-loop-guard.test.tsx, which renders the real hook and pins three behaviours: an oscillating box settles without React #185, a later resize still re-measures once an oscillation tripped the guard, and 40 instances all measure within one commit. * fix(cli): refill the ink useBoxMetrics budget from elapsed time The measurement budget added to the vendored ink hook refilled itself from a setImmediate callback. A test that installs fake timers only runs that callback when it advances the clock, and the InputPrompt suite flushes renders with microtask yields inside act(), so the refill stayed pending and React's act queue never drained with it. Ten tests in that file each burned their full 15s timeout; on CI, where the suite runs with --retry=2, that single file outlived the 60 minute job limit and the lane was cancelled without printing a summary. Refilling from elapsed wall-clock time keeps the same per-instance budget and the same protection against the commit -> layout listener -> setState loop, and leaves no handle for a test to drain, so the unmount cleanup is no longer needed either. --------- Co-authored-by: yiliang114 <[email protected]> Co-authored-by: yiliang114 <[email protected]> |
||
|
|
bc7a186cda |
fix(web-shell): finish terminal replay follow-ups (#11748)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
ddd6eea9dd |
test(core): account for the extra Windows fstat in the tail-growth session mocks (#11750)
The no-follow open fallback used when O_NOFOLLOW is unavailable performs an lstat/open/fstat identity check while opening the file, adding one fstat before the first tail read. Two tests inject a stale size on the first observed fstat to simulate growth between the stat and the read; on Windows the injection now lands on the identity fstat, so the first tail read sees the final file and one case fails deterministically in the nightly Windows lane. Skip the open-time identity fstat (one call, only where O_NOFOLLOW is missing) so the stale size reaches the first real tail read on every platform. POSIX behavior is unchanged, and the sibling re-read case now actually exercises its intended stale-tail race on Windows. Regression from #10007, which landed after the #10064 Windows lane sweep. Co-authored-by: F415643 <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
fa20933437 |
fix(dashscope): send request metadata only for qwen-family models (#11606)
* fix(dashscope): send request metadata only for qwen-family models Fixes #11590. DashScope's OpenAI-compatible endpoint is an aggregating gateway. Qwen Code injects a top-level `metadata` object (sessionId / promptId / channel) into every request sent there, but for a non-qwen model such as `ZHIPU/GLM-5.3-Flash` the gateway forwards the body to that vendor's own backend, where `metadata` is typed as a string. Deserialization fails and the request comes back as a flat "400 API 调用参数有误", which makes the model completely unusable through Qwen Code. The reporter captured two request bodies differing in exactly one top-level field: the one carrying `metadata` 400s, the one without it returns `finish_reason: stop`. `metadata` is a platform-private tracing field that only means anything to DashScope's own inference path, so it is now gated on the wire model. This is the same gate, for the same stated reason, that `buildQwenEffortConfig` already applies to the qwen-only thinking knobs a few methods above: "so the qwen-specific fields never leak to a non-qwen model sharing the DashScope endpoint." Scoped deliberately: - `buildMetadata` keeps its signature and behavior; the gate lives in a new private `buildRequestMetadata` used at both spread sites, so the existing public method and its tests are untouched. - Both the vision and non-vision paths are gated. In practice every model in VISION_MODEL_PREFIX_PATTERNS is qwen-family and `coder-model` is matched by isQwenFamilyWireModel, so the vision path is unchanged today; gating both keeps them from drifting. - This does not walk back #9103. That PR widened which *origins* count as DashScope-compatible (`*.alicloudapi.com`), which is orthogonal to which models get the field. A qwen model behind such a gateway still ships metadata, and there is a test for exactly that. 7 tests added. The three "omits metadata for the non-qwen model" cases fail against unpatched sources; the qwen-family and alicloudapi cases guard against regressing the other direction. dashscope.test.ts: 176 passed. tsc, eslint and prettier clean on both files. * feat(dashscope): add enableRequestMetadata to override the qwen-family gate Addresses the review's first concern on #11606: the gate keys on the model, but the client cannot tell a request DashScope forwards to a third-party backend from one DashScope serves itself. A non-qwen model on a first-party host under QWEN_OAUTH loses sessionId/promptId correlation too, and that request does not 400, so nothing is bought by dropping the field there. packages/core/src/core/openaiContentGenerator/pipeline.test.ts exercises exactly that shape (glm-5.2 under QWEN_OAUTH). `enableRequestMetadata` is tri-state and mirrors the existing `enableCacheControl` off-switch, read off the same config object by the same accessor: undefined keeps the auto behavior (qwen-family only, which fixes #11590), `true` restores the field for first-party non-qwen sessions whose tracing matters, `false` suppresses it everywhere. Kept as its own commit so it can be dropped if the preferred answer is the model gate alone, or if the field should instead be stripped gateway-side. 3 tests added. dashscope.test.ts: 179 passed. tsc, eslint, prettier clean. * fix(dashscope): wire enableRequestMetadata through model configuration The previous commit added enableRequestMetadata and the provider reads it off the resolved ContentGeneratorConfig, but nothing populated it. resolveGenerationConfig() copies only the fields in MODEL_GENERATION_CONFIG_FIELDS, and that list did not include it, so a settings or modelProvider generationConfig entry was discarded before it ever reached the provider. The option was readable and unsettable: the escape hatch documented on the PR could not actually be used, and the tests did not catch it because they injected the config getter directly and so never exercised the delivery path. Registered in both places the field has to appear: MODEL_GENERATION_CONFIG_FIELDS in models/constants.ts, which is what the resolver iterates, and the ModelGenerationConfig Pick in models/types.ts, which tsc flagged as soon as the first was added. This is the same pair enableCacheControl occupies, which is the switch this option was modelled on. 4 tests added in modelConfigResolver.test.ts going through resolveModelConfig rather than a stubbed getter: settings value carried through with the right source, an explicit false carried through so the option can suppress as well as restore, modelProvider winning over settings, and undefined staying undefined so the model-family default holds. Three of the four fail without the constants change. models + dashscope suites: 431 passed. tsc, eslint and prettier clean. Reported by @doudouOUC on #11606. * docs(settings): document enableRequestMetadata in model.generationConfig The delta review on #11606 noted the option was settable but undocumented: docs/users/configuration/settings.md enumerates the supported model.generationConfig keys and did not list it, which is where a user would look now that the key actually reaches the provider. The entry also answers the second suggestion from that review rather than changing behavior for it. An explicit false does not survive a model switch, because resolveGenerationConfig re-derives the field on each auth/model resolution. That is pre-existing pipeline behavior shared with enableCacheControl and samplingParams, not something this PR introduces, so the docs say the value is re-derived instead of giving this one field a bespoke re-apply that its siblings do not have. git diff -w is 2 insertions and 2 deletions; the other 25 changed lines are prettier repadding the table, since the model.generationConfig row was already the longest in it at 1369 characters and any addition widens the column. * docs(settings): point provider models at the key that actually applies 📍 Addresses the two documentation points from the maintainer verification on Where the override lives. `enableRequestMetadata` was documented only under `model.generationConfig`, but for a model declared in `modelProviders` that copy is ignored, which is #11590's own configuration shape. Now says to define it in `modelProviders[].generationConfig.enableRequestMetadata`, matching the wording `customHeaders` and `extra_body` already carry on the same line. Removed "re-derived on each auth/model resolution, so it is reapplied when you switch models". On the `openai` auth path `/model <id>` requires `modelProviders`, and with `modelProviders` present the `model.generationConfig` copy is ignored, so the sentence promised something unreachable on the path most users are on. Added a note that an explicit true/false is read from the session's ambient configuration, so a side query issued for another model may not observe a per-model override. That is the session-scope caveat rather than a behaviour change; reading it off the provider's own contentGeneratorConfig is worth doing separately, and it only affects the explicit switch, not the model-family gate that is the actual fix. `git diff -w` is 2 lines. The rest is prettier realigning the table columns after the cell grew. prettier --check is clean and was clean before. * test(dashscope): pin the metadata gate on the vision call site 🔍 Closes the coverage note from the maintainer verification on #11606: both call sites were gated at runtime, but only the non-vision one had a test. buildRequest has a second, separate return for vision models. A regression there would leave `metadata` on the wire for a non-qwen vision model while the non-vision path stayed correct, which is the exact failure the gate exists to prevent, and nothing would have gone red. Two tests, mirroring the existing pair: metadata ships on the vision path for a qwen-family vision model, and is suppressed there under enableRequestMetadata: false. Both assert vl_high_resolution_images is true so a future change that routes away from the vision branch cannot make them pass for the wrong reason. Verified by mutation. Swapping buildRequestMetadata for buildMetadata at the vision call site only, leaving the non-vision site intact, turns the suppression test red and leaves the other green, which is the correct split. dashscope.test.ts plus packages/core/src/models: 433 passed. prettier and eslint clean. * fix(config): carry enableRequestMetadata across a qwen-oauth hot model switch 🔁 The DashScope metadata gate reads enableRequestMetadata off getContentGeneratorConfig(). A qwen-oauth /model switch rebuilds that object field by field and the new flag was not in the list, so a per-model override kept reading the previous model's value until the next full refresh. Copy the flag and its source alongside the cache flags, and pin it with a switch test that fails without the copy. Also drop the alicloudapi metadata test that duplicated the existing routing test at line 553. * fix(dashscope): read enableRequestMetadata off the provider config first 🎯 A side-model generator is built with its own per-model config but shares the session Config, and the metadata gate only read the session value, so a per-model opt-out never reached it and an ambient true still shipped metadata to a backend that rejects it. Consult the provider's own config first and fall back to the session. On the main route both are the same object, which the qwen-oauth hot switch mutates in place, so nothing is latched. Pin it with two provider-vs-session cases that fail without the change, and a vision-path case whose configured model and request model disagree so the call site's model argument is what the gate sees. The settings row now describes the provider-first read instead of the session-scoped one. * fix(dashscope): read enableRequestMetadata from the provider config alone 🔒 The session fallback added last round re-admitted a value that buildAgentContentGeneratorConfig deliberately clears for a cross-provider agent, so the main model's setting could decide a side query on a vendor-forwarded model. Read only the provider's own config; on the main route that is the same object the hot switch mutates, and side generators already carry their per-model value or a deliberate undefined. The knob tests now pass the flag on the constructor config, the production shape, and a new case pins that a provider config with no value ignores a session true. It fails with the fallback in place and passes without it. * docs(settings): say the metadata switch is read per model, not from the session 📖 * test(dashscope): pin the session-false direction and tighten two descriptions 🧭 A same-provider subagent inherits the parent config, so the settings row now limits the isolation claim to a side model under a different provider. The hot-switch test comment names the provider's own config as the read path. A new case pins that a session false does not strip metadata when the provider config carries no value. * fix(models): let a side model decide enableRequestMetadata for itself 🧬 A same-auth-type side model built from a registry entry inherited the main model's explicit enableRequestMetadata, so a first-party main model set to true made every vendor-forwarded side query ship the DashScope tracing object and fail with the 400 this switch exists to avoid. Treat the field like thinkingMandatory in applyResolvedModelConfig: it follows the side model, and an unset side model falls back to the automatic gate. Pinned by a same-provider inheritance test that fails without the change. The settings docs gain two notes: an unset side model does not inherit the main model's explicit value (a fastModel absent from modelProviders still does), and the switch has no effect under Qwen OAuth, where the whole model.generationConfig block is not applied. * docs(settings): an unlisted fastModel runs on the main model, it inherits nothing 📎 |
||
|
|
d47fa0202d |
fix(core): retry status-less upstream errors instead of ending the turn (#11291)
* fix(core): retry status-less upstream errors instead of ending the turn
A gateway that fails mid-stream — pushing `{"error": {...}}` into an
already-200 SSE response — reaches us as an OpenAI SDK APIError carrying no
HTTP status: the body's `code` and `message`, plus the response's
`x-request-id`. Observed in the wild as `code: 'KeyError'`,
`message: "'id'"`, a server-side defect that ended a four-hour turn on the
first attempt, while socket cuts on the same session were retried without
incident.
The retry classifier had no branch for that shape, so it fell through to
'unknown', and both status-less gates — the default predicate and the stream
path's inline one — matched transport codes only. They now defer to the
classification's verdict, and a status-less error carrying the provider's own
request id classifies as retryable.
The request id is the whole gate. Keying on the provider `code` instead would
sweep in permanent local failures that carry a string code and no status — a
missing API key, an invalid MCP server config, MCP's numeric JSON-RPC codes —
and walk them through the full retry ladder. Only an upstream request id
evidences that the provider accepted and traced the request before failing.
The SDK spells that field `requestID`, which the classifier did not read at
all, so it is now recognized alongside `request_id`/`requestId`.
Retries stay bounded: persistent mode still requires a 429/529 status, model
fallback still requires a status, and mid-stream replay after partial output
still requires a transport classification — so a status-less upstream error
gets the normal seven attempts and can never duplicate user-visible text.
Tests pin the observed shape both as SDK properties and as a provider body
embedded in the message, drive both gates, and guard the two verdicts that
must not move: a code-only local failure and a traced 4xx.
* fix(core): honour the retry verdict where mid-stream errors actually land
Review round on #11291. The status-less upstream class the previous commit
taught to the classifier never reached either gate it widened: both run while
the stream is being established, and the OpenAI SDK builds that error inside
the lazy SSE iterator, after establishment has already resolved. The mid-stream
catch computed the classification and then ignored it — its only retry exits
required a socket code — so the reported turn still died on the first attempt.
The catch now admits the class to both recovery mechanisms beside the transport
one, through a predicate of its own: replay when nothing user-visible has been
delivered, continuation from the delivered text once it has. Both keep the
existing budgets and every no-duplication precondition, and the socket
allow-list is untouched because the Anthropic generator shares it to decide
whether deferred tool-call chunks may be released.
A request id separates upstream from local, not transient from permanent, so
the branch now fails fast on the provider codes that can never succeed on a
resend — moderation, credentials/billing, malformed request — matched case- and
separator-insensitively, while an unrecognised code such as the observed
KeyError stays retryable. An empty request id no longer opens the gate: the
header can be present and empty, and the value is now load-bearing.
The status-less verdict is computed once and shared by both establishment
gates, which had each copied it, and an SSE-delivered instance now reports the
same kind as its siblings. The comments that described the classification as
driving one control decision name the decisions it actually drives.
Tests drive the mid-stream path by throwing from the generator's first next()
instead of rejecting establishment, and cover the continuation path, the
permanent rejection that must still propagate, the transport-over-request-id
ordering, and both empty-id readers. Each was mutation-checked: removing the
production change it pins turns it red.
* test(core): stop leaving the new rejection fixtures unhandled
CI failed the unit job on three unhandled rejections while all 23,749 tests
passed. All three came from tests the previous commit added.
Two were in the retry predicate cases: advancing fake timers between creating
the promise and asserting on it left the rejection with no handler for a tick.
The advance is gone and the assertion attaches immediately, matching the other
non-retry cases in the file — the crispness it bought on a regression is not
worth an unhandled rejection, and the case still reds, by test timeout.
The third drove a stream helper that builds its collector, awaits two timer
steps, and only then hands the collector back, so a stream rejecting in between
is briefly unhandled. That test now drains through the helper that catches
inside the loop, and drops fake timers it never needed: nothing retries on the
path it asserts, so there is no backoff to advance through.
Both still discriminate. With the permanent-code list disabled the retry case
reports three calls instead of one, and the mid-stream case reports a second
generator call in about a second rather than hanging.
* fix(core): keep oversized and malformed rejections out of the new retry gate
Third review round on #11291. Widening the mid-stream recovery gate exposed an
ordering fault of its own: the gate is consulted before the one-shot overflow
recovery below it, so a status-less input-length rejection was re-sent instead
of compacted, and the continuation arm re-sent it strictly larger each time.
Both overflow detectors are now computed above the gate and their hits excluded
from the widened verdict — from that verdict only, so a socket cut keeps the
verdict it had before this branch existed.
A request id separates upstream from local but says nothing about permanence,
and the permanence list read only `code`. The OpenAI SDK puts
`invalid_request_error` on `.type` with `code` null, so the canonical
malformed-request body never matched and a rejection that cannot succeed was
re-sent through the whole ladder. `.type` is now a second input to that guard,
matched against the same anchored list, and kept out of the classification
struct because `providerCode` is compared by exact equality elsewhere.
The list also grows the codes the previous round measured by name — billing
exhaustion, model access, output moderation, the OpenAI malformed-parameter
spelling, and oversized input — while staying anchored on purpose: a sub-code
qualifier left unmatched costs a wasted ladder, whereas over-matching costs a
transient failure that is never retried, which is the defect this branch exists
to remove.
The three stream-recovery logs now carry the classification's reason, provider
code and request id. They previously recorded only a kind and a socket code, so
the class this PR recovers logged a label pointing at sockets and proxies that
were never involved, and discarded the one field a gateway ticket can be filed
against.
Two comments that this branch falsified are corrected: the Anthropic release
gate no longer claims parity with a replay boundary that has since widened, and
the `sse-provider` gloss no longer claims the label means "delivered by the
stream" when it records raw framing surviving into the message.
Tests: a status-less overflow must reach compaction rather than replay, a
permanent `.type` must fail fast while `server_error` stays retryable, the
extended code list, and an oracle that drives the real SDK constructor with
real headers so a dependency bump renaming `requestID` goes red instead of
silently reintroducing the incident. Each was mutation-checked in both
directions where the assertion could be satisfied by accident.
* fix(core): stop resuming answers that already delivered a finish reason
Fourth review round on #11291. The continuation arm the previous round
widened to status-less upstream errors had no completeness precondition:
a gateway error frame that lands after the chunk carrying a terminal
finishReason — the SDK's error scan is position-independent, and the
pipeline keeps pulling the iterator to absorb trailing usage metadata —
was resumed as if the answer had been cut. The resume instruction
asserted a connection drop that never happened, and the fabricated tail
was folded into durable history and the JSONL record. The gate now only
continues an attempt that ended without a finish reason, or with
MAX_TOKENS — the truncated shape the arm exists for.
The remaining round-4 findings were pinning gaps, each closed with a
test and mutation-checked: the payload-overflow half of the R3-1
exclusion (a status-less 413 reason phrase now has a compaction case of
its own), the classifier fields on the continuation-scheduled and
retry-not-taken logs, the Anthropic release gate's deliberate narrowness
for a traced status-less error, the `.type` permanence guard driven
through the real SDK constructor, and the message-scraped half of the
permanent-provider-code read — both the SSE-framed body and the nested
`.error.code` shape.
* fix(core): accept answers that completed before a trailing stream failure
The completeness guard keyed on lastFinishReason could never fire on the
real throw path: the OpenAI pipeline parks the finish chunk for the usage
merge and discarded it when the iterator threw, so a gateway error frame
landing after a finished answer looked identical to a cut mid-answer.
Flush the parked finish response ahead of the propagated error so the
completion signal reaches LlmChat, and decide the post-completion case
explicitly: a stream that fails after a closed finish reason (STOP,
SAFETY, ...) with content already delivered is accepted as the turn's
outcome and persisted, instead of failing the turn or fabricating a
continuation tail into durable history. The veto is now a deny-list of
closed reasons so unmapped values (FINISH_REASON_UNSPECIFIED) fail open
to continuable, and the not-taken log names the terminal finish reason
as the decline cause.
* test(core): annotate closure-captured results array in pipeline test
The array is pushed from inside an async closure, so the evolving-any
inference does not apply and tsc --build rejected it under noImplicitAny
(TS7034/TS7005). Give it the explicit GenerateContentResponse[] type used
elsewhere in the file.
Co-authored-by: Qwen-Coder <[email protected]>
* fix(core): align parked-finish flush and Anthropic release gate with LlmChat recovery
* fix(core): cast pendingFinishResponse at error-path tool-call check
The deterministic gate rejected
|
||
|
|
b5567bb7a9 |
fix(core): run web terminal PTYs on the bundled ConPTY backend (#11643)
* fix(core): run web terminal PTYs on the bundled ConPTY backend The web terminal spawned its PTYs with node-pty's default inbox ConPTY backend, where a natural shell exit orphans the conhost.exe --headless the backend spawned: the native exit watcher erases the pty baton before delivering onExit, pty_baton has no destructor, and the erased HPCON is unreachable from JS (microsoft/node-pty#965). Every exited web terminal leaked ~8 MB for the life of the CLI, and a pin bump does not fix it. Mirror the #11497 shell path: spawn with useConptyDll on Windows so the bundled backend releases its host reference right after spawn and the host exits with its last client. The bundled backend adds one synchronous throw point — a missing or unloadable conpty.dll — and a web terminal has no child_process fallback, so a failed bundled spawn retries once on the inbox backend: the pre-fix leaking behavior beats a terminal that cannot start at all. A failed spawn produced no child, so the retry cannot double-spawn. The 4001 non-retryable close path is unchanged. Also switch the agent-view PTY host to the bundled backend (same leak, lower rate), refresh the conpty-host release docs, and add a VERIFIED_NODE_PTY pin test so a node-pty bump forces re-verification of the native teardown semantics the release path depends on. Refs #11352 * fix(core): derive node-pty pins and widen the conpty-host pin guard The six `@lydell/node-pty*` pins live in three unsynchronized places — the root manifest, packages/core's manifest, and a hardcoded map in scripts/prepare-package.js — but the conpty-host tripwire asserted only one key of one manifest, so bumping the win32 prebuild keys or the root manifest left it green and skipped the human re-check it exists to force. Derive the six pins in writeDistPackageJson from packages/core's manifest (deleting the third hardcoded table), and widen the tripwire to assert all six core keys plus an equality against the root manifest's six — the declaration packages/cli's agent-view actually resolves in a dev tree. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxennouyn * fix(core): answer and scrub bundled-backend terminal queries in the web terminal Switching the web-terminal registry to the bundled ConPTY backend let a probing shell's DA / DSR query bytes into the recorded scrollback; a reconnect that replays the buffer made the client's xterm.js re-answer each query and write the fresh reply back into the still-live shell's stdin. The bundled backend answers no queries itself, and the browser is not guaranteed to be attached when the startup probe fires. Mirror the other half of the shell path (#11497): on win32, feed a headless terminal the PTY stream so it answers the probe server-side, and strip the query bytes from both the scrollback and the live stream so a replay cannot re-emit them. Also bring the releasePtyResources docstring in line with the bundled backend (R1-8). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxennouyn * fix(core): close query-strip gaps and a race in the web terminal Three review findings on the bundled-ConPTY web terminal path: - The scrollback scrub was a stateless per-chunk regex over only the `c`/`n` finals, so the query families xterm.js answers beyond those (tertiary DA, DECRQM, DECRQSS, XTVERSION, DECREQTPARM, OSC colour queries) still reached the scrollback and were re-answered into the live shell on reconnect. Replace it with a stateful stripper covering every family and carrying a probe split across two chunks. - resize() resized only session.pty, leaving the headless query responder on its 80x24 spawn grid, so geometry-dependent replies were computed against the wrong size. Forward the resize to session.queryTerminal. - loadXtermHeadless() was a second suspension point whose cancelledCreations/ disposed guards were not re-checked, so a release()/releaseWorkspace()/ dispose() landing during the import leaked a PTY the caller had cancelled. Re-check both after the import. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxrimarz8 * fix(core): strip DCS DECRQSS queries in the terminal query filter Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmty6iqs0zv * fix(core): bound the terminal query stripper and tie it to its responder The stateful stripper held any unterminated OSC (a title SET included) with no cap, so one stray ESC ] swallowed every later chunk and grew pending unbounded; the DCS alternative also required the full introducer, leaking its halves that reassemble into a re-answered DECRQSS. Hold only viable query prefixes, cap the hold, end an OSC partial at a bare ESC (accepting the C1 ST terminator), hold the DCS introducer, and gate the stripper on the responder so a failed headless load no longer strips with nobody left to answer. Correct the query-set doc comment. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtyf3dx109 * style(core): format web-terminal-registry.ts with prettier Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmtyhy9mj0e * fix(web-terminal): stop scrubbing the OSC colour family the server cannot answer The stateful stripper added here removed the OSC 10/11/12/4 colour queries from both the scrollback and the live listener, but the pinned @xterm/headless 5.5.0 responder answers none of them: `onData` carries DA/DSR/DECRQM/DECRQSS only, while `_setOrReportSpecialColor` reports on the internal `_onColor` emitter that the headless Terminal does not expose (`term.onColor` is undefined) and that nothing in this file subscribes to. Deleting the family from the browser's stream as well left a probing program unanswered, where the browser's own xterm.js 6.0.0 `_handleColorEvent` is the answerer and answered it at the merge base. Narrow both regexes to the families the responder actually answers (plus the never-display-content DA3 / XTVERSION / DECREQTPARM trio that no build answers), drop the half-covered colour arm from the partial matcher — so the strip set and the doc block no longer disagree about what the colour family is — and correct the doc block to state the split. Covered by a new test asserting the whole family reaches both the live listener and the snapshot untouched, and that nothing is written back; it fails if the colour arm is restored. The colour queries that a reconnect replay re-answers are part of the replay-suppression redesign tracked in #11734. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtyno0zh0n * fix(web-terminal): separate live replies from reconnect history Preserve raw PTY bytes and limit the Windows server responder to primary DA. Mark snapshots explicitly and restore history into a detached browser terminal while the old terminal continues forwarding user input. Activate live replies at the snapshot write callback, reject incompatible peers, and preserve the parallel color-query regression fix. Refs #11734. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
4f56430138 |
fix(lsp): synchronize target documents before queries (#11443)
* fix(lsp): synchronize target documents before queries * fix(lsp): address document synchronization review findings * fix(lsp): address second-round document synchronization review findings Evict an un-synchronizable document where the throw is raised, so one deleted or unsupported file stops being fatal for the rest of the connection, while workspace synchronization failures still escape the ordinary pull-request catch instead of returning partial success. Certify a prepare result that points at another file by observing that file inside the synchronous signing loop, where no await separates the response from the observation. The documented prepare-then-traverse workflow no longer fails closed on a fresh cross-file item, and it costs no notification, no settle delay and no second round trip. Nested items whose own file drifted stay unsigned without dropping healthy siblings, non-file items report non-retryable guidance, and tokens bind a text digest plus delivered version rather than the whole document. An unreadable warmup probe no longer cancels workspace symbol search, and the manager only blames the synchronization callback when the server's capabilities really cannot accept a notification. Both design docs now define each mutation gate inline instead of citing identifiers that resolve to nothing in the tree. * fix(lsp): resolve R3 review findings and fix replayUris memory leak - Fix memory leak: add replayUris.clear() in stop() - Resolve all 17 R3 review findings (3 Critical + 14 Suggestion) - Fix TypeScript crash restart document replay - Improve error messages with specific file URIs - Add pin tests for all fixes * fix(lsp): preserve durable replay obligations across connection change and reload Park wiped URIs into replayUris before clearing openedDocuments on a connection change, fold the durable set into the reload snapshot and the workspace-sweep tracked set, and evict a never-delivered unreadable URI so one permanently unreadable file cannot wedge later sweeps. * fix(core): preserve LSP replay and hierarchy provenance --------- Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
b4d61e3a0b |
test(web-shell): give the 200-record history-viewport case its own budget (#11740)
The variant runs the same assertions under Emulation.setCPUThrottlingRate 4x over a 2,400-record fixture, so it consumes 75-91% of the shared 60s Playwright timeout: 45,172 / 47,209 / 53,985 / 54,697 ms across four recent CI runs, and on run 34605772996 all three attempts timed out (63,825 / 63,081 / 62,428 ms), reding the smoke job of an unrelated PR. Give the variant 120s, next to the throttle that makes it slow, following web-shell.stream-performance.spec.ts. The throttle itself is the slow-rendering condition the reading-row assertion exists to prove, so it stays as is. Refs #11736 Co-authored-by: yiliang114 <[email protected]> |
||
|
|
b9eb29fdd1 |
feat(web-shell): add model role and context window configuration (#11342)
* feat(web-shell): add model role and context window configuration * fix(cli): mock image settings in worktree restore tests * fix(web-shell): Capture reading position before history pagination * test(cli): align voice route metadata expectations * fix(web-shell): preserve model configuration across updates * fix(web-shell): preserve reconnects and synchronize model writes * fix(web-shell): reconcile model settings and runtime feedback * test(core): configure image model for code mode scheduler * fix(web-shell): validate surviving model roles and preset migrations |
||
|
|
00d86315c8 |
feat(goal): rerun a stalled checkpoint in batches, smaller after each stall (#11690)
* feat(goal): rerun a stalled checkpoint in batches, smaller after each stall
A checkpoint that stalls on an overflowing window leaves the cursor where
it was, so the next attempt rebuilt the same window and sent the verifier
the identical request at temperature 0, failing the same way until the
stall breaker stopped the Goal.
The first check on a window still sends it whole. A check that starts with
a stall streak splits the evidence into consecutive batches (24 records
after one stall, 12 after two) and calls the verifier once per batch:
- each batch's claims are carried into the next as previousClaims, and
only the last batch's checkpoint is kept and moves the cursor;
- intermediate batches use their own claim id segment (<uuid>:b<k>:<n>);
- a failing batch ends the check through the existing arms, and the
recorded failure names the batch ("batch 2/5: ...") inside the cap;
- a restore replay batches by its restored streak and still spends no
stall.
Docs describe the batching and its worst-case duration, and the
goalCheckpointTimeoutSeconds description says the ceiling is per call.
* fix(goal): split only an overflowing live checkpoint, not a room window or a replay
A window with room settles a failed check as inconclusive and a restore
replay is exempt from the stall breaker, so neither can spend a stall and
splitting them bought nothing: it only added verifier calls and lossy
per-batch recompression, and for a replay held up session activation,
which waits for the replay to finish. Both now go out in one call as they
did before; only a live check on an overflowing window after a stall is
split.
|
||
|
|
ba3a51e71d |
feat(workflows): add effort and disallowedTools to agent() options (#11691)
* feat(workflows): add effort and disallowedTools to agent() options
A workflow script can now set the reasoning effort for one agent and take
tools away from it. Every agent previously ran at the session's effort with
the full tool set minus a fixed floor, so a script paid high-effort cost on
cheap stages or ran its judges at low effort, and a read-only stage could
still edit files.
agent({ effort }) accepts low, medium, high, xhigh or max with the aliases
/effort accepts, and omitting it inherits the session tier. The tier goes
onto the agent's own copy of the content-generator config, never the
session's, limited to the tiers /effort offers for that model: a tier the
model does not offer becomes the next stronger one it does, and a model
offering none keeps the inherited tier. It is written by the session
setter's logic, now one shared helper, and providers still clamp it per
request. An effort-only dispatch gets its own runtime view for that reason.
agent({ disallowedTools }) is unioned with the workflow floor and the agent
type's own denies, so it only narrows. Display names resolve through the
spawn's normalizer. A schema agent that denies structured_output is refused
before it spawns.
Both options join the resume key. The sandbox validates and normalizes them
on the revived copy that crosses to the host (alias to tier, deny list
sorted and de-duplicated), so equivalent spellings replay and different
requests run live. The workflow-authoring skill documents both.
Part of #11013
* fix(workflows): make per-agent effort and deny lists hold
Address the first review round of the agent() effort and
disallowedTools options.
Effort: an explicit tier now drops a thinking budget the agent's copy
inherited, never re-enables thinking the session turned off (also across
a provider switch), finds the declared tiers behind a gateway base URL,
and falls back to the provider's built-in tier table when the registry
declares none. An effort-only dispatch builds its own generator only
when the tier can land, refuses to open an interactive login for it, and
runs on the session's generator if that build fails. The CLI's /effort
tier rule now delegates to the same core helper.
Deny lists: an entry that matches no tool is refused instead of silently
denying nothing, built-in display names become tool names before the
resume key is derived, and a schema agent is refused when any deny in
the union reaching the spawn, including its agent type's own, names
structured_output. The fast path is driven by the option list the resume
key projects.
Sandbox: the phase is recorded only after the option gates pass, and an
echoed effort value is stripped of control characters. The review
harness mirror accepts the two options, and the authoring skill states
where effort does not reach.
Part of #11013
|
||
|
|
93c0d6d20d |
fix(core): guard internal git calls against configured helper programs (#11669)
* fix(core): stop a repository's own git config from running programs during automatic git calls A repository can name a program in its own .git/config and have git run it on the next index refresh. The git calls the agent makes on its own initiative reached that: startup context, the daemon's working-tree status and diffs, the ignore probe, the grep tool, the team-memory sync, the stale-worktree sweep, and the two worktree dialogs. Guard at the chokepoints (the simple-git factory, gitDiff's runGit, the team-memory and orphan-cleanup spawn points) with -c core.fsmonitor=, and pass --no-ext-diff --no-textconv on the diff calls for the two program-valued keys that config cannot empty. * test(core): mock execFileSync in chatRecordingService tests getGitBranch now calls execFileSync instead of execSync, so the tests' node:child_process mock must stub execFileSync to keep gitBranch resolving to 'main'. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmtx6sqi9yb * fix: close post-index-change on guarded status probes and pin remaining guards Address review feedback on the fsmonitor guard: - Add --no-optional-locks to the three guarded `status --porcelain` probes (isDirtyTree, checkoutHasWork, WorktreeExitDialog) so they no longer write the index and run a tree-shipped `post-index-change` hook. - Migrate chatRecordingService.autoTitle.test.ts to execFileSync and pin gitBranch so a stale mock target surfaces as undefined. - Add a clean-tree `false` assertion to the hasTrackedChanges canary so it distinguishes a real status read from the fail-closed `true`. - Pin the checkoutRef guard with a core.fsmonitor canary case, and add a post-index-change canary + control for isDirtyTree. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxgstgfyr * fix(core): close remaining git config execution paths * fix(core): carry --no-optional-locks in argv on the worktree dirty probes hasWorktreeChanges and countWorktreeChanges passed GIT_OPTIONAL_LOCKS=0 through simple-git's two-arg .env(), which replaces the child environment outright — dropping PATH/HOME and hiding the global core.excludesFile and safe.directory. Run the status probe through execFile with `-c core.fsmonitor= --no-optional-locks status --porcelain` instead, so the guard stays in argv and the environment is inherited. Add a canary case proving the probes read the global excludesFile (red if the child environment is clobbered). Also correct the load-simple-git loader comment: a leading literal global flag such as --no-optional-locks is safe because it is a fixed token, not a caller-supplied entry. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmty28f6zzp * fix(core): suppress post-index-change on the team-memory status probe and roll back on rejected push The session-start status probe in syncTeamMemory lacked --no-optional-locks, so when the team path is clean it was the flow's only index-writing call and a tree-shipped .git/hooks/post-index-change ran on it. Add the flag (before the status subcommand, so git does not answer rc=129). The --no-gpg-sign commit made a signing repo's push deterministically fail; the rejected commit then stranded on the branch and tripped wasAheadBeforeSync, permanently disabling push. Undo our own commit (soft reset + pathspec-limited unstage) when the push is rejected so the branch returns to its pre-sync HEAD. Add real-git regressions for both: a clean-path post-index-change canary, and a pre-receive-rejecting remote asserting HEAD is restored and a second sync does not return local-ahead. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmty28f6zzp * fix(core): close remaining git config exec and rollback gaps - Team-memory push-failure rollback now rewinds by SHA: it records the commit it created and only resets when HEAD still points at it and the remote does not already have it, so a concurrent writer's commit is no longer dropped by a blind HEAD~1. - The two worktree dirty probes pin `--untracked-files=all` so an ambient `status.showUntrackedFiles=no` (or git's default `normal` collapsing an untracked directory) cannot read a worktree clean, and set a 10 MiB maxBuffer so a large tree is not misreported as a git failure. - Widen NO_EXEC_CONFIG / NO_EXEC_CONFIG_SETTINGS to also carry `log.showSignature=false`, closing the `log.showSignature` -> `gpg.program` vector at the two remaining `git log` paths (gitDiff runGit and the simple-git loader). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtyf3dx109 * fix(core,cli): address team-memory sync review feedback - Qualify the clean-path status probe comment: pull --ff-only also writes the index on the upstream path. - Scrub ambient git config in both canary tests so a system/global core.hooksPath cannot silently disable the planted hook. - Assert the rollback leaves the index empty and working tree intact, and that a second sync commits a new note and re-reports push-failed. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtyjdph70g --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
632773e018 |
refactor(core): apply one matcher rule to every hook event (#11619)
* refactor(core): apply one matcher rule to every hook event Hook matchers meant different things per event and per registration path. Notification, PreCompact, PostCompact and StopFailure accepted only a single exact value, so `permission_prompt|idle_prompt` never matched, and session-scoped hooks, including skill frontmatter hooks, anchored their regex while settings hooks did not. One shared matchesHookPattern now serves the planner and the session hooks manager: an empty matcher, `*` or `.*` matches everything; the matcher or any entry of a `|` list matches exactly (tool events also compare aliases exactly); otherwise it is an unanchored regex on the subject. Existing tool, subagent, command, session and file-path matcher behaviour is unchanged. Part of #11610 * fix(core): decide pipe-list matchers entry by entry Review follow-up. A `|` list compared its entries exactly and then fell back to the whole pattern as a regex, so `Write|*` became an invalid regex that matched nothing, and a stray pipe such as `write_file|edit|` became a regex that matched everything. Lists are now decided entry by entry with the full rule; empty entries are ignored and never widen the list, and a group spanning the pipe such as `a(b|c)` still falls back to one regex. The planner no longer keeps its own copy of the match-all rule. The hooks and skills guides now say that skill matchers are unanchored, that an omitted skill matcher matches every tool, how to anchor runtime ids and exclusions, and that aliases only match exactly. Part of #11610 * fix(core): decide pipe-list matcher entries without compiling them A pipe-list entry now matches only as `*`, `.*`, or an exact subject or alias. Entries are no longer compiled one by one, so an escaped pipe such as `notes\|todo\.md` keeps its whole-pattern meaning instead of letting the `todo\.md` fragment match any path, and a group that spans the pipe no longer logs "Invalid regex" warnings for its unbalanced fragments. The whole-pattern fall-through is compiled from the non-empty entries, so a stray `|` is dropped rather than disabling the fall-through: `read_(file|edit)|` matches `read_file`, while a list of only `|` still matches nothing and a trailing `|` never becomes a match-all. Pin that a skill hook entry without `matcher:` matches every tool. * fix(core): trim pipe-list matcher entries and split only on unescaped pipes Trim each list entry at the split so the exact comparison and the rebuilt regular expression see the same entries: `read_.* | edit` now matches `read_file` instead of compiling alternatives that demand literal spaces. Split only on a `|` that is not preceded by a backslash, so an escaped pipe never manufactures an empty entry or a dangling escape: `notes\|` matches `x/notes|y`, `a\||b` matches `b`, and `foo\|bar` no longer matches `bar`. Scope the documented stray-pipe guarantee to lists that do not start with `^` or `(`; those matchers are compiled as written. * fix(core): split matcher lists only at top-level pipes The list reading of a matcher split on every pipe not preceded by a backslash, so it cut through character classes and groups and then trimmed spaces that belonged to the expression: `foo[ |]bar` was rebuilt as `foo[|]bar` and stopped matching `foo bar`, and `a(b | c)` lost its spaces the same way. A single preceding backslash also hid a real separator after an escaped backslash, so `C:\\temp\\|D:\\data\\|` kept its trailing empty alternative and matched everything. Split with a scan that reads backslashes in pairs and ignores pipes inside `[...]` and groups, so only top-level entries are trimmed, compared and rebuilt, and `read_.* | edit` keeps reading as `read_.*|edit`. Tests pin each shape and the `^` and `(` guards, and the hooks guide says which pipes separate entries. |
||
|
|
54aa66834b |
fix(core): fire active-todo reminders at delegation boundaries and user turns (#10963)
* fix(core): fire active-todo reminders at delegation boundaries and user turns A session that delegates to foreground subagents earns one parent tool turn per tens of minutes of real work, so the 3-turn budget of the active-todo reminder never comes due and the persisted plan freezes (#10953). Any ordinary user turn additionally discards a registered reminder, so the very message asking about progress destroys the plan context. - Force the reminder due on a tool-result batch that carries a top-level Agent tool result, in both the TUI (client.ts) and ACP (Session.ts) paths, putting the nudge exactly where delegated progress arrives. - Continue the previous work chain on an ordinary user turn while a reminder is still registered — a registered reminder implies unfinished items, since todo_write deletes it on plan completion. This mirrors the existing retry continuation. The observational contract of todo_id is unchanged. Time-aware cadence (direction c) and the delayed tool-response recording (direction e) are separate concerns and intentionally not included. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-issue-patrol/jmtlu4sumgg * docs(design): sync active-todo-context with the two rules this PR changes The doc still promised that an ordinary new prompt clears stale Todo state, and described the turn cadence as the reminder's only delivery path. This PR changes both: an ordinary turn continues the chain while a reminder is registered, and a tool-result batch carrying a top-level Agent result forces the reminder due because a delegated run earns the parent a single turn. Record the accepted cost (an abandoned plan resurfaces until a later write completes or clears it) and that the ACP todo-stop-guard lineage reset stays keyed to the retry/continue flag, so carrying a plan never widens guard trust. * fix(core): clear stale active-todo reminders on rewind and preserve automatic-turn owners History rewind / restore / setHistory discard the timeline the active-todo reminder describes, but the reminder and its work-chain owner survived, so the next turn continued work that was rewound away. Clear the reminder state where history is discarded (LlmClient.truncateHistory/setHistory, ACP rewindToTurn/restoreHistory) via a new Config.clearActiveTodoReminders(). Continuing a chain also dropped still-live related automatic turns' owner mappings (owners.clear()), so a plan completed by an in-flight automatic turn kept its reminder and kept reporting unfinished work. Re-point instead of clearing, dropping only entries whose mapped owner is discarded. Extend the continuation tests to pin the cleared-reminder branch, parameterize the ACP force test over the legacy `task` alias, and document the mid-turn `force` case on takeActiveTodoReminder. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtwvd7nnxt * fix(core): clear stale active-todo reminder when a non-owner completes the shared plan Two changes close the R4-1 regression and bound the R4-2 retention: - setActiveTodoReminder(undefined) now clears session-wide. todo_write passes undefined only when the list has no unfinished items, and the plan file is session-scoped, so a completing cron/notification/subagent/Goal turn can no longer strand the foreground reminder and make the next ordinary turn re-chain a finished plan (#10953). - startActiveTodoWorkChain drops the superseded foreground head before re-pointing, so the owners map stays bounded instead of growing one entry per continuation. Still-live related automatic-turn mappings are preserved. Forward setActiveTodoReminder through buildSubagentContextOverride so a subagent's completing todo_write reaches the parent's session-scoped reminder instead of its isolated wrapper map. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxcihx2yk * docs: document the history-discard reminder clear in active-todo-context The same PR adds a fourth reminder-clear condition (rewind / restore / setHistory / truncateHistory) across both frontends, but the design doc still enumerated only three clear conditions and had no Verification bullet for the discard rule. Add the condition and the matching bullet so the enumeration stays exhaustive. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxcihx2yk * fix(core): remove dead active-todo reminder write-through in subagent context todo_write is the only production caller of Config.setActiveTodoReminder and is excluded from both subagents and teammates (EXCLUDED_TOOLS_FOR_SUBAGENTS / EXCLUDED_TOOLS_FOR_TEAMMATES), so the write-through shim installed on the derived subagent context can never run; the single-layer test beside it pinned a layering production never builds. Todo state is parent-owned by design. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxl34z1yy * test(core): pin retention loop with a non-completing automatic-turn write The completing-write sibling clears session-wide regardless of ownership, so it cannot discriminate the owners-map retention loop against owners.clear(); a non-completing write only lands on the shared owner when the loop keeps the prompt-auto -> prompt-user mapping alive. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxl34z1yy * style(core): format config.test.ts * fix(core): gate active-todo carry on the last plan-writer owner An isolated cron/notification/Goal turn that rewrites the session-scoped plan file under its own owner left the foreground's stale snapshot in place, and the continuation guard re-delivered it every turn. Record the owner of the last real todo_write and require owner equality in the continuation guard so only the plan's current owner carries the reminder. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmty6iqs0zv * fix(cli): gate ACP todo continuation on the plan-writer owner Port the owner-equality gate that landed in core (client.ts:3501-3510) to the ACP continuation guard. The ACP guard carried on reminder presence alone, so an isolated cron/notification rewrite of the session plan under a foreign owner left the stale foreground snapshot re-delivered on every turn and at every Agent-result boundary, diverging from core on the same state. Extend the three ACP Config mocks and pin the foreign-owner no-carry case with an ACP twin of the core test. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtyf3dx109 * fix(cli): reserve ACP turn-start todo injection for machine continuations The ACP prompt path forced takeActiveTodoReminder(promptId, true) for every turn that reaches its straight-line body. Before the carry rule that was inert on an ordinary turn (the fresh-chain branch cleared the reminder first); with the carry rule it splices up to 800 chars of model-authored plan text ahead of the user's own text on every ordinary turn, permanently recorded in history, while core injects at turn start only for Retry | Cron | Notification | Teammate. Gate the force to the turns that continue a chain as machine work (retry, resumed interrupted turn, runtime goal continuation), keeping the restore-turn exclusion and the retry force, and align the design doc plus both test files: the reminder now comes back from the mock only under force, so the ordinary turn's request assertion fails if the gate is reverted. --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
3ba01990e1 |
fix(core): keep the mirrored reasoning field off Fireworks requests (#11701)
Fireworks' OpenAI-compatible endpoint accepts the non-standard reasoning_content field on input but rejects the additional reasoning field with HTTP 400 (Extra inputs are not permitted, field: 'messages[N].reasoning'). The default provider mirrors reasoning_content into reasoning for any model whose name contains 'qwen3' — a family match that does not establish the endpoint accepts the extra field — so Qwen3 thinking models on Fireworks fail every multi-turn continuation that replays a thinking turn, including tool-call continuation (issue #11657). Add a Fireworks provider, detected by api.fireworks.ai hostname in determineProvider (following the Cerebras precedent from #11049), that undoes the mirror at the outbound request boundary. Only the exact mirror copy (reasoning === reasoning_content) is dropped; a distinct explicit reasoning field and reasoning_content itself — which Fireworks documents for reasoning replay — are preserved. Conversation history is never mutated and non-Fireworks endpoints keep the existing mirroring. |
||
|
|
d50dc84e36 |
fix(goal): remove backtracking fence unwrap from judge replies (#11693)
* fix(goal): replace backtracking stripCodeFence with linear stripMarkdownFence The judge's fence unwrap used /^```(?:json)?\s*([\s\S]*?)\s*```$/i, whose three overlapping quantifiers backtrack super-linearly on a reply that opens a fence and then degenerates without closing it (measured ~49 s for 5 000 spaces), blocking the event loop past the judge's own 25 s timeout. Reuse the index-scan stripMarkdownFence already in the checkpoint verifier instead, and drop the judge's copy. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-issue-patrol/jmtxx8drhzh * fix(goal): parse judge replies whose payload is glued to the fence line stripMarkdownFence's multi-line branch drops the opening fence line, so a judge reply that puts its JSON payload on the opening line (with the closing fence on the next line) unwraps to brace-less text and parseJudgeReply returns an error verdict. Fall back to the raw reply when the unwrap produced no '{', so the tolerant index-scan still finds the payload. Also pin the judge's fence unwrap with two new regression tests and tighten the existing degenerate-fence guard: a per-test timeout (the old backtracking regex took ~49 s on this shape, past the runner's raised 60 s ceiling) and a stage assertion confirming the reply actually reached parseJudgeReply. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmty28f6zzp * style(core): format goalJudge.test.ts with prettier Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmty53ay9zt * fix(goal): drop dead fence unwrap from judge reply parsing parseJudgeReply's tolerant first-`{`-to-last-`}` scan already finds the payload for every fence shape a judge produces: markdown fences (backticks, tildes) and their info strings carry no braces, so the scan reaches the payload without a separate unwrap. The stripMarkdownFence import added to replace the backtracking stripCodeFence was therefore dead weight, and its `includes('{')` fallback latched onto a decoy brace when the payload was glued to the opening fence line and quoted a braced excerpt, rejecting a verdict the deleted stripCodeFence parsed. Remove the import and the fallback; the remaining scan is already index-based and linear, preserving the ReDoS fix. Add a witness test for the glued pretty-printed reply quoting a braced excerpt. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtyat2ck02 * fix(goal): keep checkpoint fence parsing private Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
b5c7635ff9 |
fix(core): compute cron next-fire time with epoch arithmetic across DST fall-back (#11723)
nextFireTime advanced its minute-by-minute scan with local Date setters (setSeconds / setMinutes). ECMA-262 resolves an ambiguous fall-back wall-clock to its earlier occurrence, so an anchor inside the repeated autumn hour was re-projected up to 59 minutes into the past, breaking the strictly-after contract, and a forward scan skipped the entire second occurrence of the hour. Traverse elapsed time instead: initialise the candidate with epoch arithmetic (floor(after/60000)*60000 + 60000) and advance with candidate.setTime(candidate.getTime() + 60000), while continuing to match cron fields with local getters. Cron syntax, jitter policy, and other scheduler behavior are unchanged. Fixes the root cause reported in #11720. Patrol-Run: qwen-issue-patrol/jmtygitr20b Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
1331c7af85 |
fix(core): reap surviving hook process trees on Windows (#11623)
* fix(core): reap surviving hook process trees on Windows terminateSurvivingHookProcessGroup was an explicit no-op on win32, so when a parent-exit-surviving hook (MessageDisplay / StopFailure / SessionDelete) was cancelled, nothing reaped the shell the supervisor had started. Those hooks spawn a detached supervisor plus a shell child and unref() it, so the parent's own tree kill on the supervisor is not a reliable substitute: the supervisor may already have exited, which reparents the shell out of its tree, or its taskkill may fail. The result is the leftover cmd.exe processes reported in #11303. Implement the branch with taskkill /f /t, extracted from terminateWindowsHookProcessTree as a shared pid-only helper. Guarded by a process.kill(pid, 0) liveness probe first. Windows has no process group to signal, so an unguarded taskkill against a pid that has already exited could land on a recycled pid belonging to an unrelated application — the #6067 collateral-kill failure mode. The POSIX branch gets that safety for free by signalling the negated pid. * fix(core): reuse shared pid liveness probe in Windows surviving-hook reap * fix(core): warn when a surviving-hook liveness probe fails unexpectedly An unexpected process.kill(pid, 0) errno (ENOMEM, EMFILE, libuv's UV_UNKNOWN catch-all, ...) read as "dead" through isPidAlive, so the Windows surviving-hook reap skipped without a trace even though the hook's cmd.exe tree may still be running. Classify the probe at the call site into alive/gone/unknown — the skip decision is unchanged — and warn on the unknown case. Also pin that the win32 branch never falls through to the POSIX process-group path with a negative-pid assertion, and record the warn in the design docs. * fix(core): size code-mode host startup grace for slow machines A code-mode execution's wall clock is timeoutMs + CODE_MODE_HOST_STARTUP_GRACE_MS, measured from host spawn. A 5s grace is below the observed cold boot of the host process (node + tsx + QuickJS WASM init, ~4s unloaded and worse under parallel load) on slow machines, so executions with a small timeoutMs were killed during boot with a spurious "timed out" error. Raise the grace to 30s, matching the guest timeout scale; fast machines see no change since the deadline only binds when startup is slow. Also lift two scheduler tests' intermediate vi.waitFor budgets (4s) and one exec-context test's (1s default) to 30s: nested tool dispatch cannot be observed before the host has booted, so deadlines below the boot time failed deterministically on slow runners. Assertions are unchanged; each test's own timeout still caps it. * fix(core): sharpen surviving-hook probe diagnostics and code-mode timeout message - Carry the probe errno in the surviving-hook skip warning and share the tri-state liveness classification with the SIGKILL-fallback re-probe, which now warns instead of skipping silently on an unexpected errno. - Report the wall budget actually enforced (guest budget + host startup grace) when the code-mode host never frames a response. - Raise the enclosing per-test caps above the 30s waitFor budgets so a genuine stall still surfaces the assertion diff, and state in the design docs that the probe warnings live in the debug log (--debug). --------- Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
85509c749d |
fix(core): redact error text in usage-statistics telemetry sink (#11649)
* fix(core): redact error text in usage-statistics telemetry sink * fix(core): close telemetry error-redaction gaps Close the highest-severity review findings on the redaction table: - Add `error` to ERROR_TEXT_PROPERTY_KEYS so hook failure text written to properties['error'] is redacted at the enqueue choke point (R1-1). - Skip any auth scheme (Basic/token/Digest/…) before the value, not only Bearer, so `Authorization: Basic <creds>` no longer leaks (R1-2). - Add `key` to the env key alternation so *_API_KEY / *_ACCESS_KEY* are redacted (R1-6). - Run redaction on the raw text before stripAnsiAndControl so the whitespace-delimited patterns still see their newline/tab delimiters (R1-7). - Share one secret-value pattern that accepts a quote-opened value (R1-3) and let the flag name contain the secret keyword anywhere (R1-4). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtwznj86xz * fix(core): harden telemetry error redaction patterns Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtx1sp0ry2 * fix(core): run prettier on qwen-logger redaction test Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmtx4nkoby7 * fix(core): close telemetry error-redaction quote/continuation/ANSI gaps Harden the usage-statistics error-text redaction pass against the review findings on this branch: - Normalise shell line continuations (`\` + newline + leading whitespace) ahead of every pass, so a credential split across a continuation (including a URL credential) can't survive as a glued cleartext run. - Make SECRET_VALUE quote-aware (a quoted run matches to its closing quote), so interior quotes and quoted values containing spaces redact whole instead of leaking a suffix after the marker. - Give the env pattern the same quoted alternative behind its floor. - Skip an optional leading backslash in each separator so a shell-escaped nested quote (`Authorization: Bearer \"secret\"`) still redacts. - Strip ANSI/C0 per line (after widening tabs to spaces) before the patterns run, so an escape or control character can't hide or reassemble a credential the pattern pass never saw. Fix the line-continuation test to assert the secret is absent rather than merely that no marker is emitted, and add regression cases for interior quotes, quoted values with spaces, ANSI/C0, and continuations. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxennouyn * fix(core): bound secret-name quantifiers and unify flag/env value floors - Bound the non-secret name chars around the secret keyword in SECRET_FLAG_PATTERN and ENV_SECRET_PATTERN (previously unbounded `*`, now `{0,64}`) to prevent quadratic backtracking on adversarial error text that runs through the synchronous telemetry redaction pass. - Give SECRET_FLAG_PATTERN the same 10-char value floor as ENV_SECRET_PATTERN so `--max-tokens 8192` and `max_tokens=8192` are handled consistently (both pass through untouched). - Align the ENV_SECRET_PATTERN docblock examples with the actual 10-char floor (the old `ghs_xxx` / `sk_xxx` examples were shorter than the floor and were never redacted). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtxgstgfyr * fix(core): drop raw error text from usage telemetry * fix(core): drop degenerate logPrompts gate on hook error text `redactEventErrorText` already replaces every string under `error_message` / `error_excerpt` / top-level `message` with a fixed marker at the enqueue boundary, so `logHookCallEvent`'s `getTelemetryLogPromptsEnabled()` gate on `properties['error']` could only ever emit the constant `'***REDACTED***'` — a consent flag that no longer gates any content. Hook failure is already signalled by `success` / `exit_code`, so the raw error text is dropped fail-closed instead, and the now-dead `'error'` entry is removed from `ERROR_TEXT_PROPERTY_KEYS`. The failed-hook test now drives a config with telemetry log prompts enabled and asserts `properties` carries no `error` key, which goes red when the gated assignment is restored. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmtycy85205 --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
94449f9dfb |
docs(daemon): describe the prompt rate-limit and queue-full responses separately (#11733)
* docs(daemon): split prompt queue-full from the rate-limit failure * test(cli): floor the guide protocol-anchor scan |