fix: device pairing floods stacked approval alerts for a retrying device and stale approvals no-op#100976
Conversation
… devices A device retrying with a broken token minted a new pending requestId (and approval alert broadcast) every 5-minute TTL window because refresh preserved ts and expiry keyed on ts. Pending requests now stay alive via an internal refreshedAtMs keepalive (ts still owns ordering/--latest), superseded device requests broadcast device.pair.resolved like node pairing already did, and the Mac app keeps one alert per device, closes superseded visible alerts, and resyncs its queue when approving a stale request. Closes #100974
…ing prompter changes
|
Codex review: needs real behavior proof before merge. Reviewed July 6, 2026, 12:02 PM ET / 16:02 UTC. Summary PR surface: Source +57, Tests +49, Docs +2, Other +61. Total +169 across 11 files. Reproducibility: yes. Source inspection on current main shows active retries preserve the original Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Land this gateway/macOS pairing fix after maintainer review and exact-head gates, keeping Do we have a high-confidence way to reproduce the issue? Yes. Source inspection on current main shows active retries preserve the original Is this the best way to solve the issue? Yes. The best fix boundary is the combination used here: keep one live gateway request for active retries, broadcast superseded request IDs, and make the macOS queue recover from stale local alerts. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against dc600d4a77fd. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +57, Tests +49, Docs +2, Other +61. Total +169 across 11 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
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
|
|
Merged via squash.
|
…ice and stale approvals no-op (openclaw#100976) * fix(gateway): stop device pairing approval alert floods from retrying devices A device retrying with a broken token minted a new pending requestId (and approval alert broadcast) every 5-minute TTL window because refresh preserved ts and expiry keyed on ts. Pending requests now stay alive via an internal refreshedAtMs keepalive (ts still owns ordering/--latest), superseded device requests broadcast device.pair.resolved like node pairing already did, and the Mac app keeps one alert per device, closes superseded visible alerts, and resyncs its queue when approving a stale request. Closes openclaw#100974 * chore: satisfy swiftformat and refresh native i18n inventory for pairing prompter changes
…ice and stale approvals no-op (openclaw#100976) * fix(gateway): stop device pairing approval alert floods from retrying devices A device retrying with a broken token minted a new pending requestId (and approval alert broadcast) every 5-minute TTL window because refresh preserved ts and expiry keyed on ts. Pending requests now stay alive via an internal refreshedAtMs keepalive (ts still owns ordering/--latest), superseded device requests broadcast device.pair.resolved like node pairing already did, and the Mac app keeps one alert per device, closes superseded visible alerts, and resyncs its queue when approving a stale request. Closes openclaw#100974 * chore: satisfy swiftformat and refresh native i18n inventory for pairing prompter changes
Closes #100974
What Problem This Solves
Fixes an issue where users with a device stuck in pairing repair (for example a Mac node whose stored token no longer verifies) would wake up to an unbounded stack of "New Mac wants to connect" approval alerts in the Mac app — roughly one new alert per 5 minutes — and where clicking Approve on each stacked alert silently did nothing except reveal the next stale alert.
Why This Change Was Made
The gateway expired pending device-pairing requests 5 minutes after creation even while the device kept retrying (refresh preserves
tsfor anti-queue-jumping, and the TTL also keyed onts), so every TTL window it silently pruned the old request and minted a freshrequestIdplus adevice.pair.requestedbroadcast. Pending requests now carry a store-internalrefreshedAtMskeepalive so an actively retrying device keeps one live request (and one approvable alert);tsstill owns approval ordering and the implicit--latestrace protection, and the field never crosses the protocol boundary (stripped like node pairing's internalrevision). Superseded device requests now broadcastdevice.pair.resolved, mirroring the existing node-pairing behavior. The Mac app additionally keeps at most one queued alert per device, dedupes byrequestId, closes a visible alert that got superseded, and resyncs its queue from the gateway when approving a stale request fails — so an already-accumulated alert pile collapses after a single click.User Impact
device.pair.requestedbroadcasts to every operator-scoped client (Mac app, control UI).Evidence
src/infra/device-pairing.test.ts).src/infra/device-pairing.test.ts,src/infra/device-pairing-churn.test.ts).apps/macos/Tests/OpenClawIPCTests/NodePairingApprovalPrompterTests.swift).pnpm test src/infra/device-pairing.test.ts src/infra/device-pairing-churn.test.ts→ 62/62 passed.pnpm check:changedgate green (typecheck lanes, lint, import cycles, pairing guards) — run https://github.com/openclaw/openclaw/actions/runs/28801209780.swift test --filter PairingApprovalPrompterTestsbuild + 6/6 tests passed (arm64e, macOS 26.5).gpt-5.5,--mode local): clean, no actionable findings.