mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
Respect source channel for agent event surfacing (#36030)
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
isMarkdownCapableMessageChannel,
|
||||
resolveMessageChannel,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import { stripHeartbeatToken } from "../heartbeat.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import type { VerboseLevel } from "../thinking.js";
|
||||
@@ -113,11 +114,17 @@ export async function runAgentTurnWithFallback(params: {
|
||||
didNotifyAgentRunStart = true;
|
||||
params.opts?.onAgentRunStart?.(runId);
|
||||
};
|
||||
const shouldSurfaceToControlUi = isInternalMessageChannel(
|
||||
params.followupRun.run.messageProvider ??
|
||||
params.sessionCtx.Surface ??
|
||||
params.sessionCtx.Provider,
|
||||
);
|
||||
if (params.sessionKey) {
|
||||
registerAgentRunContext(runId, {
|
||||
sessionKey: params.sessionKey,
|
||||
verboseLevel: params.resolvedVerboseLevel,
|
||||
isHeartbeat: params.isHeartbeat,
|
||||
isControlUiVisible: shouldSurfaceToControlUi,
|
||||
});
|
||||
}
|
||||
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { TypingMode } from "../../config/types.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import { stripHeartbeatToken } from "../heartbeat.js";
|
||||
import type { OriginatingChannelType } from "../templating.js";
|
||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
@@ -131,10 +132,17 @@ export function createFollowupRunner(params: {
|
||||
return async (queued: FollowupRun) => {
|
||||
try {
|
||||
const runId = crypto.randomUUID();
|
||||
const shouldSurfaceToControlUi = isInternalMessageChannel(
|
||||
resolveOriginMessageProvider({
|
||||
originatingChannel: queued.originatingChannel,
|
||||
provider: queued.run.messageProvider,
|
||||
}),
|
||||
);
|
||||
if (queued.run.sessionKey) {
|
||||
registerAgentRunContext(runId, {
|
||||
sessionKey: queued.run.sessionKey,
|
||||
verboseLevel: queued.run.verboseLevel,
|
||||
isControlUiVisible: shouldSurfaceToControlUi,
|
||||
});
|
||||
}
|
||||
let autoCompactionCompleted = false;
|
||||
|
||||
@@ -612,6 +612,29 @@ describe("agent event handler", () => {
|
||||
expect(nodePayload.runId).toBe("run-fallback-client");
|
||||
});
|
||||
|
||||
it("suppresses chat and node session events for non-control-UI-visible runs", () => {
|
||||
const { broadcast, nodeSendToSession, handler } = createHarness({
|
||||
resolveSessionKeyForRun: () => "session-hidden",
|
||||
});
|
||||
registerAgentRunContext("run-hidden", {
|
||||
sessionKey: "session-hidden",
|
||||
isControlUiVisible: false,
|
||||
verboseLevel: "off",
|
||||
});
|
||||
|
||||
handler({
|
||||
runId: "run-hidden",
|
||||
seq: 1,
|
||||
stream: "assistant",
|
||||
ts: Date.now(),
|
||||
data: { text: "Reply from imessage" },
|
||||
});
|
||||
emitLifecycleEnd(handler, "run-hidden", 2);
|
||||
|
||||
expect(chatBroadcastCalls(broadcast)).toHaveLength(0);
|
||||
expect(nodeSendToSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses agent event sessionKey when run-context lookup cannot resolve", () => {
|
||||
const { broadcast, handler } = createHarness({
|
||||
resolveSessionKeyForRun: () => undefined,
|
||||
|
||||
@@ -502,6 +502,7 @@ export function createAgentEventHandler({
|
||||
const chatLink = chatRunState.registry.peek(evt.runId);
|
||||
const eventSessionKey =
|
||||
typeof evt.sessionKey === "string" && evt.sessionKey.trim() ? evt.sessionKey : undefined;
|
||||
const isControlUiVisible = getAgentRunContext(evt.runId)?.isControlUiVisible ?? true;
|
||||
const sessionKey =
|
||||
chatLink?.sessionKey ?? eventSessionKey ?? resolveSessionKeyForRun(evt.runId);
|
||||
const clientRunId = chatLink?.clientRunId ?? evt.runId;
|
||||
@@ -556,7 +557,7 @@ export function createAgentEventHandler({
|
||||
const lifecyclePhase =
|
||||
evt.stream === "lifecycle" && typeof evt.data?.phase === "string" ? evt.data.phase : null;
|
||||
|
||||
if (sessionKey) {
|
||||
if (isControlUiVisible && sessionKey) {
|
||||
// Send tool events to node/channel subscribers only when verbose is enabled;
|
||||
// WS clients already received the event above via broadcastToConnIds.
|
||||
if (!isToolEvent || toolVerbose !== "off") {
|
||||
|
||||
@@ -61,4 +61,26 @@ describe("agent-events sequencing", () => {
|
||||
|
||||
expect(phases).toEqual(["start", "end"]);
|
||||
});
|
||||
|
||||
test("omits sessionKey for runs hidden from Control UI", async () => {
|
||||
resetAgentRunContextForTest();
|
||||
registerAgentRunContext("run-hidden", {
|
||||
sessionKey: "session-imessage",
|
||||
isControlUiVisible: false,
|
||||
});
|
||||
|
||||
let receivedSessionKey: string | undefined;
|
||||
const stop = onAgentEvent((evt) => {
|
||||
receivedSessionKey = evt.sessionKey;
|
||||
});
|
||||
emitAgentEvent({
|
||||
runId: "run-hidden",
|
||||
stream: "assistant",
|
||||
data: { text: "hi" },
|
||||
sessionKey: "session-imessage",
|
||||
});
|
||||
stop();
|
||||
|
||||
expect(receivedSessionKey).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,8 @@ export type AgentRunContext = {
|
||||
sessionKey?: string;
|
||||
verboseLevel?: VerboseLevel;
|
||||
isHeartbeat?: boolean;
|
||||
/** Whether control UI clients should receive chat/agent updates for this run. */
|
||||
isControlUiVisible?: boolean;
|
||||
};
|
||||
|
||||
// Keep per-run counters so streams stay strictly monotonic per runId.
|
||||
@@ -37,6 +39,9 @@ export function registerAgentRunContext(runId: string, context: AgentRunContext)
|
||||
if (context.verboseLevel && existing.verboseLevel !== context.verboseLevel) {
|
||||
existing.verboseLevel = context.verboseLevel;
|
||||
}
|
||||
if (context.isControlUiVisible !== undefined) {
|
||||
existing.isControlUiVisible = context.isControlUiVisible;
|
||||
}
|
||||
if (context.isHeartbeat !== undefined && existing.isHeartbeat !== context.isHeartbeat) {
|
||||
existing.isHeartbeat = context.isHeartbeat;
|
||||
}
|
||||
@@ -58,10 +63,10 @@ export function emitAgentEvent(event: Omit<AgentEventPayload, "seq" | "ts">) {
|
||||
const nextSeq = (seqByRun.get(event.runId) ?? 0) + 1;
|
||||
seqByRun.set(event.runId, nextSeq);
|
||||
const context = runContextById.get(event.runId);
|
||||
const sessionKey =
|
||||
typeof event.sessionKey === "string" && event.sessionKey.trim()
|
||||
? event.sessionKey
|
||||
: context?.sessionKey;
|
||||
const isControlUiVisible = context?.isControlUiVisible ?? true;
|
||||
const eventSessionKey =
|
||||
typeof event.sessionKey === "string" && event.sessionKey.trim() ? event.sessionKey : undefined;
|
||||
const sessionKey = isControlUiVisible ? (eventSessionKey ?? context?.sessionKey) : undefined;
|
||||
const enriched: AgentEventPayload = {
|
||||
...event,
|
||||
sessionKey,
|
||||
|
||||
Reference in New Issue
Block a user