You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Browse filesBrowse the repository at this point in the historyBrowse files
authored
Fix zero-state chat trap and own-node pairing auto-approve
* Fix zero-state chat trap: drop CreateThreadAsync fiction, route by canonical sessionKey
Root cause: OpenClawChatDataProvider maintained a C# replica of gateway session
state keyed by 'threadId' that conflated UI selection token with server-assigned
sessionKey. On fresh install (zero sessions), CreateThreadAsync synthesized a
ChatThread with Id='main' (literal). But the gateway's hello-ok publishes BOTH
sessionDefaults.mainKey ('main' alias) AND sessionDefaults.mainSessionKey
('agent:main:main' canonical), and the parser was reading the alias. Optimistic
local entries landed under 'main' while gateway echoes used 'agent:main:main' —
two timelines, message orphaned, UI stuck on the welcome screen with no composer.
Fix (real, not a workaround — three-model agreement, dual-model code review):
Protocol layer (OpenClawGatewayClient):
- TryGetHandshakeMainSessionKey now prefers canonical mainSessionKey over alias.
- _mainSessionKey becomes string? (no 'main' default), with Volatile.Read/Write
for cross-thread publication.
- _hasHandshakeSnapshot tracks handshake completion; reset on disconnect.
- Extracted ResolveEffectiveSessionKey helper — throws InvalidOperationException
if no canonical key resolved. No more silent fallback to a stale literal.
- HandleChatEvent/HandleAgentEvent stop substituting 'main'/'unknown' for empty
sessionKey — pass through so the provider can surface the protocol gap.
Contract (IChatDataProvider):
- Deleted CreateThreadAsync. The gateway has no session.create RPC; pretending
it does was the original sin.
- Added ChatComposeTarget {SessionKey, IsReady} on ChatDataSnapshot — first-class
'where to send next' concept, distinct from 'what sessions exist'.
Provider (OpenClawChatDataProvider):
- Deleted CreateThreadAsync impl, ResolveMainOrFirstThreadLocked, and every
?? 'main' fallback in event handlers (5 sites). Empty SessionKey now drops
the event with a warning.
- BuildSnapshotLocked projects ComposeTarget from bridge state and injects a
synthetic ChatThread with Id=canonicalKey when the compose key has optimistic
entries but no SessionInfo yet — so SessionsUpdated later replaces it in
place with no re-keying or migration.
- ResolveDefaultThreadIdLocked uses SessionInfo.IsMain instead of string-equal
to literal 'main'.
UI (OpenClawChatRoot):
- Deleted StartFirstChat and the safety-net fallback (now structurally
impossible to fire).
- effectiveThread = selectedThread ?? composeOnlyThread; composer visible
whenever ComposeTarget.IsReady.
- Friendlier zero-state suggestions ('Say hi 👋', 'What can you do?',
'Give me a quick tour of OpenClaw').
- firstSendInFlight debounce clears only when the snapshot's compose-target
timeline has the optimistic user entry — not on every unrelated snapshot
(presence, models, channel health).
Tests: deleted 2 obsolete CreateThreadAsync tests; added 7 provider tests
covering fresh-install compose target, optimistic-by-canonical-key routing,
SessionsUpdated reconciliation, empty-sessionKey drop, canonical echo merge,
IsMain default resolution. Added 6-case OpenClawGatewayClientSessionKeyTests
exercising the ResolveEffectiveSessionKey throw directly.
Validation: build OK, Shared.Tests 1782/1810 (28 skipped), Tray.Tests 1096/1096.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Auto-approve own-node pairing on operator-side broadcast + surface pending state
Bug: on a fresh install where the device is already paired, the Windows node
sub-pairing (a separate gateway-side table, /home/openclaw/.openclaw/nodes/
paired.json) can sit empty indefinitely. The node connects, advertises its
caps/commands in the connect frame, gets into the gateway's pending list — but
the gateway returns caps:[], commands:[] in node.list responses until the
node-sub-pairing is approved. Result: tray menu shows the node Connected
with "Capabilities (0) · Commands (0)", agent capabilities silently inert.
Why the existing auto-approve didn't fire: GatewayConnectionManager's
OnNodePairingStatusChanged only triggers on the node-side
WindowsNodeClient.PairingStatusChanged event, which fires Pending only when
the node-side connection itself is pending. With the device already paired,
the node-side client sees PairingStatus.Paired and never raises Pending — the
only signal for the missing node-sub-pairing is the operator-side
NodePairListUpdated broadcast.
Fix (3 parts):
1. GatewayConnectionManager: subscribe NodePairListUpdated on every new
operator client. When a pending entry's NodeId matches our own
_nodeConnector.NodeDeviceId AND we have operator.admin/pairing scope,
call NodePairApproveAsync. Reuses _autoApproveInFlight CAS guard and
_lastAutoApprovedRequestId dedup. After approval, restart the node
connection so caps propagate.
2. InstancesPage: caution-yellow banner at top of the Nodes page when any
node or device pair is pending. Subtext is contextual ("This node is
connected but its capabilities won't activate…" when our own deviceId
is the pending one). Button navigates to ConnectionPage where the
existing per-row approve/reject UI lives.
3. ConnectionPage: Initialize() now eagerly calls RequestNodePairListAsync
+ RequestDevicePairListAsync, and pushes any already-loaded AppState
pending lists into the banner immediately, so the user sees pending
state on first navigation instead of waiting for the gateway's next
broadcast.
Tests: 4 new cases in NodePairAutoApproveTests cover own-node approve,
other-node ignored, missing scope, and dedup-on-rebroadcast. All 228
connection tests pass; Shared 1782; Tray 1096.
Live verified end-to-end: after launching the new build against a WSL
gateway where the node was stuck at caps:[], the operator-side
auto-approve fired and node.list now reports caps:[app, browser, camera,
canvas, device, location, screen, system] and 11 commands.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address Hanselman review of auto-approve path
Fixes from dual-model adversarial review of fa421a3:
H1 — Spin loop on approval failure: _lastAutoApprovedRequestId was set
only inside if (approved). A failed approve (transient or policy
reject) meant every subsequent NodePairListUpdated re-broadcast retried
the same id forever. Now always recorded in finally, with a re-check
against the current generation so a reconnect-during-await doesn't
overwrite the new-generation null with a stale id. Same fix applied to
the pre-existing node-side path.
H2 — �reak after first attempt stranded second own-node pending: on
exception/rejection the next own-pending in the same snapshot was never
attempted (gateway might not re-broadcast if approve frame round-tripped
and was rejected mid-flight). Now continue to scan all own-pending
entries, then perform a single post-approval reconnect for the
last-approved id (rather than per-entry, which would race with itself).
H3 — CAS guard held across Task.Delay + StartNodeConnectionAsync (5–30s
on WSL cold-start), starving unrelated approvals: moved the
_autoApproveInFlight clear into a inally scoped JUST to the approve
RPC. Post-approve reconnect runs outside the guard. Same on both paths.
M1 — Stale _lastAutoApprovedRequestId write across generations: now
re-checks _generation == gen before writing so a disconnect/reconnect
during the approve await doesn't blow away the new generation's clean
slate.
M4 — Reflection-based test event raiser: added
[InternalsVisibleTo("OpenClaw.Connection.Tests")] +
internal RaiseNodePairListUpdatedForTests on OpenClawGatewayClient.
Test helper FireNodePairListUpdated now delegates to that instead of
poking the private event backing field via reflection (which silently
breaks the moment events grow explicit add/remove blocks).
M3 — Eager refresh swallowed exceptions silently: ConnectionPage and
InstancesPage Task.Run blocks now log via Services.Logger.Warn instead
of swallowing, matching the comment's stated intent.
Validation: Connection 228 / Shared 1782 / Tray 1096, all pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix node auto-approve retry dedupe
Only record a node pairing request as deduped after an actual approve attempt. This keeps the operator-side fallback able to approve the same request once the operator client is ready or gains approval scope.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
0 commit comments