Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ jobs:
go-whatsapp-security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Setup Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
with:
go-version: "1.26.5"
cache-dependency-path: src/whatsapp-whatsmeow-worker/go.sum
Expand All @@ -49,10 +49,10 @@ jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

Expand Down Expand Up @@ -102,10 +102,10 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name != 'schedule'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

Expand Down Expand Up @@ -152,10 +152,10 @@ jobs:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
timeout-minutes: 60
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

Expand Down Expand Up @@ -225,10 +225,10 @@ jobs:
needs: build-and-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

Expand Down Expand Up @@ -268,7 +268,7 @@ jobs:
permissions:
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c
Expand Down Expand Up @@ -299,10 +299,10 @@ jobs:
env:
OPENCLAW_PUBLIC_SMOKE: "1"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ jobs:
env:
PACKAGE_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ jobs:
rid: osx-arm64

steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

Expand Down Expand Up @@ -211,7 +211,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Download release artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
Expand Down
15 changes: 13 additions & 2 deletions src/OpenClaw.Channels/WebSocketChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,14 @@ await SendEnvelopeToStateAsync(
continue;
}

if (IsIgnorableClientEnvelope(parsed.Type))
continue;

var msg = new InboundMessage
{
ChannelId = ChannelId,
SenderId = clientId,
RequestCancellation = ct,
AuthenticatedUserId = authenticatedUserId,
SessionId = parsed.SessionId,
Type = parsed.Type,
Expand All @@ -142,8 +146,7 @@ await SendEnvelopeToStateAsync(
ValueJson = parsed.CanvasEnvelope?.ValueJson,
Sequence = parsed.CanvasEnvelope?.Sequence,
ApprovalId = parsed.ApprovalId,
Approved = parsed.Approved,
RequestCancellation = ct
Approved = parsed.Approved
};

if (OnMessageReceived is not null)
Expand Down Expand Up @@ -558,6 +561,11 @@ private static ParsedWsInbound TryParseClientEnvelope(string payload)
return new ParsedWsInbound(true, env.Type, "", env.SessionId, env.MessageId, env.ReplyToMessageId, env.ApprovalId, env.Approved);
}

if (env is not null && IsIgnorableClientEnvelope(env.Type))
{
return new ParsedWsInbound(true, env.Type, "", env.SessionId, env.MessageId, env.ReplyToMessageId, null, null);
}

if (env is not null && IsCanvasClientEnvelope(env.Type))
{
var text = env.Type switch
Expand Down Expand Up @@ -586,6 +594,9 @@ private static ParsedWsInbound TryParseClientEnvelope(string payload)
return new ParsedWsInbound(false, null, payload, null, null, null, null, null, null);
}

private static bool IsIgnorableClientEnvelope(string? type)
=> type is "heartbeat";

private async ValueTask HandleCanvasClientEnvelopeAsync(string clientId, WsClientEnvelope envelope, CancellationToken ct)
{
if (OnCanvasClientEnvelopeReceived is not null)
Expand Down
34 changes: 34 additions & 0 deletions src/OpenClaw.Gateway/wwwroot/webchat.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ let activeRawContent = "";
let isAwaitingResponse = false;
let reconnectAttempts = 0;
let reconnectTimer = null;
let heartbeatTimer = null;
let streamRenderTimer = null;
let liveResponseHasAudio = false;
let liveAudioQueue = [];
Expand Down Expand Up @@ -172,6 +173,11 @@ let pendingFileUrls = []; // non-image files queued for upload

const WEBCHAT_CONFIG = {
streamRenderDebounceMs: Math.max(20, Number(window.OPENCLAW_WEBCHAT_CONFIG?.streamRenderDebounceMs ?? 120)),
heartbeatIntervalMs: (() => {
const raw = Number(window.OPENCLAW_WEBCHAT_CONFIG?.heartbeatIntervalMs ?? 45000);
const safe = Number.isFinite(raw) ? raw : 45000;
return Math.min(2147483647, Math.max(1000, safe));
})(),
initialReconnectDelayMs: Math.max(250, Number(window.OPENCLAW_WEBCHAT_CONFIG?.initialReconnectDelayMs ?? 1000)),
maxReconnectDelayMs: Math.max(1000, Number(window.OPENCLAW_WEBCHAT_CONFIG?.maxReconnectDelayMs ?? 30000)),
reconnectBackoffFactor: Math.max(1.1, Number(window.OPENCLAW_WEBCHAT_CONFIG?.reconnectBackoffFactor ?? 2)),
Expand Down Expand Up @@ -946,6 +952,31 @@ function resetActiveResponse() {
activeRawContent = '';
}

function clearHeartbeatTimer() {
if (!heartbeatTimer) {
return;
}

clearInterval(heartbeatTimer);
heartbeatTimer = null;
}

function startHeartbeatLoop() {
clearHeartbeatTimer();

heartbeatTimer = setInterval(() => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return;
}

try {
ws.send(JSON.stringify({ type: 'heartbeat' }));
} catch (_) {
clearHeartbeatTimer();
}
}, WEBCHAT_CONFIG.heartbeatIntervalMs);
}

function createRow(type) {
const row = document.createElement('div');
row.className = `message-row ${type}-row`;
Expand Down Expand Up @@ -2406,6 +2437,7 @@ async function openDoctorDiagnostics(addToChat = false) {
}

function connect() {
clearHeartbeatTimer();
setConnectionState(reconnectAttempts > 0 ? 'reconnecting' : 'connecting');
appendSystem('Connecting to OpenClaw.NET Gateway...');
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
Expand All @@ -2427,6 +2459,7 @@ function connect() {
reconnectTimer = null;
}
dismissFirstRunCard();
startHeartbeatLoop();
refreshChatState();
appendSystem('Connected successfully.');
sendCanvasReady();
Expand Down Expand Up @@ -2566,6 +2599,7 @@ function connect() {
};

ws.onclose = (event) => {
clearHeartbeatTimer();
updateComposerAvailability();
resetActiveResponse();

Expand Down
50 changes: 50 additions & 0 deletions src/OpenClaw.Tests/WebSocketChannelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,35 @@ await channel.HandleConnectionAsync(
Assert.Equal("acct-123", received!.AuthenticatedUserId);
}

[Fact]
public async Task HandleConnectionAsync_ForwardsRequestCancellationToken()
{
var channel = new WebSocketChannel(new WebSocketConfig { MaxMessageBytes = 1024 });
var ws = new TestWebSocket();

ws.QueueReceiveText("hello");
ws.QueueClose();

InboundMessage? received = null;
channel.OnMessageReceived += (msg, _) =>
{
received = msg;
return ValueTask.CompletedTask;
};

using var requestCts = new CancellationTokenSource();
await channel.HandleConnectionAsync(
ws,
"client",
IPAddress.Loopback,
requestCts.Token,
authenticatedUserId: null);

Assert.NotNull(received);
Assert.Equal(requestCts.Token, received!.RequestCancellation);
Assert.True(received.RequestCancellation.CanBeCanceled);
}

[Fact]
public async Task HandleConnectionAsync_RoutesCanvasReadyWithoutUserMessage()
{
Expand Down Expand Up @@ -238,6 +267,27 @@ public async Task HandleConnectionAsync_RoutesCanvasReadyWithoutUserMessage()
Assert.False(messageObserved);
}

[Fact]
public async Task HandleConnectionAsync_IgnoresHeartbeatEnvelopeWithoutUserMessage()
{
var channel = new WebSocketChannel(new WebSocketConfig { MaxMessageBytes = 1024 });
var ws = new TestWebSocket();

ws.QueueReceiveText("""{"type":"heartbeat"}""");
ws.QueueClose();

var messageObserved = false;
channel.OnMessageReceived += (_, _) =>
{
messageObserved = true;
return ValueTask.CompletedTask;
};

await channel.HandleConnectionAsync(ws, "client", IPAddress.Loopback, TestContext.Current.CancellationToken);

Assert.False(messageObserved);
}

[Fact]
public async Task HandleConnectionAsync_RoutesCanvasAckAndResultsWithoutUserMessage()
{
Expand Down
Loading