Skip to content

Commit 674edb1

Browse files
committed
fix(codex): bound streamed plan snapshots
Generated-By: PostHog Code Task-Id: 7305b2f0-aa76-413e-b0ea-a2078250be66
1 parent 5b5e747 commit 674edb1

2 files changed

Lines changed: 101 additions & 3 deletions

File tree

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2992,6 +2992,64 @@ describe("CodexAppServerAgent", () => {
29922992
});
29932993
});
29942994

2995+
it("coalesces streamed plan snapshots in notification history", async () => {
2996+
const { agent, stub } = makePlanAgent({
2997+
outcome: { outcome: "selected", optionId: "reject_with_feedback" },
2998+
});
2999+
const { done } = await startPlanTurn(agent, stub);
3000+
3001+
stub.emit("item/plan/delta", { itemId: "p1", delta: "first" });
3002+
stub.emit("item/plan/delta", { itemId: "p1", delta: " second" });
3003+
stub.emit("item/plan/delta", { itemId: "p1", delta: " third" });
3004+
stub.emit("turn/completed", {
3005+
turn: { id: "turn_1", status: "completed" },
3006+
});
3007+
await done;
3008+
3009+
const session = (
3010+
agent as unknown as {
3011+
session: {
3012+
notificationHistory: Array<{ update?: Record<string, unknown> }>;
3013+
};
3014+
}
3015+
).session;
3016+
const streamedUpdates = session.notificationHistory.filter(
3017+
(notification) =>
3018+
notification.update?.sessionUpdate === "tool_call_update" &&
3019+
notification.update.status === "in_progress",
3020+
);
3021+
expect(streamedUpdates).toHaveLength(1);
3022+
expect(streamedUpdates[0].update?.rawInput).toEqual({
3023+
plan: "first second third",
3024+
});
3025+
});
3026+
3027+
it("caps streamed plans before rendering or storing them", async () => {
3028+
const { agent, stub, permissionRequests, sessionUpdates } = makePlanAgent({
3029+
outcome: { outcome: "selected", optionId: "reject_with_feedback" },
3030+
});
3031+
const { done } = await startPlanTurn(agent, stub);
3032+
3033+
stub.emit("item/plan/delta", { itemId: "p1", delta: "a".repeat(75_000) });
3034+
stub.emit("item/plan/delta", { itemId: "p1", delta: "b".repeat(75_000) });
3035+
stub.emit("turn/completed", {
3036+
turn: { id: "turn_1", status: "completed" },
3037+
});
3038+
await done;
3039+
3040+
const renderedPlans = sessionUpdates.flatMap((notification) => {
3041+
const rawInput = notification.update?.rawInput as
3042+
| { plan?: unknown }
3043+
| undefined;
3044+
return typeof rawInput?.plan === "string" ? [rawInput.plan] : [];
3045+
});
3046+
const approvalPlan = permissionRequests[0].toolCall.rawInput as {
3047+
plan: string;
3048+
};
3049+
expect(renderedPlans.every((plan) => plan.length <= 100_000)).toBe(true);
3050+
expect(approvalPlan.plan).toHaveLength(100_000);
3051+
});
3052+
29953053
it("stays in plan mode when the handoff is rejected without feedback", async () => {
29963054
const { agent, stub, sessionUpdates, permissionRequests } = makePlanAgent({
29973055
outcome: { outcome: "selected", optionId: "reject_with_feedback" },

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

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ type GoalCommand =
115115
| { kind: "resume" }
116116
| { kind: "set"; objective: string };
117117

118+
const MAX_PLAN_PROPOSAL_CHARS = 100_000;
119+
118120
type CodexSkill = {
119121
name?: string;
120122
description?: string;
@@ -1168,7 +1170,7 @@ export class CodexAppServerAgent extends BaseAcpAgent {
11681170
sessionId: this.sessionId,
11691171
update,
11701172
} as unknown as Parameters<AgentSideConnection["sessionUpdate"]>[0];
1171-
this.appendNotification(this.sessionId, notification);
1173+
this.appendPlanApprovalNotification(notification);
11721174
void this.client.sessionUpdate(notification).catch((error) => {
11731175
this.logger.warn("Failed to emit plan approval tool call update", {
11741176
error: String(error),
@@ -1178,6 +1180,36 @@ export class CodexAppServerAgent extends BaseAcpAgent {
11781180
});
11791181
}
11801182

1183+
private appendPlanApprovalNotification(
1184+
notification: Parameters<AgentSideConnection["sessionUpdate"]>[0],
1185+
): void {
1186+
const update = notification.update as Record<string, unknown>;
1187+
if (
1188+
update.sessionUpdate === "tool_call_update" &&
1189+
update.status === "in_progress" &&
1190+
typeof update.toolCallId === "string"
1191+
) {
1192+
for (
1193+
let index = this.session.notificationHistory.length - 1;
1194+
index >= 0;
1195+
index--
1196+
) {
1197+
const previous = this.session.notificationHistory[index] as unknown as {
1198+
update?: Record<string, unknown>;
1199+
};
1200+
if (
1201+
previous.update?.sessionUpdate === "tool_call_update" &&
1202+
previous.update.status === "in_progress" &&
1203+
previous.update.toolCallId === update.toolCallId
1204+
) {
1205+
this.session.notificationHistory[index] = notification;
1206+
return;
1207+
}
1208+
}
1209+
}
1210+
this.appendNotification(this.sessionId, notification);
1211+
}
1212+
11811213
/** Emit a plain agent message (user-facing status the model didn't produce). */
11821214
private broadcastAgentText(text: string): void {
11831215
if (!this.sessionId) return;
@@ -1459,7 +1491,10 @@ export class CodexAppServerAgent extends BaseAcpAgent {
14591491
params as { item?: { type?: string; id?: string; text?: string } }
14601492
)?.item;
14611493
if (item?.type === "plan" && typeof item.text === "string" && item.text) {
1462-
this.planProposal = { itemId: item.id ?? "codex-plan", text: item.text };
1494+
this.planProposal = {
1495+
itemId: item.id ?? "codex-plan",
1496+
text: item.text.slice(0, MAX_PLAN_PROPOSAL_CHARS),
1497+
};
14631498
if (this.config.mode === "plan" && this.streamedPlanToolCallId) {
14641499
this.emitPlanProposal(
14651500
this.buildPlanApprovalToolCall(this.planProposal),
@@ -1482,7 +1517,12 @@ export class CodexAppServerAgent extends BaseAcpAgent {
14821517
: (this.planProposal?.itemId ?? "codex-plan");
14831518
const previousText =
14841519
this.planProposal?.itemId === proposalId ? this.planProposal.text : "";
1485-
this.planProposal = { itemId: proposalId, text: previousText + delta };
1520+
const remainingChars = MAX_PLAN_PROPOSAL_CHARS - previousText.length;
1521+
if (remainingChars <= 0) return;
1522+
this.planProposal = {
1523+
itemId: proposalId,
1524+
text: previousText + delta.slice(0, remainingChars),
1525+
};
14861526
if (this.config.mode === "plan") {
14871527
this.emitPlanProposal(
14881528
this.buildPlanApprovalToolCall(this.planProposal),

0 commit comments

Comments
 (0)