mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
@@ -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" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user