Skip to content

Commit 37aaa82

Browse files
committed
fix(cloud-agent): inject prContext into codex prompts
1 parent 49e79c2 commit 37aaa82

2 files changed

Lines changed: 63 additions & 1 deletion

File tree

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,46 @@ describe("CodexAcpAgent", () => {
127127
).toBe("read-only");
128128
});
129129

130+
it("prepends _meta.prContext to the forwarded prompt but not to the broadcast", async () => {
131+
const { agent, client } = createAgent();
132+
mockCodexConnection.newSession.mockResolvedValue({
133+
sessionId: "session-1",
134+
modes: { currentModeId: "auto", availableModes: [] },
135+
configOptions: [],
136+
} satisfies Partial<NewSessionResponse>);
137+
await agent.newSession({
138+
cwd: process.cwd(),
139+
} as never);
140+
141+
mockCodexConnection.prompt.mockResolvedValue({ stopReason: "end_turn" });
142+
143+
await agent.prompt({
144+
sessionId: "session-1",
145+
prompt: [{ type: "text", text: "ship the fix" }],
146+
_meta: { prContext: "PR #123 is open; review before editing." },
147+
} as never);
148+
149+
// codex-acp receives the PR context prepended as a text block.
150+
expect(mockCodexConnection.prompt).toHaveBeenCalledWith(
151+
expect.objectContaining({
152+
prompt: [
153+
{ type: "text", text: "PR #123 is open; review before editing." },
154+
{ type: "text", text: "ship the fix" },
155+
],
156+
}),
157+
);
158+
// The broadcast shows only the real user turn — the prContext prefix
159+
// is internal routing and should not render as a user message.
160+
expect(client.sessionUpdate).toHaveBeenCalledTimes(1);
161+
expect(client.sessionUpdate).toHaveBeenCalledWith({
162+
sessionId: "session-1",
163+
update: {
164+
sessionUpdate: "user_message_chunk",
165+
content: { type: "text", text: "ship the fix" },
166+
},
167+
});
168+
});
169+
130170
it("broadcasts user prompt as user_message_chunk before delegating to codex-acp", async () => {
131171
const { agent, client } = createAgent();
132172
// Seed an active session so prompt() has the state it expects.

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,24 @@ function toCodexPermissionMode(mode?: string): PermissionMode {
9393
return "auto";
9494
}
9595

96+
/**
97+
* Prepend `_meta.prContext` (set by the agent-server on Slack-originated
98+
* follow-up runs) to the prompt as a text block, mirroring Claude's
99+
* `promptToClaude` behavior. Without this, codex cloud runs lose the
100+
* PR-review context that follow-up flows rely on.
101+
*/
102+
function prependPrContext(params: PromptRequest): PromptRequest {
103+
const prContext = (params._meta as Record<string, unknown> | undefined)
104+
?.prContext;
105+
if (typeof prContext !== "string" || prContext.length === 0) {
106+
return params;
107+
}
108+
return {
109+
...params,
110+
prompt: [{ type: "text", text: prContext }, ...params.prompt],
111+
};
112+
}
113+
96114
const CODEX_NATIVE_MODE: Record<CodeExecutionMode, CodexNativeMode> = {
97115
default: "auto",
98116
acceptEdits: "auto",
@@ -373,9 +391,13 @@ export class CodexAcpAgent extends BaseAcpAgent {
373391
// channel, so without this broadcast the tapped stream (persisted to S3
374392
// and rendered by the PostHog web UI) never sees a user turn and only
375393
// the assistant reply shows up. Mirrors ClaudeAcpAgent.broadcastUserMessage.
394+
// The original params (no _meta.prContext prefix) is broadcast so the
395+
// injected PR context is not rendered as a user message.
376396
await this.broadcastUserMessage(params);
377397

378-
const response = await this.codexConnection.prompt(params);
398+
const response = await this.codexConnection.prompt(
399+
prependPrContext(params),
400+
);
379401

380402
// Usage is already accumulated via sessionUpdate notifications in
381403
// codex-client.ts. Do NOT also add response.usage here or tokens

0 commit comments

Comments
 (0)