Skip to content

Commit 9a9c8bc

Browse files
author
octane0411
committed
Respect source channel for agent event surfacing
1 parent 2c8ee59 commit 9a9c8bc

File tree

6 files changed

+71
-5
lines changed

6 files changed

+71
-5
lines changed

src/auto-reply/reply/agent-runner-execution.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
isMarkdownCapableMessageChannel,
2727
resolveMessageChannel,
2828
} from "../../utils/message-channel.js";
29+
import { isInternalMessageChannel } from "../../utils/message-channel.js";
2930
import { stripHeartbeatToken } from "../heartbeat.js";
3031
import type { TemplateContext } from "../templating.js";
3132
import type { VerboseLevel } from "../thinking.js";
@@ -113,11 +114,17 @@ export async function runAgentTurnWithFallback(params: {
113114
didNotifyAgentRunStart = true;
114115
params.opts?.onAgentRunStart?.(runId);
115116
};
117+
const shouldSurfaceToControlUi = isInternalMessageChannel(
118+
params.followupRun.run.messageProvider ??
119+
params.sessionCtx.Surface ??
120+
params.sessionCtx.Provider,
121+
);
116122
if (params.sessionKey) {
117123
registerAgentRunContext(runId, {
118124
sessionKey: params.sessionKey,
119125
verboseLevel: params.resolvedVerboseLevel,
120126
isHeartbeat: params.isHeartbeat,
127+
isControlUiVisible: shouldSurfaceToControlUi,
121128
});
122129
}
123130
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;

src/auto-reply/reply/followup-runner.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { TypingMode } from "../../config/types.js";
1010
import { logVerbose } from "../../globals.js";
1111
import { registerAgentRunContext } from "../../infra/agent-events.js";
1212
import { defaultRuntime } from "../../runtime.js";
13+
import { isInternalMessageChannel } from "../../utils/message-channel.js";
1314
import { stripHeartbeatToken } from "../heartbeat.js";
1415
import type { OriginatingChannelType } from "../templating.js";
1516
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
@@ -131,10 +132,17 @@ export function createFollowupRunner(params: {
131132
return async (queued: FollowupRun) => {
132133
try {
133134
const runId = crypto.randomUUID();
135+
const shouldSurfaceToControlUi = isInternalMessageChannel(
136+
resolveOriginMessageProvider({
137+
originatingChannel: queued.originatingChannel,
138+
provider: queued.run.messageProvider,
139+
}),
140+
);
134141
if (queued.run.sessionKey) {
135142
registerAgentRunContext(runId, {
136143
sessionKey: queued.run.sessionKey,
137144
verboseLevel: queued.run.verboseLevel,
145+
isControlUiVisible: shouldSurfaceToControlUi,
138146
});
139147
}
140148
let autoCompactionCompleted = false;

src/gateway/server-chat.agent-events.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,29 @@ describe("agent event handler", () => {
520520
expect(nodePayload.runId).toBe("run-fallback-client");
521521
});
522522

523+
it("suppresses chat and node session events for non-control-UI-visible runs", () => {
524+
const { broadcast, nodeSendToSession, handler } = createHarness({
525+
resolveSessionKeyForRun: () => "session-hidden",
526+
});
527+
registerAgentRunContext("run-hidden", {
528+
sessionKey: "session-hidden",
529+
isControlUiVisible: false,
530+
verboseLevel: "off",
531+
});
532+
533+
handler({
534+
runId: "run-hidden",
535+
seq: 1,
536+
stream: "assistant",
537+
ts: Date.now(),
538+
data: { text: "Reply from imessage" },
539+
});
540+
emitLifecycleEnd(handler, "run-hidden", 2);
541+
542+
expect(chatBroadcastCalls(broadcast)).toHaveLength(0);
543+
expect(nodeSendToSession).not.toHaveBeenCalled();
544+
});
545+
523546
it("uses agent event sessionKey when run-context lookup cannot resolve", () => {
524547
const { broadcast, handler } = createHarness({
525548
resolveSessionKeyForRun: () => undefined,

src/gateway/server-chat.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ export function createAgentEventHandler({
451451
const chatLink = chatRunState.registry.peek(evt.runId);
452452
const eventSessionKey =
453453
typeof evt.sessionKey === "string" && evt.sessionKey.trim() ? evt.sessionKey : undefined;
454+
const isControlUiVisible = getAgentRunContext(evt.runId)?.isControlUiVisible ?? true;
454455
const sessionKey =
455456
chatLink?.sessionKey ?? eventSessionKey ?? resolveSessionKeyForRun(evt.runId);
456457
const clientRunId = chatLink?.clientRunId ?? evt.runId;
@@ -505,7 +506,7 @@ export function createAgentEventHandler({
505506
const lifecyclePhase =
506507
evt.stream === "lifecycle" && typeof evt.data?.phase === "string" ? evt.data.phase : null;
507508

508-
if (sessionKey) {
509+
if (isControlUiVisible && sessionKey) {
509510
// Send tool events to node/channel subscribers only when verbose is enabled;
510511
// WS clients already received the event above via broadcastToConnIds.
511512
if (!isToolEvent || toolVerbose !== "off") {

src/infra/agent-events.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,26 @@ describe("agent-events sequencing", () => {
6161

6262
expect(phases).toEqual(["start", "end"]);
6363
});
64+
65+
test("omits sessionKey for runs hidden from Control UI", async () => {
66+
resetAgentRunContextForTest();
67+
registerAgentRunContext("run-hidden", {
68+
sessionKey: "session-imessage",
69+
isControlUiVisible: false,
70+
});
71+
72+
let receivedSessionKey: string | undefined;
73+
const stop = onAgentEvent((evt) => {
74+
receivedSessionKey = evt.sessionKey;
75+
});
76+
emitAgentEvent({
77+
runId: "run-hidden",
78+
stream: "assistant",
79+
data: { text: "hi" },
80+
sessionKey: "session-imessage",
81+
});
82+
stop();
83+
84+
expect(receivedSessionKey).toBeUndefined();
85+
});
6486
});

src/infra/agent-events.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export type AgentRunContext = {
1515
sessionKey?: string;
1616
verboseLevel?: VerboseLevel;
1717
isHeartbeat?: boolean;
18+
/** Whether control UI clients should receive chat/agent updates for this run. */
19+
isControlUiVisible?: boolean;
1820
};
1921

2022
// Keep per-run counters so streams stay strictly monotonic per runId.
@@ -37,6 +39,9 @@ export function registerAgentRunContext(runId: string, context: AgentRunContext)
3739
if (context.verboseLevel && existing.verboseLevel !== context.verboseLevel) {
3840
existing.verboseLevel = context.verboseLevel;
3941
}
42+
if (context.isControlUiVisible !== undefined) {
43+
existing.isControlUiVisible = context.isControlUiVisible;
44+
}
4045
if (context.isHeartbeat !== undefined && existing.isHeartbeat !== context.isHeartbeat) {
4146
existing.isHeartbeat = context.isHeartbeat;
4247
}
@@ -58,10 +63,10 @@ export function emitAgentEvent(event: Omit<AgentEventPayload, "seq" | "ts">) {
5863
const nextSeq = (seqByRun.get(event.runId) ?? 0) + 1;
5964
seqByRun.set(event.runId, nextSeq);
6065
const context = runContextById.get(event.runId);
61-
const sessionKey =
62-
typeof event.sessionKey === "string" && event.sessionKey.trim()
63-
? event.sessionKey
64-
: context?.sessionKey;
66+
const isControlUiVisible = context?.isControlUiVisible ?? true;
67+
const eventSessionKey =
68+
typeof event.sessionKey === "string" && event.sessionKey.trim() ? event.sessionKey : undefined;
69+
const sessionKey = isControlUiVisible ? (eventSessionKey ?? context?.sessionKey) : undefined;
6570
const enriched: AgentEventPayload = {
6671
...event,
6772
sessionKey,

0 commit comments

Comments
 (0)