Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 49e79c2

Browse files
authored
fix(cloud-agent): broadcast user prompt on codex cloud runs (#1718)
## Problem Cloud tasks running on the codex adapter don't display the user's message in the PostHog web UI log. codex-acp doesn't echo `session/prompt` back as a `session/update`, so the tapped stream persisted to S3 never sees a user turn and only the agent reply shows up. Regressed by #1644 (first time codex became a cloud adapter — Claude had always broadcast). ## Changes Mirror `ClaudeAcpAgent.broadcastUserMessage` in `CodexAcpAgent.prompt`: emit one `user_message_chunk` session update per prompt chunk before delegating to `codexConnection.prompt`. ## How did you test this? - New unit test in `codex-agent.test.ts` asserting `client.sessionUpdate` is called with `user_message_chunk` for each chunk before `codexConnection.prompt` resolves. - Full `@posthog/agent` suite green.
1 parent ee63784 commit 49e79c2

2 files changed

Lines changed: 87 additions & 6 deletions

File tree

‎packages/agent/src/adapters/codex/codex-agent.test.ts‎

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,32 @@ describe("CodexAcpAgent", () => {
6060
vi.clearAllMocks();
6161
});
6262

63-
function createAgent(): CodexAcpAgent {
63+
function createAgent(overrides: Partial<AgentSideConnection> = {}): {
64+
agent: CodexAcpAgent;
65+
client: AgentSideConnection & {
66+
extNotification: ReturnType<typeof vi.fn>;
67+
sessionUpdate: ReturnType<typeof vi.fn>;
68+
};
69+
} {
6470
const client = {
6571
extNotification: vi.fn(),
66-
} as unknown as AgentSideConnection;
67-
68-
return new CodexAcpAgent(client, {
72+
sessionUpdate: vi.fn(),
73+
...overrides,
74+
} as unknown as AgentSideConnection & {
75+
extNotification: ReturnType<typeof vi.fn>;
76+
sessionUpdate: ReturnType<typeof vi.fn>;
77+
};
78+
79+
const agent = new CodexAcpAgent(client, {
6980
codexProcessOptions: {
7081
cwd: process.cwd(),
7182
},
7283
});
84+
return { agent, client };
7385
}
7486

7587
it("applies the requested initial mode for a new session", async () => {
76-
const agent = createAgent();
88+
const { agent } = createAgent();
7789
mockCodexConnection.newSession.mockResolvedValue({
7890
sessionId: "session-1",
7991
modes: { currentModeId: "auto", availableModes: [] },
@@ -96,7 +108,7 @@ describe("CodexAcpAgent", () => {
96108
});
97109

98110
it("preserves the live session mode when loading an existing session", async () => {
99-
const agent = createAgent();
111+
const { agent } = createAgent();
100112
mockCodexConnection.loadSession.mockResolvedValue({
101113
modes: { currentModeId: "read-only", availableModes: [] },
102114
configOptions: [],
@@ -114,4 +126,53 @@ describe("CodexAcpAgent", () => {
114126
.sessionState.permissionMode,
115127
).toBe("read-only");
116128
});
129+
130+
it("broadcasts user prompt as user_message_chunk before delegating to codex-acp", async () => {
131+
const { agent, client } = createAgent();
132+
// Seed an active session so prompt() has the state it expects.
133+
mockCodexConnection.newSession.mockResolvedValue({
134+
sessionId: "session-1",
135+
modes: { currentModeId: "auto", availableModes: [] },
136+
configOptions: [],
137+
} satisfies Partial<NewSessionResponse>);
138+
await agent.newSession({
139+
cwd: process.cwd(),
140+
} as never);
141+
142+
const callOrder: string[] = [];
143+
client.sessionUpdate.mockImplementation(async () => {
144+
callOrder.push("sessionUpdate");
145+
});
146+
mockCodexConnection.prompt.mockImplementation(async () => {
147+
callOrder.push("prompt");
148+
return { stopReason: "end_turn" };
149+
});
150+
151+
await agent.prompt({
152+
sessionId: "session-1",
153+
prompt: [
154+
{ type: "text", text: "first chunk" },
155+
{ type: "text", text: "second chunk" },
156+
],
157+
} as never);
158+
159+
expect(client.sessionUpdate).toHaveBeenCalledTimes(2);
160+
expect(client.sessionUpdate).toHaveBeenNthCalledWith(1, {
161+
sessionId: "session-1",
162+
update: {
163+
sessionUpdate: "user_message_chunk",
164+
content: { type: "text", text: "first chunk" },
165+
},
166+
});
167+
expect(client.sessionUpdate).toHaveBeenNthCalledWith(2, {
168+
sessionId: "session-1",
169+
update: {
170+
sessionUpdate: "user_message_chunk",
171+
content: { type: "text", text: "second chunk" },
172+
},
173+
});
174+
// Broadcast must land before the prompt reaches codex-acp so the user
175+
// turn is persisted even if the underlying prompt fails.
176+
expect(callOrder).toEqual(["sessionUpdate", "sessionUpdate", "prompt"]);
177+
});
117178
});

‎packages/agent/src/adapters/codex/codex-agent.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,12 @@ export class CodexAcpAgent extends BaseAcpAgent {
369369
this.session.interruptReason = undefined;
370370
resetUsage(this.sessionState);
371371

372+
// codex-acp does not echo the user prompt back on the agent→client
373+
// channel, so without this broadcast the tapped stream (persisted to S3
374+
// and rendered by the PostHog web UI) never sees a user turn and only
375+
// the assistant reply shows up. Mirrors ClaudeAcpAgent.broadcastUserMessage.
376+
await this.broadcastUserMessage(params);
377+
372378
const response = await this.codexConnection.prompt(params);
373379

374380
// Usage is already accumulated via sessionUpdate notifications in
@@ -417,6 +423,20 @@ export class CodexAcpAgent extends BaseAcpAgent {
417423
});
418424
}
419425

426+
private async broadcastUserMessage(params: PromptRequest): Promise<void> {
427+
for (const chunk of params.prompt) {
428+
const notification = {
429+
sessionId: params.sessionId,
430+
update: {
431+
sessionUpdate: "user_message_chunk" as const,
432+
content: chunk,
433+
},
434+
};
435+
await this.client.sessionUpdate(notification);
436+
this.appendNotification(params.sessionId, notification);
437+
}
438+
}
439+
420440
async setSessionMode(
421441
params: SetSessionModeRequest,
422442
): Promise<SetSessionModeResponse> {

0 commit comments

Comments
 (0)