openclaw - 💡(How to fix) Fix [Bug]: Node exec approval replay fails with APPROVAL_CLIENT_MISMATCH for webchat (turnSourceTo null)

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

After approving node exec from webchat, the backend gateway-client replay always fails with APPROVAL_CLIENT_MISMATCH. Root cause: canBridgeNoDeviceChatApprovalFromBackend() uses matchesRequiredString for turnSourceTo, which returns false when the value is null. WebChat has no "to" recipient, so this field is always null. The function already correctly uses matchesOptionalString for turnSourceAccountId and turnSourceThreadId, but turnSourceTo was missed.

Root Cause

canBridgeNoDeviceChatApprovalFromBackend() in server-methods-CxcGaVP0.js (~line 5836):

return matchesRequiredString({ expected: request.turnSourceChannel, // "webchat" ✓ actual: params.rawParams.turnSourceChannel, // "webchat" ✓ lowercase: true }) && matchesRequiredString({ expected: request.turnSourceTo, // null ← PROBLEM HERE actual: params.rawParams.turnSourceTo // null }) && matchesRequiredString({ expected: plan?.sessionKey ?? request.sessionKey, // "agent:main:main" ✓ actual: params.rawParams.sessionKey // "agent:main:main" ✓ }) && matchesOptionalString({ // ← correctly Optional expected: plan?.agentId ?? request.agentId, actual: params.rawParams.agentId }) && matchesOptionalString({ // ← correctly Optional expected: request.turnSourceAccountId, actual: params.rawParams.turnSourceAccountId }) && matchesOptionalString({ // ← correctly Optional expected: request.turnSourceThreadId, actual: params.rawParams.turnSourceThreadId })

matchesRequiredString implementation:

function matchesRequiredString(params) { const expected = normalizeComparableString(params.expected, { lowercase: params.lowercase }); if (!expected) return false; // null/empty → instant false return expected === normalizeComparableString(params.actual, { lowercase: params.lowercase }); }

WebChat has no "to" recipient concept, so request.turnSourceTo is always null → matchesRequiredString returns false → entire bridge function returns false → APPROVAL_CLIENT_MISMATCH.

Fix Action

Fix / Workaround

After applying this one-line patch, webchat node exec approval replay works correctly on multiple headless Linux nodes.


Temporary workaround: patch the dist file directly in the container to replace matchesRequiredString with matchesOptionalString for the turnSourceTo check, then restart Gateway.

Code Example

## Debug Trace

Added console.log before the APPROVAL_CLIENT_MISMATCH guard in server-methods-CxcGaVP0.js:

### Approval snapshot at guard:

{"snapshotDeviceId":null,"clientDeviceId":null,"requestedByConnId":"0d1c8e18-bd10-4465-85db-40632f41ab6f","clientConnId":"6354a36c-b0c3-4e7b-8a87-5ba64451c07d","requestedByDeviceTokenAuth":false,"requestedByClientId":"gateway-client","clientMode":"backend","turnSourceChannel":"webchat","rawTurnSourceChannel":"webchat"}

### Bridge function detailed output:

{"bridge2":false,"snapTo":null,"rawTo":null,"snapSession":"agent:main:main","rawSession":"agent:main:main","snapPlanSession":"agent:main:main","snapAgent":"main","rawAgent":"main","snapAccount":null,"rawAccount":null,"snapThread":null,"rawThread":null}

All fields match (channel, session, agent, account, thread) but bridge2=false because of turnSourceTo.

## Root Cause

canBridgeNoDeviceChatApprovalFromBackend() in server-methods-CxcGaVP0.js (~line 5836):

return matchesRequiredString({
    expected: request.turnSourceChannel,       // "webchat" ✓
    actual: params.rawParams.turnSourceChannel, // "webchat" ✓
    lowercase: true
}) && matchesRequiredString({
    expected: request.turnSourceTo,            // null ← PROBLEM HERE
    actual: params.rawParams.turnSourceTo      // null
}) && matchesRequiredString({
    expected: plan?.sessionKey ?? request.sessionKey,  // "agent:main:main" ✓
    actual: params.rawParams.sessionKey                // "agent:main:main" ✓
}) && matchesOptionalString({                  // ← correctly Optional
    expected: plan?.agentId ?? request.agentId,
    actual: params.rawParams.agentId
}) && matchesOptionalString({                  // ← correctly Optional
    expected: request.turnSourceAccountId,
    actual: params.rawParams.turnSourceAccountId
}) && matchesOptionalString({                  // ← correctly Optional
    expected: request.turnSourceThreadId,
    actual: params.rawParams.turnSourceThreadId
})

matchesRequiredString implementation:

function matchesRequiredString(params) {
    const expected = normalizeComparableString(params.expected, { lowercase: params.lowercase });
    if (!expected) return false;  // null/empty → instant false
    return expected === normalizeComparableString(params.actual, { lowercase: params.lowercase });
}

WebChat has no "to" recipient concept, so request.turnSourceTo is always null → matchesRequiredString returns false → entire bridge function returns falseAPPROVAL_CLIENT_MISMATCH.

## Fix (verified locally)

Change matchesRequiredString to matchesOptionalString for the turnSourceTo field:

// Before (broken):
matchesRequiredString({ expected: request.turnSourceTo, actual: params.rawParams.turnSourceTo })

// After (fixed):
matchesOptionalString({ expected: request.turnSourceTo, actual: params.rawParams.turnSourceTo })

matchesOptionalString returns true when expected is null (field not applicable), which is the correct behavior for channels without a "to" target.

After applying this one-line patch, webchat node exec approval replay works correctly on multiple headless Linux nodes.
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

After approving node exec from webchat, the backend gateway-client replay always fails with APPROVAL_CLIENT_MISMATCH. Root cause: canBridgeNoDeviceChatApprovalFromBackend() uses matchesRequiredString for turnSourceTo, which returns false when the value is null. WebChat has no "to" recipient, so this field is always null. The function already correctly uses matchesOptionalString for turnSourceAccountId and turnSourceThreadId, but turnSourceTo was missed.

Steps to reproduce

  1. Run Gateway 2026.5.12 with a paired headless Linux node (via openclaw node run)
  2. From webchat, trigger an exec command on the node (e.g. echo test)
  3. Approve the exec request in the webchat approval prompt
  4. Observe: command fails with invoke-failed / APPROVAL_CLIENT_MISMATCH

Expected behavior

After webchat approval, the backend gateway-client replay should succeed and the command should execute on the node. This worked correctly in 2026.4.22.

Actual behavior

canBridgeNoDeviceChatApprovalFromBackend() returns false because matchesRequiredString({ expected: null, actual: null }) returns false (treats null as "required field missing"). The approval is consumed but the node invoke is rejected.

Gateway log: [ws] ⇄ res ✓ exec.approval.waitDecision 1114ms conn=173f01ae…98a6 id=60d464b3…67af [ws] ⇄ res ✗ node.invoke 4ms errorCode=INVALID_REQUEST errorMessage=approval id not valid for this client conn=6354a36c…c07d id=e52a9060…0ef2

OpenClaw version

2026.5.5 – 2026.5.12 (confirmed on both)

Operating system

Oracle Linux 8.10 (aarch64)

Install method

docker

Model

github-copilot/claude-opus-4.6

Provider / routing chain

github-copilot (not relevant — issue is in Gateway approval logic)

Additional provider/model setup details

N/A. Issue is purely in the Gateway node exec approval replay path, unrelated to model or provider.

Logs, screenshots, and evidence

## Debug Trace

Added console.log before the APPROVAL_CLIENT_MISMATCH guard in server-methods-CxcGaVP0.js:

### Approval snapshot at guard:

{"snapshotDeviceId":null,"clientDeviceId":null,"requestedByConnId":"0d1c8e18-bd10-4465-85db-40632f41ab6f","clientConnId":"6354a36c-b0c3-4e7b-8a87-5ba64451c07d","requestedByDeviceTokenAuth":false,"requestedByClientId":"gateway-client","clientMode":"backend","turnSourceChannel":"webchat","rawTurnSourceChannel":"webchat"}

### Bridge function detailed output:

{"bridge2":false,"snapTo":null,"rawTo":null,"snapSession":"agent:main:main","rawSession":"agent:main:main","snapPlanSession":"agent:main:main","snapAgent":"main","rawAgent":"main","snapAccount":null,"rawAccount":null,"snapThread":null,"rawThread":null}

All fields match (channel, session, agent, account, thread) but bridge2=false because of turnSourceTo.

## Root Cause

canBridgeNoDeviceChatApprovalFromBackend() in server-methods-CxcGaVP0.js (~line 5836):

return matchesRequiredString({
    expected: request.turnSourceChannel,       // "webchat"    actual: params.rawParams.turnSourceChannel, // "webchat"    lowercase: true
}) && matchesRequiredString({
    expected: request.turnSourceTo,            // null ← PROBLEM HERE
    actual: params.rawParams.turnSourceTo      // null
}) && matchesRequiredString({
    expected: plan?.sessionKey ?? request.sessionKey,  // "agent:main:main"    actual: params.rawParams.sessionKey                // "agent:main:main"}) && matchesOptionalString({                  // ← correctly Optional
    expected: plan?.agentId ?? request.agentId,
    actual: params.rawParams.agentId
}) && matchesOptionalString({                  // ← correctly Optional
    expected: request.turnSourceAccountId,
    actual: params.rawParams.turnSourceAccountId
}) && matchesOptionalString({                  // ← correctly Optional
    expected: request.turnSourceThreadId,
    actual: params.rawParams.turnSourceThreadId
})

matchesRequiredString implementation:

function matchesRequiredString(params) {
    const expected = normalizeComparableString(params.expected, { lowercase: params.lowercase });
    if (!expected) return false;  // null/empty → instant false
    return expected === normalizeComparableString(params.actual, { lowercase: params.lowercase });
}

WebChat has no "to" recipient concept, so request.turnSourceTo is always null → matchesRequiredString returns false → entire bridge function returns false → APPROVAL_CLIENT_MISMATCH.

## Fix (verified locally)

Change matchesRequiredString to matchesOptionalString for the turnSourceTo field:

// Before (broken):
matchesRequiredString({ expected: request.turnSourceTo, actual: params.rawParams.turnSourceTo })

// After (fixed):
matchesOptionalString({ expected: request.turnSourceTo, actual: params.rawParams.turnSourceTo })

matchesOptionalString returns true when expected is null (field not applicable), which is the correct behavior for channels without a "to" target.

After applying this one-line patch, webchat node exec approval replay works correctly on multiple headless Linux nodes.

Impact and severity

  • Affected: All webchat users attempting node exec
  • Severity: High (completely blocks node exec from webchat)
  • Frequency: 100% reproducible
  • Consequence: No node commands can be executed from webchat; approval is consumed but command never runs

Additional information

Last known good version: 2026.4.22. Regression introduced in 2026.5.5.

Related: PR #78728 added the canBridgeNoDeviceChatApprovalFromBackend() function for chat-bound approval replay. The turnSourceTo field was incorrectly classified as required (matchesRequiredString) when it should be optional — same as turnSourceAccountId and turnSourceThreadId which already use matchesOptionalString in the same function.

Temporary workaround: patch the dist file directly in the container to replace matchesRequiredString with matchesOptionalString for the turnSourceTo check, then restart Gateway.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

After webchat approval, the backend gateway-client replay should succeed and the command should execute on the node. This worked correctly in 2026.4.22.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING