fix(browser): wake page shares when heartbeat is disabled (#116790)

* fix(browser): wake page shares with heartbeat disabled

* fix(browser): preserve page-share session agent
This commit is contained in:
Vincent Koc
2026-08-03 12:30:36 +08:00
committed by GitHub
parent 72ac91b1a9
commit a2f3c01068
3 changed files with 109 additions and 42 deletions
@@ -5,15 +5,24 @@ import {
setPageShareSink,
} from "./page-share.js";
function createSink() {
function createSink(sessionKey = "agent:main:main") {
const enqueueSystemEvent = vi.fn();
const requestHeartbeat = vi.fn();
const resolveDefaultAgentId = vi.fn(() => "main");
const resolveMainSessionKey = vi.fn(() => sessionKey);
const sink = {
enqueueSystemEvent,
requestHeartbeat,
resolveMainSessionKey: () => "agent:main:main",
resolveDefaultAgentId,
resolveMainSessionKey,
};
return {
enqueueSystemEvent,
requestHeartbeat,
resolveDefaultAgentId,
resolveMainSessionKey,
sink,
};
return { enqueueSystemEvent, requestHeartbeat, sink };
}
afterEach(() => {
@@ -21,8 +30,14 @@ afterEach(() => {
});
describe("page share delivery", () => {
it("formats metadata, keeps the note trusted, and wraps selected page text", async () => {
const { enqueueSystemEvent, requestHeartbeat, sink } = createSink();
it("formats metadata, keeps the note trusted, and wakes a scoped session", async () => {
const {
enqueueSystemEvent,
requestHeartbeat,
resolveDefaultAgentId,
resolveMainSessionKey,
sink,
} = createSink("agent:ops:main");
setPageShareSink(sink);
await deliverPageShare({
@@ -34,8 +49,10 @@ describe("page share delivery", () => {
});
expect(enqueueSystemEvent).toHaveBeenCalledOnce();
expect(resolveDefaultAgentId).not.toHaveBeenCalled();
expect(resolveMainSessionKey).toHaveBeenCalledOnce();
const [text, options] = enqueueSystemEvent.mock.calls[0] as [string, { sessionKey: string }];
expect(options).toEqual({ sessionKey: "agent:main:main" });
expect(options).toEqual({ sessionKey: "agent:ops:main" });
expect(text).toContain(
"Page shared from the OpenClaw Chrome extension.\nNote: Summarize for me",
);
@@ -50,10 +67,33 @@ describe("page share delivery", () => {
const boundaryStart = text.indexOf("<<<EXTERNAL_UNTRUSTED_CONTENT");
expect(text.indexOf("Title: Example article")).toBeGreaterThan(boundaryStart);
expect(text.indexOf("URL: https://example.com/article")).toBeGreaterThan(boundaryStart);
expect(requestHeartbeat).toHaveBeenCalledWith({
source: "other",
expect(requestHeartbeat).toHaveBeenCalledExactlyOnceWith({
source: "notifications-event",
intent: "immediate",
reason: "browser-page-share",
reason: "wake",
sessionKey: "agent:ops:main",
heartbeat: { target: "last" },
});
});
it("supplies the default agent for the global session", async () => {
const { requestHeartbeat, resolveDefaultAgentId, sink } = createSink("global");
setPageShareSink(sink);
await deliverPageShare({
url: "https://example.com",
title: "Example",
content: "full page content",
});
expect(resolveDefaultAgentId).toHaveBeenCalledOnce();
expect(requestHeartbeat).toHaveBeenCalledExactlyOnceWith({
source: "notifications-event",
intent: "immediate",
reason: "wake",
agentId: "main",
sessionKey: "global",
heartbeat: { target: "last" },
});
});
@@ -1,9 +1,11 @@
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
import { requestHeartbeat } from "openclaw/plugin-sdk/heartbeat-runtime";
import { wrapExternalContent } from "openclaw/plugin-sdk/security-runtime";
import {
enqueueSystemEvent,
resolveMainSessionKeyFromConfig,
} from "openclaw/plugin-sdk/system-event-runtime";
import { getRuntimeConfig } from "../../sdk-config.js";
import type { PageSharePayload } from "./relay-protocol.js";
export const PAGE_SHARE_GATEWAY_REQUIRED_ERROR =
@@ -11,7 +13,15 @@ export const PAGE_SHARE_GATEWAY_REQUIRED_ERROR =
type PageShareSink = {
enqueueSystemEvent(text: string, opts: { sessionKey: string }): unknown;
requestHeartbeat(opts: { source: "other"; intent: "immediate"; reason: string }): unknown;
requestHeartbeat(opts: {
source: "notifications-event";
intent: "immediate";
reason: "wake";
agentId?: string;
sessionKey: string;
heartbeat: { target: "last" };
}): unknown;
resolveDefaultAgentId(): string;
resolveMainSessionKey(): string;
};
@@ -27,6 +37,7 @@ export function createGatewayPageShareSink(): PageShareSink {
return {
enqueueSystemEvent,
requestHeartbeat,
resolveDefaultAgentId: () => resolveDefaultAgentId(getRuntimeConfig()),
resolveMainSessionKey: resolveMainSessionKeyFromConfig,
};
}
@@ -51,10 +62,14 @@ export async function deliverPageShare(payload: PageSharePayload): Promise<void>
].join("\n");
const text = `${header}\n\n${wrapped}`;
await sink.enqueueSystemEvent(text, { sessionKey: sink.resolveMainSessionKey() });
const sessionKey = sink.resolveMainSessionKey();
await sink.enqueueSystemEvent(text, { sessionKey });
await sink.requestHeartbeat({
source: "other",
source: "notifications-event",
intent: "immediate",
reason: "browser-page-share",
reason: "wake",
...(sessionKey === "global" ? { agentId: sink.resolveDefaultAgentId() } : {}),
sessionKey,
heartbeat: { target: "last" },
});
}
+41 -29
View File
@@ -986,37 +986,49 @@ describe("startHeartbeatRunner", () => {
runner.stop();
});
it("runs a targeted notification wake for an agent without a heartbeat schedule", async () => {
it.each([
{
name: "an agent without a heartbeat schedule",
cfg: {
agents: { list: [{ id: "main", heartbeat: { every: "30m" } }, { id: "ops" }] },
} as OpenClawConfig,
agentId: "ops",
sessionKey: "agent:ops:main",
heartbeat: { target: "last" },
},
{
name: "the global main session when periodic heartbeats are disabled",
cfg: {
agents: { defaults: { heartbeat: { every: "0m" } }, list: [{ id: "main" }] },
session: { scope: "global" },
} as OpenClawConfig,
agentId: "main",
sessionKey: "global",
heartbeat: { every: "0m", target: "last" },
},
])("runs one targeted notification wake for $name", async (testCase) => {
useFakeHeartbeatTime();
const runSpy = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 });
const runner = startHeartbeatRunner({
cfg: {
agents: {
list: [{ id: "main", heartbeat: { every: "30m" } }, { id: "ops" }],
},
} as OpenClawConfig,
runOnce: runSpy,
stableSchedulerSeed: TEST_SCHEDULER_SEED,
});
requestHeartbeat({
source: "notifications-event",
intent: "immediate",
reason: "wake",
sessionKey: "agent:ops:main",
heartbeat: { target: "last" },
coalesceMs: 0,
});
await vi.advanceTimersByTimeAsync(1);
expect(runSpy).toHaveBeenCalledTimes(1);
expectRunCallFields(runSpy, 0, {
agentId: "ops",
source: "notifications-event",
intent: "immediate",
reason: "wake",
sessionKey: "agent:ops:main",
heartbeat: { target: "last" },
const runner = await expectWakeDispatch({
cfg: testCase.cfg,
runSpy,
wake: {
source: "notifications-event",
intent: "immediate",
reason: "wake",
...(testCase.sessionKey === "global" ? { agentId: testCase.agentId } : {}),
sessionKey: testCase.sessionKey,
heartbeat: { target: "last" },
coalesceMs: 0,
},
expectedCall: {
agentId: testCase.agentId,
source: "notifications-event",
intent: "immediate",
reason: "wake",
sessionKey: testCase.sessionKey,
heartbeat: testCase.heartbeat,
},
});
runner.stop();
});