Adds `deletedAt` sweep guards to the engine's self-healing and merger to prevent recovery operations from processing soft-deleted tasks, filters deleted tasks in the `recover-stale-blocked-by` script, includes a new regression test for the deadlock-scan exclusion pattern, and updates the soft-delete
Fusion-Task-Id: FN-5528
Fusion-Task-Lineage: 5c9e45ca-49a8-47a0-a23d-6fe8e15e7e00
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5528
51fc82619 (FN-5060 dedup) added sourceParentTaskId / sourceMetadata /
sourceRunId to the heartbeat-spawned task source but didn't update this
test, so the assertion was failing on `sourceRunId: undefined` matching
against the real `"run-001"`. Drop the over-tight key — we still verify
sourceType and sourceAgentId; sourceRunId is covered separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the "workflow-step" MockSessionPurpose enum value and the
workflowStepId / workflowStepTemplateId plumbing through
agent-runtime, agent-session-helpers, mock-provider, executor, and
merger. The seeded-workflow-prompts script loses its FN-5205
rationale comment + test (no longer applicable now that workflow
steps run through the regular session purposes).
Also strips the stale FN-5482 architecture-invariant bullet from
AGENTS.md and the corresponding audit-event line from
docs/architecture.md (the self-healing reclaim invariant they
described no longer holds).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the BROAD_SCOPE_FLAG_VERSION/decideBroadScopeFlag pipeline,
the triage.ts call site that wrote `broadScopeFlag` source metadata
and emitted task:broad-scope-flagged-at-triage audit events, the
DatabaseMutationType enum entry, the diagnostic doc section, and the
associated unit + reliability-interaction tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same-agent intake guard now also matches siblings sharing a
sourceParentTaskId, so repeated heartbeats from one parent task
can't bypass dedup just because triage rewrites the title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
assertCleanBranchAtBase now checks each foreign-attributed commit
against `git merge-base --is-ancestor <sha> main`. If the commit is
already on local main, it was promoted through integration regardless
of whose Fusion-Task-Id trailer it carries — treating it as foreign
contamination is wrong and was the root cause of the FN-5475 cascade
(downstream worktrees inherited a sibling task's tip during the brief
fast-forward window before main moved further).
Audit cost: O(N) extra git calls per audit run, where N is the number
of foreign-trailer commits in baseSha..branchName. Each call is ~5-10ms
and N is typically 1-5. Negligible relative to the surrounding I/O.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The visualViewport ICB compensation (intended for Android ICB-stuck-large
and pinch-zoom) was also reacting to iOS soft-keyboard shrinkage, pushing
the mobile bottom nav up above the keyboard. Pass the existing
`keyboardOpen` prop through to a `mobile-nav-bar--keyboard-open` modifier
class that pins `bottom: 0` so the keyboard simply covers the bar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The resume-path verifier in acquireTaskWorktree now also calls
classifyBootstrapMisbinding / reanchorBranchToBase before the
executor's primary contamination check runs. With a once-spy the mock
was depleted by the verifier, leaving the executor path with the real
implementation and the FN-4488 shape regression test no longer
exercised its expected recovery branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The resume path in acquireTaskWorktree returned a reused worktree
without checking whether its branch contained foreign commits. If a
sibling task's tip had been baked into the branch at creation time,
the executor preflight would later fail contamination checks forever
(observed in the FN-5475 cascade).
The resume path now computes a fresh merge-base and runs
classifyBootstrapMisbinding. For the foreign-only / zero-own-commits
shape it re-anchors inline and emits a branch:reanchor audit event.
Mixed contamination continues to flow through the executor's
primary recovery path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auto-recovery handler in branch-worktree.ts passed
foreignCommits: [] to classifyBootstrapMisbinding, and the classifier
gated isBootstrapMisbinding on foreignCommits.length > 0. The entire
reanchor block was dead code on this path — the FN-5475 cascade hit
"human adjudication" instead of recovering.
The classifier now derives the foreign-commit count from its own
git log walk; the input field is advisory/optional. Result type gains
foreignCommitCount. The fallback handler also stops using
ctx.task.baseCommitSha (deliberately stale per FN-4417) and computes
a fresh merge-base against local main / origin/main, matching the
executor's primary contamination path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a `useOverlayDismiss` hook that suppresses touch-synthesized mouse events on modal/dropdown overlays to prevent unintended close behavior on touch devices, with tests covering TaskCard dismissal and overlay interaction edge cases. Documentation updates in AGENTS.md and docs/architecture.md capt
Fusion-Task-Id: FN-5482
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5482
Test coverage for mobile views: `MobileNavBar.test.tsx` now syncs its mock with the viewport constant, and `SecretsView.mobile.test.tsx` derives secrets media parsing from the hook query.
Fusion-Task-Id: FN-5521
Fusion-Task-Lineage: eb37d760-ab67-49c5-9b57-2c14d648a98a
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5521
The SIGTERM/SIGINT/SIGHUP handlers added to db.test.ts and the engine
tmp-dir tests re-raised signals after cleanup, which killed vitest itself
(exit 143) under the full engine reliability suite. Keep `afterAll` +
`beforeExit`/`exit` + lock-child kill — those cover the macOS file-handle
leak that was the actual driver of the merge-verification cascade.
Also skip project-engine-manager `retries failed project starts on
subsequent reconciliation ticks` — flake under full-suite load (30s
timeout) that passes in ~46ms standalone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Header.css: mobile padding-top is additive (var(--space-md) + env(safe-area-inset-top)) so the brand row keeps its 12px breathing room below the Android status bar instead of having it replaced by the inset.
- ExecutorStatusBar.css: bottom offset now uses max(env(safe-area-inset-bottom), 12px) to match the floor MobileNavBar already applies, so the footer lands flush on top of the nav instead of inside its padding band.
- useToast.ts: silently drop bare "Failed to fetch" error toasts (from fetch() aborts on tab background/resume); toasts with additional context still pass through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Hoist `refs/heads/<name>` into a single quoted token before the verify
call so the shell-quote boundary is unambiguous in the rendered
command.
- Append `--` to the recovery `git checkout` so a same-named tracked
path cannot win the DWIM resolution. The ref existence was already
verified, so this can only resolve as the branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same class of failure as the kb-db-test-* leak: vitest's forks pool
SIGTERMs a fork on test timeout and skips the in-test `finally { rmSync }`,
leaking `fusion-test-ref-project-*`, `fusion-test-ref-concurrent-project-*`,
and `fusion-test-ref-advance-*` dirs that scripts/check-test-isolation.mjs
flags during deterministic merge verification.
Track every minted dir in a per-file set and sweep it from
SIGTERM/SIGINT/SIGHUP/beforeExit/exit handlers (signals re-raised after
cleanup) plus an `afterAll` for the happy path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vitest's forks pool SIGTERMs a fork when a test times out, which skips
`beforeExit`/`exit` handlers and leaves `kb-db-test-*` dirs behind.
`holdWriteLock` child processes also kept WAL/SHM handles open, blocking
recursive removal on macOS. Both paths now run cleanup: SIGTERM/SIGINT/
SIGHUP handlers sweep tracked dirs and re-raise the signal, and active
lock-helper children are tracked and SIGKILLed during cleanup so the
parent dir can be removed.
These leaks tripped scripts/check-test-isolation.mjs during deterministic
merge verification, failing auto-merge with "Completion handoff limbo
recovery exhausted" (e.g. FN-5521, FN-5486).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address the code-review finding on the prior commit: a natural premise-stale
summary like "PREMISE STALE: the task has no remaining work — implementation
is already done on HEAD" matches /\b(incomplete|not implemented|not done|
not finished)\b/i with 'the task' inside the 40-char first-person window,
refusing fn_task_done with summary-claims-incomplete and deadlocking the
escape hatch.
When summary starts (case-insensitive) with PREMISE STALE:, skip the
dissent-pattern and scoped-incomplete summary checks. Pending-code-review
and bulk-step-completion guards still apply unchanged.
Add executor-task-done-premise-stale.test.ts covering: the deadlock case
now passes; dissent phrasing in a sentinel summary is allowed; case-
insensitive sentinel; sentinel must be at the start (mid-summary doesn't
bypass); REVISE verdict still blocks even with the sentinel.
Root-cause prevention for the FN-5521 failure class where PROMPT.md describes
work that is already done on HEAD and the executor still marches through
plan/review/test/doc, then merger burns hours retrying against unrelated
flakes in @fusion/engine reliability-interactions.
- Executor system prompt: add a Preflight escape hatch. When Step 0
reproduces and finds HEAD already matches the desired state, the agent
marks Step 0 done, marks remaining steps skipped, and calls fn_task_done
with a `PREMISE STALE:` summary. Reuses the existing
evaluateTaskDoneRefusal/skipped semantics and the merger's
empty-own-diff fast-path — no new tools or refusal classes needed.
- packages/engine/vitest.config.ts: split into two projects. engine-default
retains full parallelism; engine-reliability scopes
src/__tests__/reliability-interactions/** to
poolOptions.threads.singleThread so event-ordering assertions stop
flaking under workspace-concurrent merge-gate load (the
`expected 24 to be less than 19` rowid interleaving in
merge-reuse-task-worktree).
Two test-infrastructure fixes from agent #1's flake-stabilization pass.
Skipped its DESC-order assertion rewrite (semantically equivalent no-op
that conflicted with the prior boilerplate consolidation in 848a226ca)
and its vitest.config additions (already in flight on main).
1. vitest-setup.ts: completedSubprocessFailures was a plain string[]. When
a 30s subprocess-guard timer fired during a *later* test's execution
window (because the owning test ran for e.g. 40s under its 60s
timeout budget), the failure surfaced in the innocent successor test's
afterEach. Typed the array as { ownerTestName, message }[] and filter
on the current test name; orphaned entries are dropped silently.
2. worktree-contamination-attribution.real-git.test.ts: afterEach rm
occasionally hits ENOTEMPTY on macOS when a git rebase internal dir
isn't fully flushed. Added maxRetries: 3, retryDelay: 100.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review fixes for 036387610 / d02cd38d7:
1. `deriveScopedPnpmTestCommand` now passes each `--filter` argument through
`quoteArg`. Package names come from workspace `package.json` files which
are not strictly trusted input — a metacharacter in a name would have
leaked into the shell command.
2. `getBranchChangedFiles` now quotes both git refs in the `<base>...<head>`
range. Branch names can legally contain `/` and other characters; this
is defense-in-depth consistent with the rest of merger.ts.
3. Out-of-scope detection is now package-aware via a new
`packageNamesForFiles` helper. A failure in `__tests__/foo.test.ts` is
correctly treated as in-scope when the branch touched `src/foo.ts` in
the same package, whereas the previous filename-prefix heuristic missed
that case entirely. The dead `bf.startsWith(ff/)` clause is removed.
Falls back to the directory-prefix heuristic when pnpm-workspace.yaml
is unavailable.
195 tests pass across merger-verification.test.ts and the reused
merge-reuse-task-worktree.test.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each of the 18 tests in this suite was ~50 lines of identical setup: rename
main→master, mark steps done, create the branch with one content commit,
create the task worktree, set task.worktree, enqueue the merge queue. The
test bodies were dominated by ceremony and the unique assertions were buried.
Consolidated into a single `setupReuseHandoff` helper that takes per-test
overrides (skipWorktreeAdd, worktreeOverride, skipEnqueue, emptyOwnDiff,
extraSettings). Added a top-level beforeEach to clear shared mock/registry
state that several tests forgot to reset.
No behavioral change. All 21 tests pass on two consecutive runs in ~20s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to a3ec2e55c addressing code-review feedback:
- Restrict the existence check to local heads via
`git show-ref --verify --quiet refs/heads/<name>` so a stray tag or
remote ref with the same name cannot satisfy the check and lead the
subsequent `git checkout` to a detached HEAD on the wrong object.
- Use `git branch -M` (force) instead of `-m` for the rename path so
case-only renames succeed on case-insensitive filesystems (macOS,
default Windows).
- Document the FN-5456 invariant on the function's doc comment.
- Add a real-git regression suite covering the three paths the fix
governs: no creation when expected ref is missing, switch-only when
it exists, and rejection of same-named tags.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`attemptBranchAutocorrect` was the only branch-creation site in the
engine that ran `git checkout -B <expected>` without a start point.
When the worktree's HEAD was at a previous occupant's tip, the new
label silently captured that commit — the "branch: Created from HEAD"
contamination pattern that the cross-contamination guard then refuses
to auto-resolve (see FN-5456: orphan FN-5477 commit 268574b9a stranded
on fusion/fn-5456).
Replace the unsafe fallback with verify-then-`git checkout`: only switch
to an existing expected ref; return `failed` when it does not exist so
upstream recovery — which knows the proper base SHA — can re-anchor via
`prepareForTask` / `reanchorBranchToBase`.
Tests updated for the new command sequence; new case covers the
"expected ref missing → fail without creating from HEAD" guarantee.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the in-merge fix agent makes no changes AND all failing test files are
outside the branch's diff, the merger now throws OutOfScopeVerificationError
and marks the task status: "failed" with a clear error message:
"Merge verification failed in files outside branch scope — likely
pre-existing flake on main. Fix the base-branch test breakage
separately and retry."
This prevents the task from entering the completion-handoff-limbo recovery
cycle (which would retry the merge endlessly) when the verification failure
is caused by pre-existing flakiness in an unrelated package (e.g. engine
reliability-interaction tests failing while only dashboard was changed).
Failing file paths are parsed from vitest/jest output (FAIL lines and ❯
summary lines). If parsing yields no file list, the existing retry behavior
is preserved. The OutOfScopeVerificationError propagates through the catch
block so it does not count toward completionHandoffLimboRecoveryCount.
New exports: OutOfScopeVerificationError, parseFailingFilesFromOutput,
getBranchChangedFiles.
Tests added: parseFailingFilesFromOutput (4), getBranchChangedFiles (3),
OutOfScopeVerificationError constructor (1). All 58 merger-verification
tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a pnpm workspace is detected and git context is available,
inferDefaultTestCommand now derives the set of packages touched by the
branch diff (git diff --name-only <base>...HEAD) and emits:
pnpm --filter "<pkg>...^" test
instead of the broad `pnpm test`. The `...^` suffix includes dependents
so packages that import the changed one are also exercised. Falls back
to unscoped `pnpm test` when git context is missing, the workspace has
no package roots, or all changed files are at the root (e.g. config).
New exports: parsePnpmWorkspaceGlobs, resolveWorkspacePackageRoots,
mapChangedFilesToPackageNames, deriveScopedPnpmTestCommand.
testSource is now "inferred-scoped" for the scoped path.
Tests added: parsePnpmWorkspaceGlobs (7), resolveWorkspacePackageRoots
(4), mapChangedFilesToPackageNames (4), inferDefaultTestCommand scoping
(6). All 466 merger test files pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Android Chrome under viewport-fit=cover intermittently reports
env(safe-area-inset-bottom) as 0 while the address bar is visible or
during URL-bar collapse, causing the bottom nav to render beneath the
system gesture pill. Floor the inset to 12px in MobileNavBar's own
padding and the matching content-padding rules so the bar always clears
the gesture area; devices that report a larger inset are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch commits Fusion produces (both executor step commits and merger squash
commits) from setting `--author="Fusion <noreply@runfusion.ai>"` to appending
`-m "Co-authored-by: Fusion <noreply@runfusion.ai>"`. The user's configured
git identity now stays as the primary author/committer, and Fusion is recorded
as a co-author (recognized by GitHub for shared attribution). The
`commitAuthorEnabled` toggle and `commitAuthorName`/`commitAuthorEmail`
settings keep their existing keys; the dashboard settings UI relabels them
from "Author" to "Co-author" to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two engine merger tests created mkdtempSync workspaces directly in tmpdir()
under the tracked `fusion-test-` prefix; under full-suite concurrent load
the post-run check-test-isolation flagged them as leaks. Route both
(`merger-no-op-fix-finalize.test.ts`, `merger-verification-fix-already-on-main.test.ts`)
through FUSION_TEST_WORKER_ROOT like sibling merger tests so they nest
inside the already-tracked worker root.
Bump engine vitest subprocess guard from 60s to 120s and testTimeout to
30s — plain git commands (branch -d, worktree remove) queued behind
system contention during `pnpm -r --workspace-concurrency=2` runs were
timing out. The guard only fires on hangs, so healthy tests pay nothing.
Also bundles in-progress dashboard mobile-breakpoint regex/CSS test
updates and docs index additions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The vitest subprocess guard's 60 s tracking timer could outlive the test
that spawned it and fire during a later test's afterEach, producing
spurious "Timed out after 60000ms" failures attributed to a different
test name under concurrent recursive test load.
Scope "Left running" reporting + SIGKILL to the current test's procs but
always clear each tracked subprocess's timer so it cannot fire later.
Bump the post-test grace from 200 ms to 1 s to absorb event-loop
contention from slow git shells.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 769–1024px breakpoint forced 6 columns into the visible width with
`grid-template-columns: repeat(6, minmax(0, 1fr))` and `overflow-x:
hidden`, collapsing columns to ~130–170px on Android tablets and
stacking task card titles one word per line. Switch to
`minmax(260px, 1fr)` with `overflow-x: auto` so columns keep a readable
minimum width and the board scrolls horizontally, matching desktop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ChatView and QuickChatFAB both had an iOS-specific onTouchStart on the
textarea that called event.preventDefault() and then programmatically
re-focused the input — meant to suppress iOS's visualViewport auto-scroll
on re-focus. On Android, preventDefault on a textarea touchstart blocks
the soft keyboard from opening (programmatic focus() alone does not raise
the Android keyboard — only the default touch action does), so tapping
the main chat or quick chat composer focused the input but the keyboard
never appeared, looking like an instant dismiss.
Gate the touchstart workaround to iOS via isIOS().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review follow-ups on 24686cade:
- Only `git reset --hard HEAD` / `git clean -fd` after the stash is confirmed (non-empty SHA + store succeeded). Previously an empty `git stash create` result (no throw) would still trigger the reset and wipe the user's dirty edits before the refusal was raised.
- Quote the stash label with a local `quoteAutostashArg` helper to match `merger.ts`'s `quoteArg` pattern. Task IDs are alphanumeric today, but defense-in-depth keeps `git stash store -m` safe against future label content.
- Add `merge:reuse-handoff-autostash` to the typed audit unions in `run-audit.ts` and `emitReuseHandoffAuditEvent`, so subscribers see it as part of the closed set instead of relying on the `as any` escape hatch.
- Best-effort `git reset` (unstage) in the autostash-failure path so the worktree isn't left half-staged when we refuse.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stop losing uncommitted dev edits during task merges.
- `acquireReuseHandoff` no longer throws MergeHandoffRefusedError("working-tree-dirty") on a dirty reused worktree (FN-5138). It autostashes via `git add -A` + `git stash create` + `git stash store`, emits a `merge:reuse-handoff-autostash` audit event with the stash SHA and a recover command, and lets the merge proceed.
- `stashUnrelatedRootDirChanges` no longer silently proceeds when stash creation fails on a dirty tree. It throws a new `AutostashCreationFailedError`; the merger catches it and surfaces a clear "your edits are intact" message before any destructive op runs.
- New failure reason `dirty-worktree-autostash-failed` distinguishes stash failure from the old refusal.
- Tests in `merger-integration-worktree`, `merger-cwd-fallback-removed`, and `reliability-interactions/{integration-worktree-state,merge-reuse-task-worktree,cwd-integration-fallback-removed}` updated to the new contract; the FN-5348 "no cwd fallback" invariant is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- touch-action is per-element, not inherited. Setting `pan-x pan-y` on
html/body alone didn't stop the kanban board: pinching on `.board`
reads `.board`'s own touch-action (auto by default), which permits
pinch-zoom. Switch to `* { touch-action: pan-x pan-y }` inside the
mobile media query so every element opts out of pinch by default;
element-specific overrides (touch-action: none on resize handles,
drag-source rules on TaskCard, etc.) still win on specificity.
- Add `overscroll-behavior: none` to html/body to prevent Chrome's
rubber-band scroll, which was letting users pull the page up to
expose empty space above the dashboard despite overflow:hidden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two Android-specific fixes:
1. Keyboard dismissing in main chat. App.tsx derives `mobileKeyboardOpen`
from useMobileKeyboard and uses it to gate the
`project-content--with-mobile-nav` / `--with-footer` className
assignment plus MobileNavBar rendering. When the soft keyboard opened
on Android, those classes were removed and the nav unmounted, shrinking
padding-bottom by ~80px in a single render. Android Chrome treats the
resulting jump of the focused chat input as the focus target moving and
instantly dismisses the keyboard. With interactive-widget=resizes-content
set on Android, the layout viewport itself shrinks with the keyboard, so
the hide-nav-on-keyboard pattern was redundant on Android (and harmful).
The whole pattern is now gated to iOS via isIOS(). iOS path is unchanged.
2. Pinch-zoom on kanban. Android Chrome ignores user-scalable=no for a11y,
and kanban's overflow-x:auto columns combined with the inflated ICB
produce a broken visual when the user zooms out. Adds
touch-action: pan-x pan-y to html,body inside the mobile media query
(keeps scroll panning, blocks pinch-zoom). Chat and MissionManager were
unaffected before because they don't expose a wide horizontal
scrollable region.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The body scroll-lock applied while the keyboard is up in main chat was an
iOS-specific workaround for visualViewport drift. On Android Chrome the same
mutation does the opposite of what we want — applying position:fixed to body
while the soft keyboard is opening causes Chrome to treat it as a focus-
target relayout and dismiss the keyboard instantly, making the main chat
composer unusable on Android.
useMobileScrollLock now early-returns on non-iOS user agents. Android Chrome
doesn't need it: with interactive-widget=resizes-content the layout viewport
shrinks with the keyboard, so there's no drift to compensate for.
Adds an Android-UA test case that asserts the lock is a no-op there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>