Skip to content

fix(android): keep cold-start gateway auto-connect from overriding explicit intents#101799

Merged
steipete merged 2 commits into
mainfrom
claude/wonderful-mestorf-078018
Jul 7, 2026
Merged

fix(android): keep cold-start gateway auto-connect from overriding explicit intents#101799
steipete merged 2 commits into
mainfrom
claude/wonderful-mestorf-078018

Conversation

@steipete

@steipete steipete commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Two Android JVM unit tests flaked repeatedly on Linux CI (android-test-play) across unrelated PRs (#101002, #101387, #101396, observed 2026-07-06/07) while passing locally on macOS:

  1. GatewayBootstrapAuthTest.refreshGatewayConnection_reconnectsSavedManualEndpointAfterDisconnectIllegalStateException at the desiredConnection(runtime, "nodeSession") ?: error(...) assertion (plus a sibling failure in switchToUndiscoveredGatewayKeepsCurrentConnectionAndActiveGateway in run 28830800141).
  2. GatewaySessionInvokeTest.disconnectReportsUnknownOutcomeForFireAndForgetRpcTimeoutCancellationException awaiting the fire-and-forget onError (attempt-1 log of run 28847716276).

Root causes:

  1. NodeRuntime.init collects gateway discovery on a background dispatcher and auto-connects the saved active gateway whenever that collector happens to run. On loaded CI runners it lands mid-test and races explicit connect/disconnect/refresh through gatewayLifecycleIntentSeq (silent guarded-block drop) and gatewaySwitchMutex (async defer). This is also a real product hole: a late discovery emission could reconnect over an explicit user disconnect.
  2. GatewaySession.Connection.joinOwnedWork() cancels connectionJob right after failPending(). The fire-and-forget error watcher launched by sendRequestFrame is a DEFAULT-start child of that job; if still queued for dispatch it is cancelled without ever running, silently dropping the terminal UNAVAILABLE "Gateway disconnected before response" callback.

Why This Change Was Made

  • autoConnectIfNeeded() now atomically claims the very first lifecycle intent (gatewayLifecycleIntentSeq.compareAndSet(0, 1)) and stands down permanently when any explicit connect/disconnect/switch intent already exists. Both connect() overloads share a new launchConnect helper.
  • sendRequestFrame starts its watcher with CoroutineStart.ATOMIC (stable @DelicateCoroutinesApi in kotlinx-coroutines 1.11.0, documented to start even when its job is already cancelled). Since failPending() always precedes the cancel, await() surfaces the disconnect outcome without suspending.
  • The two racy tests now cancel-and-join the runtime scope before arming the registry (neutralizeColdStartAutoConnect), making the scripted sequences fully synchronous; two new regression tests pin the auto-connect intent-claim contract. Supersedes the poll-based stabilization from fix(android): stop saved gateway reconnect CI flake #101519 with a deterministic setup (its bounded waitForDesiredConnection remains for the final assert).

User Impact

  • Android app no longer auto-connects over an explicit disconnect/connect/switch issued before gateway discovery settles.
  • Fire-and-forget gateway RPC callers (e.g. talk-mode audio append) reliably receive the terminal error on disconnect instead of silently losing it.
  • The android-test-play CI lane stops flaking on these two tests.

Evidence

  • Reproduced flake (1) on a Blacksmith Linux box at iteration 1 of a 120x loop under taskset -c 0,1, with a seq probe proving the auto-connect interloper (seqBefore=3 after only connect+disconnect).
  • Proved mechanism (2) with a deterministic manual-dispatcher demo on kotlinx-coroutines 1.11.0: DEFAULT start drops the terminal error when cancelled before first dispatch; ATOMIC delivers it. Verified DispatchedTask exceptional-resume dominance and JobSupport final-cause selection in the 1.11.0 sources.
  • Post-fix on the same box: 3 pinned stress rounds (120/150 looped iterations plus both real classes, 0 failures), full :app:testPlayDebugUnitTest (109 classes, 0 failures), :app:ktlintCheck green; revalidated after rebasing onto latest main.
  • Codex autoreview on the diff: clean, no actionable findings.

…plicit intents

NodeRuntime's init-time auto-connect now atomically claims the first gateway
lifecycle intent (CAS 0->1) and stands down permanently when any explicit
connect/disconnect/switch intent already exists, so a late discovery emission
can no longer override a user decision. This was also the root cause of the
CI-only flakes in GatewayBootstrapAuthTest (refreshGatewayConnection_* at
:616/:627 and switchToUndiscoveredGateway* on PRs #101002/#101387/#101396).

GatewaySession.Connection.sendRequestFrame now starts its response watcher
with CoroutineStart.ATOMIC: disconnect teardown cancels connectionJob right
after failPending(), and a DEFAULT-start watcher still queued for dispatch was
cancelled without running, silently dropping the terminal UNAVAILABLE onError
owed to fire-and-forget callers (root cause of the flaky
GatewaySessionInvokeTest.disconnectReportsUnknownOutcomeForFireAndForgetRpc).

Tests neutralize runtime background work (cancel+join the runtime scope)
before arming the registry and scripting lifecycle steps, and add regression
coverage for the auto-connect intent claim.

Proof: refresh flake reproduced at iteration 1 under taskset -c 0,1 on a
Blacksmith Linux box; post-fix 3 pinned stress rounds (120/150 looped
iterations) plus full :app:testPlayDebugUnitTest (109 classes, 0 failures)
and :app:ktlintCheck green on the same box.
@openclaw-barnacle openclaw-barnacle Bot added app: android App: android size: S maintainer Maintainer-authored PR labels Jul 7, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93e53bdede

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1748 to +1749
if (!gatewayLifecycleIntentSeq.compareAndSet(0L, 1L)) return
launchConnect(endpoint, explicitAuth = null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind auto-connect to the claimed intent

Because compareAndSet(0L, 1L) only claims the sequence before launchConnect, an explicit lifecycle call can still increment the sequence in the gap before launchGatewayLifecycle() samples it. In that interleaving (auto CAS succeeds, user disconnects/connects and bumps the sequence, then this call captures the newer value), the auto-connect block passes the guard and reconnects to the saved endpoint, so cold-start discovery can still override the user's explicit intent. Pass the claimed intent into the guarded launch or recheck against the claimed value under the lifecycle lock.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs changes before merge. Reviewed July 7, 2026, 1:43 PM ET / 17:43 UTC.

Summary
The branch changes Android NodeRuntime cold-start gateway auto-connect handling, GatewaySession fire-and-forget RPC watcher startup, related Android gateway tests, and native i18n line metadata.

PR surface: Other +68. Total +68 across 4 files.

Reproducibility: yes. from source inspection: the PR head can still let an explicit intent increment gatewayLifecycleIntentSeq after the auto-connect CAS but before launchGatewayLifecycle samples it. I did not run Android tests in this read-only review.

Review metrics: none identified.

Root-cause cluster
Relationship: canonical
Canonical: #101799
Summary: This PR remains the live canonical candidate for the Android cold-start gateway auto-connect race and fire-and-forget disconnect callback fix, but it still needs the claimed-intent repair.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦐 gold shrimp
Proof: 🦞 diamond lobster
Patch quality: 🦐 gold shrimp
Result: needs maintainer review before merge.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Carry the CAS-claimed auto-connect intent into the guarded launch or recheck it under the lifecycle lock.
  • [P2] Add a targeted regression test that forces an explicit lifecycle intent between the auto-connect claim and the guarded launch, then rerun the focused Android gateway tests.

Risk before merge

  • [P1] Merging this head as-is would still leave a narrow cold-start interleaving where stale auto-connect can reconnect over an explicit user lifecycle intent.

Maintainer options:

  1. Decide the mitigation before merge
    Land this after auto-connect uses the CAS-claimed lifecycle intent as the guard token, with a regression test that forces an explicit intent between the claim and guarded launch.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P2] There is a narrow mechanical repair: carry the claimed auto-connect lifecycle intent into the guarded launch and add one interleaving regression test.

Security
Cleared: The diff changes Android runtime/test code and i18n line metadata without adding dependencies, workflows, secrets, permissions, package-resolution changes, or external code execution.

Review findings

  • [P2] Bind auto-connect to the claimed intent — apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt:1749
Review details

Best possible solution:

Land this after auto-connect uses the CAS-claimed lifecycle intent as the guard token, with a regression test that forces an explicit intent between the claim and guarded launch.

Do we have a high-confidence way to reproduce the issue?

Yes from source inspection: the PR head can still let an explicit intent increment gatewayLifecycleIntentSeq after the auto-connect CAS but before launchGatewayLifecycle samples it. I did not run Android tests in this read-only review.

Is this the best way to solve the issue?

No, not yet. The GatewaySession ATOMIC watcher is supported by the coroutine contract, but NodeRuntime still needs to bind the claimed auto-connect intent into the guarded launch.

Full review comments:

  • [P2] Bind auto-connect to the claimed intent — apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt:1749
    This is the still-unfixed prior blocker. The latest head did not change NodeRuntime after the previous review, so compareAndSet(0L, 1L) still calls launchConnect() without carrying the claimed value forward; if an explicit disconnect or connect increments gatewayLifecycleIntentSeq before launchGatewayLifecycle() samples it, the stale auto-connect captures that newer intent and can reconnect over the user's action.
    Confidence: 0.93

Overall correctness: patch is incorrect
Overall confidence: 0.91

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 53d0c4ca5050.

Label changes

Label justifications:

  • P1: The PR targets an Android gateway workflow regression where explicit user disconnect/connect intent and fire-and-forget terminal errors can be lost in active app use.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (terminal): The PR body includes terminal-style Blacksmith/Linux repro, post-fix stress runs, full Android unit evidence, and ktlint evidence; that proof is sufficient for the reported paths but does not remove the source-level blocker above.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes terminal-style Blacksmith/Linux repro, post-fix stress runs, full Android unit evidence, and ktlint evidence; that proof is sufficient for the reported paths but does not remove the source-level blocker above.
Evidence reviewed

PR surface:

Other +68. Total +68 across 4 files.

View PR surface stats
Area Files Added Removed Net
Source 0 0 0 0
Tests 0 0 0 0
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 4 107 39 +68
Total 4 107 39 +68

Acceptance criteria:

  • [P1] node scripts/run-android-gradle.mjs :app:testPlayDebugUnitTest --tests ai.openclaw.app.GatewayBootstrapAuthTest --tests ai.openclaw.app.gateway.GatewaySessionInvokeTest --rerun-tasks.
  • [P1] node scripts/run-android-gradle.mjs :app:ktlintCheck.
  • [P1] corepack pnpm native:i18n:sync.

What I checked:

Likely related people:

  • steipete: Recent path history shows repeated Android gateway and NodeRuntime work, including multi-gateway registry/switching, reconnect recovery, and the merged saved-gateway reconnect stabilization related to this PR. (role: feature owner and recent area contributor; confidence: high; commits: d931839e0c12, 2b48e9814840, 9cd29dcb9a45; files: apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt, apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt, apps/android/app/src/test/java/ai/openclaw/app/GatewayBootstrapAuthTest.kt)
  • vincentkoc: Recent adjacent test history includes gateway bootstrap preference isolation, which is close to the tests changed by this PR. (role: recent area contributor; confidence: medium; commits: 7bbe75089ac7; files: apps/android/app/src/test/java/ai/openclaw/app/GatewayBootstrapAuthTest.kt)
  • NianJiuZst: The GatewaySession pending-RPC-on-close behavior traces to the merged Android gateway pending RPC cancellation work, which is adjacent to this PR's fire-and-forget callback fix. (role: adjacent owner; confidence: medium; commits: 0ccdef5dcf76; files: apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt, apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewaySessionInvokeTest.kt)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (1 earlier review cycle)
  • reviewed 2026-07-07T16:46:48.852Z sha 93e53bd :: needs changes before merge. :: [P2] Bind auto-connect to the claimed intent

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P1 High-priority user-facing bug, regression, or broken workflow. labels Jul 7, 2026
@steipete
steipete merged commit 3d53ed7 into main Jul 7, 2026
81 checks passed
@steipete
steipete deleted the claude/wonderful-mestorf-078018 branch July 7, 2026 17:51
@steipete

steipete commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Merged via squash.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 8, 2026
…plicit intents (openclaw#101799)

* fix(android): keep cold-start gateway auto-connect from overriding explicit intents

NodeRuntime's init-time auto-connect now atomically claims the first gateway
lifecycle intent (CAS 0->1) and stands down permanently when any explicit
connect/disconnect/switch intent already exists, so a late discovery emission
can no longer override a user decision. This was also the root cause of the
CI-only flakes in GatewayBootstrapAuthTest (refreshGatewayConnection_* at
:616/:627 and switchToUndiscoveredGateway* on PRs openclaw#101002/openclaw#101387/openclaw#101396).

GatewaySession.Connection.sendRequestFrame now starts its response watcher
with CoroutineStart.ATOMIC: disconnect teardown cancels connectionJob right
after failPending(), and a DEFAULT-start watcher still queued for dispatch was
cancelled without running, silently dropping the terminal UNAVAILABLE onError
owed to fire-and-forget callers (root cause of the flaky
GatewaySessionInvokeTest.disconnectReportsUnknownOutcomeForFireAndForgetRpc).

Tests neutralize runtime background work (cancel+join the runtime scope)
before arming the registry and scripting lifecycle steps, and add regression
coverage for the auto-connect intent claim.

Proof: refresh flake reproduced at iteration 1 under taskset -c 0,1 on a
Blacksmith Linux box; post-fix 3 pinned stress rounds (120/150 looped
iterations) plus full :app:testPlayDebugUnitTest (109 classes, 0 failures)
and :app:ktlintCheck green on the same box.

* chore(i18n): resync native inventory line positions after NodeRuntime edits
giodl73-repo pushed a commit to giodl73-repo/openclaw that referenced this pull request Jul 8, 2026
…plicit intents (openclaw#101799)

* fix(android): keep cold-start gateway auto-connect from overriding explicit intents

NodeRuntime's init-time auto-connect now atomically claims the first gateway
lifecycle intent (CAS 0->1) and stands down permanently when any explicit
connect/disconnect/switch intent already exists, so a late discovery emission
can no longer override a user decision. This was also the root cause of the
CI-only flakes in GatewayBootstrapAuthTest (refreshGatewayConnection_* at
:616/:627 and switchToUndiscoveredGateway* on PRs openclaw#101002/openclaw#101387/openclaw#101396).

GatewaySession.Connection.sendRequestFrame now starts its response watcher
with CoroutineStart.ATOMIC: disconnect teardown cancels connectionJob right
after failPending(), and a DEFAULT-start watcher still queued for dispatch was
cancelled without running, silently dropping the terminal UNAVAILABLE onError
owed to fire-and-forget callers (root cause of the flaky
GatewaySessionInvokeTest.disconnectReportsUnknownOutcomeForFireAndForgetRpc).

Tests neutralize runtime background work (cancel+join the runtime scope)
before arming the registry and scripting lifecycle steps, and add regression
coverage for the auto-connect intent claim.

Proof: refresh flake reproduced at iteration 1 under taskset -c 0,1 on a
Blacksmith Linux box; post-fix 3 pinned stress rounds (120/150 looped
iterations) plus full :app:testPlayDebugUnitTest (109 classes, 0 failures)
and :app:ktlintCheck green on the same box.

* chore(i18n): resync native inventory line positions after NodeRuntime edits
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: android App: android maintainer Maintainer-authored PR P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. size: S status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant