Repository navigation
Tags
Tags
🤖 fix: put the phone header beside the status ring on the unfolded iP… …hone Duo in portrait (#8164) ## Summary On the unfolded iPhone Duo held in portrait, the phone header now sits in the top strip beside the iOS status ring, where it used to start 82 pt down under an empty bar. ## Background On that display iOS reports its 82 pt portrait status bar as the top safe-area inset, but it draws only a clock and a status ring in the top right corner. Using the full inset left an empty bar across the whole width. Native apps put their content beside the ring. ## Implementation - New `--safe-area-top` CSS variable (defaults to `env(safe-area-inset-top)`) used by the app shell, the phone header, the sidebar drawer and the left sidebar toggle bar, plus the static first-run view. - A media query that matches only the Duo's inner display in portrait (`device-width: 466px`, `device-height: 678px`, `min-width: 600px`; the Duo reports the same screen on both displays and only the inner one is wider than 600) lowers it to `min(inset, 25px)` and reserves `--ios-status-corner` on the header's right so its buttons stop left of the ring. - Dialogs and sheets keep the full inset. `min()` leaves a web clip that reports no top inset unchanged. ## Validation - Device Hub simulator, unfolded inner display rotated to portrait, black-translucent web clip: the header row moves from y 82 pt to y 25 pt, rightmost button ends at x 533 pt, left of the ring at x 549 pt. - Same simulator: the cover display and the inner display in landscape render as before. - `.storybook/test-runner.ts` (`assertDuoInnerPortraitTopStrip`, Chromium) emulates the 466x678 screen, a 669x951 viewport and an 82 pt top inset through CDP. It asserts the header top, the shell's top padding, that the header's controls end left of the ring, that a missing inset stays 0, and that another screen with the same viewport keeps the full inset. It fails with `main`'s CSS. ## Screenshots Device Hub simulator, unfolded inner display in portrait, dark theme (top of the screen). **Before (`main`):** the header starts 82 pt down, under a bar that holds only the clock and ring.  **After:** the header sits at the top, beside the ring; its buttons stop left of it.  ## Risks Low. The media query matches only the Duo's inner display in portrait; every other device keeps `env(safe-area-inset-top)`. Known limitations, not changed here: - Home Screen icons added with the older (non-translucent) status-bar style get no top inset, so they see no change until the icon is re-added. - With the translucent style, iOS starts the web app at 869 of 951 pt, leaving an 82 pt empty band at the bottom until the page grows. `main` has the same band; a follow-up should fix it for this layout. > Xum opened this PR on Mike's behalf. --- _Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high` • Cost: `$9.01`_ <!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high costs=9.01 -->
🤖 feat(tasks): let task_remove force-remove a sub-agent with unsaved … …work (#8153) ## Why The goal, in the maintainer's words: "allow xum to force remove sub-agents and workspaces with a flag. I'm not interested in cleaning up after you." Today `task_remove` refuses to remove a sub-agent that holds unsaved work. It then tells the agent that the user must remove the child by hand from the command palette. An agent that owns 13 finished children cannot clean up after itself, and the user must do it. This PR gives `task_remove` a `force` flag. Workspace archive is a separate PR (see Scope). ## What changed - `task_remove` takes `force: boolean | null` (`src/common/utils/tools/toolDefinitions.ts`). Only `force: true` forces. `false`, `null` and an omitted flag keep the refusal. - `TaskService` models the choice as one typed value: `lossyWorkPolicy: "refuse" | "force"` (`src/node/services/taskService.ts`). A model-driven removal always sets it. The palette and automatic cleanup still omit it. - Under `"force"`, the removal skips only the unsaved-work refusal. Under both policies these refusals stay: a running background bash process, an external editor or native terminal the user opened, a pending patch artifact, an active sub-agent, and remaining descendants. - A forced removal still computes the same description of the lost work, under the same locks, right before the removal. When the removal succeeds, the `removed` result carries `deletedWork` (what was deleted, past tense, plus the checkout path and the branch) and `paths` (the file list). A checkout that could not be checked is removed anyway, and `deletedWork` says it could not be checked. - The branch in `deletedWork` is the workspace name. I verified that each runtime forks the sub-agent checkout with `newWorkspaceName: plan.workspaceName` and that removal deletes the branch by that name. - The refusal text now tells the agent that `force: true` exists and when to use it: the user asked for cleanup, or the work is provably saved (pushed, merged, or in an applied patch). It keeps "you must not delete that work yourself to get around it". It no longer sends the user to the command palette. - A forced removal that fails after it deleted the checkout (for example when the config write fails) returns the report on the error result too, worded "may have deleted". A retry finds no unsaved work, so the failure is the only place to report it. - When the runtime leaves part of the checkout behind (a forced removal then succeeds with warnings), the report says some of the work may remain and lists what was left. It never claims a full deletion that did not happen. - `WorkspaceService` now returns a warning when a forced removal could not delete the checkout, even if the runtime named no leftover (single runtime and multi-project). Before, it logged the failure, removed the row and returned nothing, so no caller could tell that the checkout might still exist. The palette and parent-delete paths still succeed as before and only gain that warning. - The `task_remove` card shows `deletedWork` and `paths` on removed and failed rows. - Generated docs (`docs/hooks/tools.mdx`, the docs skill snapshot) follow the schema. ## Scope - This PR covers sub-agents (`task_remove`) only. `task_workspace_lifecycle` (workspace archive) is the sibling PR, and its behavior is unchanged here. The one shared piece is a type: `TaskService` returns the shared `WorkspaceLifecycleResult`, so this PR adds the optional `deletedWork` field to `TaskWorkspaceLifecycleBaseResultSchema`. The archive tool does not set it. - No partial fix for "commits reachable from a remote ref or patch-equivalent to origin/main do not count" (decision D5 below). - No new IPC field and no change to the palette (D2). ## Tradeoffs - The guard against misuse is a prompt rule, not enforcement. The tool description and the refusal text say to set `force` only when the user asked for cleanup or the work is provably saved. The maintainer accepted this. - `force` is all or nothing for the unsaved-work check. It does not take a list of paths to keep. - A forced removal that fails before it deletes anything (mutation gate, a new attempt, a stream stop timeout) still says "may have deleted". Those failures share one error result with the ones that fail after the deletion, and telling them apart would mean matching error text. The wording is hedged and nothing is lost, so I left it. - `deletedWork` is one string, not several fields. The model and the card both read prose, and one field keeps the result small. - I renamed the private characterization probe `refuseLossySubagentRemoval` to `settleLossySubagentRemoval` in `taskService.removeInactiveDescendant.test.ts`. The step now returns a refusal or a forced report, so the old name was wrong. ## Blast Radius - Anyone who can call `task_remove` on their own descendants can now delete their unsaved work. Scope checks are unchanged, so an agent cannot force-remove another tree's children or a user's workspace. - The default call behaves as before, except for the refusal wording. - Automatic cleanup, the command palette, and the parent-deletion cascade do not change. ## Decisions (root, 2026-11-12) - D1. Flag shape: `force: z.boolean().nullish()`. The handler tests `args.force === true`. - D2. Agent tools only. The palette and IPC removal already delete without this check, after the user confirms. - D3 (task_remove half). Force skips only the unsaved-work refusal: uncommitted and untracked files, commits without a ready patch artifact, commits on other branches or the stash, and "cannot verify" failures. It keeps the checks for someone still working in the checkout. - D4. A forced removal reports what it deleted: the summary, the checkout path, the branch, and `paths`. The card shows it. - D5. No partial fix. It does not meet the request (a flag), and squash merges are not patch-equivalent per commit, so the flag covers those cases. - Threat model, short form: - Who can set the flag: any agent that owns the target sub-agent. - What can be lost: uncommitted and untracked files, commits not captured by a patch artifact, commits on other local branches or the stash in self-owned repositories. - Audit: every forced deletion is reported in the tool result and on the card. - Not changed: what removal deletes, scope checks, and the user-mediated paths. ## Question for @ThomasK33 `force` skips only the unsaved-work refusal. A running background bash process and an editor you opened still refuse. Do you want `force` to skip those too? - Option A (current): keep those refusals. The agent can stop its own background bash with `task_stop`. An editor you opened has no agent-side fix, so those children still need you. - Option B: skip them too. The agent never needs you, but it can delete files while a process or editor still writes to them. - My recommendation is Option A, because those refusals protect work that is still changing, not saved work. ## Screenshots Dark theme, from the `TaskRemoveForcedNarrow` story, cropped to the card. 375px:  Wider card:  ## Verification - `taskService*.test.ts` plus the cascade and multi-project removal suites: 1810 pass, 0 fail (58 files). All `workspaceService*.test.ts`: 1248 pass, 0 fail (52 files). Tool, common, browser Tools, `workspaceService.pendingRemoval` and `subagentRemovalWorkCheck` suites: 2570 pass, 0 fail (136 files). - New tests: a real git checkout with an uncommitted file and an uncaptured commit (refuse refuses, force removes and reports the path, the commit count, the checkout and the branch), an unverifiable checkout, a clean checkout, a removal that leaves files behind, a removal that fails, a runtime that fails to delete the checkout (real worktrees: parent cascade warning and the task_remove report), the same for a multi-project workspace, removal warnings, both policies keep the live-writer guards, force keeps the scope, descendant, active-child and pending-patch refusals, the tool passes `force` only for `true`, and the card shows `deletedWork`. - Hand mutants, each turned red by the named test: force skipped, report dropped, live-writer guard dropped (background bash and editor), force skips the scope, descendant, active or pending-patch refusal, refusal dropped, failed-removal report dropped or worded "Deleted", leftover caveat dropped or always added, tool ignores `force`, tool always forces, tool drops `deletedWork`, card hides `deletedWork`. - `make typecheck`, oxlint, ESLint on the 11 changed files, Prettier, docs sync check, `make crap-check`, `make dry-check`: pass. - `make mutate-check` for `taskService.ts` runs only the 10 nearest test files, and they do not include `taskService.lifecycle.test.ts`. Its survivors are a test-selection artifact. I ran each survivor by hand against the lifecycle suite, and each is now killed. - Story play test `TaskRemoveForcedNarrow` passes (the story sets no Pixel snapshot, because the snapshot budget is at its cap). - Not run on my host: `hadolint` (not installed, and I changed no Dockerfile). --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh` • Cost: `$4.57`_ <!-- mux-attribution: model=anthropic:claude-sonnet-5-5 thinking=xhigh costs=4.57 -->
🤖 fix: send GLM 5.3 reasoning_effort on custom OpenAI-compatible prov… …iders (#8149) > Xum opened this PR for Mike. ## Summary GLM 5.3 on a custom OpenAI-compatible provider (for example local oMLX serving `GLM-5.3-Flash-oQ4e-mtp`) ran every turn at Max thinking, whatever the picker said. Xum now recognizes self-hosted GLM 5.3 ids and sends `reasoning_effort` (`low`, `high` or `max`) to those providers, and only for models it recognizes as GLM 5.3. ## Background - `buildProviderOptions` returned `{}` for every custom `openai-compatible` provider, so the SDK never wrote `reasoning_effort`. - GLM's chat template honors only `reasoning_effort: "low"` and `"high"`. Anything else (absent, `medium`, `xhigh`, `max`) renders "Reasoning Effort: Max". - `isGlm53Model` matched only `glm-5.3[-flash]` with a digit or `latest` suffix, so `GLM-5.3-Flash-oQ4e-mtp` got the default off/low/medium/high picker. ## Implementation - `isGlm53Model` (`src/common/types/thinking.ts`) also accepts the tags that self-hosted deployments append: a `-` or `:` followed by a token that carries a digit or names a packaging format (`mtp`, `mlx`, `gguf`, `awq`, `gptq`). Ids are already lowercased by `stripModelProviderPrefixes`. Z.ai's named variants (`FlashX`, `Air`, `Turbo`) are plain words, so they stay unmatched. - A new `openaiCompatibleGlm53Options` helper replaces the final `return {}` of `buildProviderOptions`. It returns `{ openaiCompatible: { reasoningEffort } }` when the raw prefix is a custom `openai-compatible` provider and the capability model (the id, or its `mappedToModel`) is GLM 5.3. Every OpenAI-compatible chat model reads the `openaiCompatible` key, whatever the custom provider's name, and the SDK writes it as `reasoning_effort`. Unrecognized models still get no field, since strict servers may reject unknown fields. - The effort mapping is shared with the Z.ai builder (`glm53ReasoningEffort`): `max` -> `max`, `high`/`xhigh` -> `high`, everything else -> `low`. ### Mapping decision The brief proposed `medium` -> `high` and `xhigh` -> `max`. The GLM thinking policy (`low`, `high`, `max`) clamps the level before `buildProviderOptions` runs (`medium` -> `low`, `xhigh` -> `high`), so those inputs never reach this code on a turn. I reused the Z.ai mapping so the same level behaves the same on Z.ai and on a self-hosted GLM. ## Validation - Unit tests run the real `createOpenAICompatible` model against a capturing `fetch` and assert the `reasoning_effort` in the request body: every level off..max, a `mappedToModel` alias, and no field for unrecognized ids (a Qwen id, `glm-5.3-flashx`). Policy tests cover the new id shapes and the named variants. Three of the new tests fail without the fix. - Wire check in a `dev-server-sandbox` from this branch, with its own `providers.jsonc` (`omlx` -> a logging spy -> oMLX): | Model | Picker level | Request body | | --- | --- | --- | | `omlx:GLM-5.3-Flash-oQ4e-mtp` | low | `reasoning_effort: "low"` | | `omlx:GLM-5.3-Flash-oQ4e-mtp` | high | `reasoning_effort: "high"` | | `omlx:GLM-5.3-Flash-oQ4e-mtp` | max | `reasoning_effort: "max"` | | `omlx:spy-unrecognized-model` | high | no `reasoning_effort` | On a small puzzle the completion tokens were 5 (low), 5 (high) and 45 (max). ## Risks Low. The new branch runs only for custom `openai-compatible` providers whose model resolves to GLM 5.3; every other provider path is unchanged. The broader id match also applies to the Z.ai builder and the thinking policy, so a Z.ai id with a deployment tag now gets the GLM levels. Notes: - The per-model default floor is `medium` for recognized GLM (as for `zai:glm-5.3-flash`), so the picker shows high and max until the user lowers the floor in Settings > Models. This is the existing GLM behavior. - Follow-up, not in this PR: the sidebar status generator (`propose_status`) sends no provider options for any provider, so GLM status summaries still run at Max. - Follow-up, not in this PR: a custom provider whose id shadows a built-in (`zai`, `openai`, `google`, ...) still gets that built-in's provider options, and the OpenAI-compatible SDK spreads them raw onto the request body for every model. Shadowed `zai` and `openai` already carry `reasoning_effort` that way; the others send fields such as `thinkingConfig` or `searchParameters` instead. That predates this PR and needs its own change to the format selection. --- _Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh` • Cost: `$7.47`_ <!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=xhigh costs=7.47 -->
🤖 release: v0.32.2 (#8119) > Xum opened this PR for Mike. ## Summary Version bump for the v0.32.2 patch release. It ships the new models on main since v0.32.1: Gemini 4 Carbon (#8108) and Claude Fable 5.2 and Mythos 5.2 as the `fable` and `mythos` models (#8078), with the Fable 5.2 refusal fallback (#8094) and Fast mode removed from Claude Opus 4.8 (#8099). It also carries every other change on main since v0.32.1 (297 commits). ## Implementation Bumped with `node ./scripts/set-package-version.js 0.32.2`, so the root `package.json` and the legacy `packages/mux-compat` forwarding package stay version-locked, as in #7686. `src/common/compat/productIdentity.test.ts` passes locally. After this PR merges, the GitHub Release `v0.32.2` will be published on the squash commit. That starts the desktop, Docker (`latest`), VS Code and npm (`latest`) pipelines. --- _Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high` • Cost: `$19.92`_ <!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high costs=19.92 -->
🤖 release: v0.32.1 (#7686) ## Summary Version bump for the v0.32.1 release. It ships the Electron 44.10.0 bump (#7684), whose bundled Node 24.22.1 has the Brotli decoder fix for CVE-2026-111497 ("Node2Shell"), and the guard for `xum run` and `xum workflow` (#7383). It also carries every change on main since v0.32.0. The GitHub Release notes will list the highlights. ## Implementation Bumped with `node ./scripts/set-package-version.js 0.32.1`, so the root `package.json` and the legacy `packages/mux-compat` forwarding package stay version-locked. A stable npm publish uses the `package.json` version as-is. `src/common/compat/productIdentity.test.ts` passes locally. After this PR merges, the GitHub Release `v0.32.1` will be published on the squash commit. That starts the desktop, Docker (`latest`), VS Code and npm (`latest`) pipelines.
🤖 fix: refuse Brotli responses while Electron's Node lacks the CVE-20… …26-111497 fix (#7374) ## Summary The desktop app's Electron 44.5.1 bundles Node 24.21.0. That Node is affected by CVE-2026-111497 ("Node2Shell"), and its `fetch` decodes `Content-Encoding: br` responses with the vulnerable Brotli decoder. Until Electron ships a 44.x with Node 24.22.1 or newer (#7358), the backend asks servers for gzip or deflate only and refuses a Brotli response before `fetch` decodes it. The guard turns itself off on Bun and on any Node with the fix, so it goes quiet once the Electron bump lands. ## Background #7359 closed the inbound path: the API server refuses compressed request bodies. Outbound fetches were still open: a malicious or compromised MCP server, OAuth endpoint or provider base URL could answer with crafted Brotli data. Electron has merged Node 24.22.1 into its 44 branch (electron/electron#55162) but has not released it yet. ## Implementation - `refuseBrotliResponses` is an undici interceptor. It removes `br` (and the `*` wildcard) from `Accept-Encoding`, and it aborts a response whose `Content-Encoding` lists `br` at the header stage, so the body never reaches the decoder. A block inside the decoder makes `fetch` hang instead of failing, so the guard sits on the dispatcher. - Its handler forwards every call itself instead of extending undici's `DecoratorHandler`, which allows one response start: undici starts one for each informational response (103 Early Hints) before the final one. - `ServiceContainer` installs it on the global dispatcher, which covers every plain `fetch` (MCP SDK transports, OAuth). Desktop, `xum server` and ACP all build a `ServiceContainer` before any service runs. If Node's own fetch ran first on Node 22, the global dispatcher comes from Node's bundled undici 6, which calls the older handler methods, so a dispatcher from the locked undici takes its place there. - A `dispatcher` passed to `fetch` bypasses the global one, so the three pools that `fetch` uses directly are wrapped too: provider calls (`providerModelFactory`), the bash AI proxy and server updates. - Model discovery, Bedrock discovery and MCP icons use `undici.request`, which never decodes `Content-Encoding`, so they need nothing. - `BROTLI_FIXED_NODE_RELEASES` moves from `src/cli/nodeVersion.ts` to `src/node/utils/nodeBrotliFix.ts`, so the CLI warning and the guard share one table. The Docker build's Node check now copies both compiled files with their `dist/` paths, because `nodeVersion.js` requires the table by a relative path. Not covered: `google-auth-library` (Vertex sign-in) fetches through `node-fetch`, which decodes Brotli itself. It talks only to Google's token endpoints. ## Validation - `brotliResponseGuard.test.ts` runs real undici `fetch` against a local server. Without the interceptor the Brotli body decodes; with it the request fails. Each test fails when the refusal or the header rewrite is removed. The 103 Early Hints test failed with `ERR_ASSERTION` on the `DecoratorHandler` version. - The compiled guard on Node 22.19.0, Node 24.21.0 and Electron 44.5.1, each with and without a `fetch` before the install: gzip and 103 + gzip decode, Brotli is refused. Before the fix, Node 22.19.0 with an earlier `fetch` failed every request (`invalid onError method`). - The Docker Node check, run on the compiled files in that layout: Node 22.23.4 and 24.22.1 pass; 22.19.0, 24.21.0 and 26.8.1 fail with the CVE warning. With `nodeVersion.js` copied alone, as before, it fails with `Cannot find module '../node/utils/nodeBrotliFix'` (the first merge-queue run). - On Electron 44.5.1 (Node 24.21.0), with the compiled main-process modules: before `new ServiceContainer(...)`, a Brotli response decoded. After it, plain `fetch` and `fetchDistTags` (the server-update pool) refused it, gzip responses still worked, and all three wrapped pools were composed. ## Risks Low. A server that ignores `Accept-Encoding` and sends Brotli anyway now fails with "Refused a Brotli-encoded response (CVE-2026-111497)". The guard is off under Bun and on fixed Node, so `xum server` on a current Node and the Docker image do not change.
🤖 release: v0.31.1 (#7361) ## Summary Version bump for the v0.31.1 patch release. It ships the Node2Shell fix (#7359, CVE-2026-111497) to Stable: today `ghcr.io/coder/mux:latest` (v0.31.0) runs Node 22.23.3 and decodes compressed request bodies before auth on a published port. The release also carries every change on main since v0.31.0. The GitHub Release notes will list the highlights. ## Implementation Bumped with `node ./scripts/set-package-version.js 0.31.1`, so the root `package.json` and the legacy `packages/mux-compat` forwarding package stay version-locked. A stable npm publish uses the `package.json` version as-is. `src/common/compat/productIdentity.test.ts` passes locally. After this PR merges, the GitHub Release `v0.31.1` will be published on the squash commit. That starts the desktop, Docker (`latest`), VS Code and npm (`latest`) pipelines.
🤖 release: v0.31.0 Bump the main and legacy compatibility packages together for the next minor release. --- _Generated with `xum` • Model: `openai:gpt-6.1-sol` • Thinking: `high` • Cost: unavailable_ <!-- mux-attribution: model=openai:gpt-6.1-sol thinking=high costs=unknown -->
🤖 fix: drop node-pty and the Electron rebuild step, make buildTime re… …producible (#4309) The Nix build at `60d40394d` had two problems: `postinstall.sh` always failed inside the sandbox (no Electron headers, `--ignore-scripts` in the FOD) and `exit 0` swallowed the failure, and `buildTime` came from the wall clock so the output was never reproducible. Whilst looking into the first one, it turned out the rebuild step never did necessary work. #660 moved us to `node-pty@1.1.0-beta39` and added `@electron/rebuild` in the same PR, but beta39 is where `node-pty` switched from `nan` to `node-addon-api`. Node-API is ABI-stable across Node and Electron, so the rebuild was producing a binary functionally identical to the prebuild it overwrote. #664 then added `@lydell/node-pty` as a fallback on the assumption that its Mac prebuilds weren't built for the Electron ABI; they're Node-API too, and the desktop app has been loading them inside Electron for SSH, Docker and devcontainer terminals since that PR landed. On Linux `node-pty` has no prebuild at all, so Nix and Docker were already on `@lydell` for every terminal. So, we'll just keep `@lydell/node-pty`. This removes `node-pty`, `@electron/rebuild`, `scripts/postinstall.sh`, `make rebuild-native`, the two-package loader in `ptySpawn.ts`, and the `XUM_HEADLESS` plumbing in the Dockerfile, flake and benchmark setup. The DuckDB rebuild step goes too; `@duckdb/node-bindings` has no `binding.gyp`, so it was a no-op. `asarUnpack` now covers `@lydell/**` so `pty.node`, `spawn-helper` and the conpty files leave the asar. `@types/node` is pinned at `^24.9.0` because it was previously hoisted from `electron` and would otherwise fall back to the Node 20 types. For `buildTime`, `generate-version.sh` now uses `SOURCE_DATE_EPOCH`, falling back to the commit time and then the wall clock (Docker builds have no `.git`). The flake sets `SOURCE_DATE_EPOCH` from `self.lastModified` so About shows the commit date rather than 1980. The "reuse existing buildTime" block from #2915 is gone since the existing `cmp -s` early exit now gives the same HMR stability. Release builds will show the tagged commit's time rather than when CI ran. I verified the untouched `@lydell` Linux prebuild from the Nix output loads and spawns a shell under Electron 40.10.0 and 43.6.0 via `ELECTRON_RUN_AS_NODE`, and `nix build .#xum --rebuild` passes. The one path that actually changes behaviour is Local terminals on macOS/Windows desktop, which move from one Node-API pty prebuild to another of the same fork. I haven't run `make dist-win`, so Windows desktop is the least exercised path here. Unrelated: the `offlineCache` FOD flakes with `Fail extracting tarball for "mermaid"` in roughly 2 of 3 sandbox runs, with both the old and new lockfile. I dug into it (`strace`, `curl` inside the sandbox) and it looks like a bun extraction flake on the largest tarball rather than a network or lockfile issue. Out of scope for now. --- _Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `xhigh` • Cost: `$17.46`_ <!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=xhigh costs=17.46 -->
PreviousNext