fix(server): preserve codex assistant item boundaries
This commit is contained in:
@@ -983,6 +983,51 @@ describe("Codex app-server provider", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("emits a markdown divider when a new Codex assistant item starts after the previous one completed", () => {
|
||||
const session = createSession();
|
||||
const events: AgentStreamEvent[] = [];
|
||||
session.subscribe((event) => events.push(event));
|
||||
|
||||
asInternals(session).handleNotification("item/agentMessage/delta", {
|
||||
itemId: "assistant-item-3",
|
||||
delta:
|
||||
"I’m in the waiting phase now. The next read is intentionally delayed so we get meaningful CI state instead of churn.",
|
||||
});
|
||||
asInternals(session).handleNotification("item/completed", {
|
||||
item: {
|
||||
id: "assistant-item-3",
|
||||
type: "agentMessage",
|
||||
text: "I’m in the waiting phase now. The next read is intentionally delayed so we get meaningful CI state instead of churn.",
|
||||
},
|
||||
});
|
||||
asInternals(session).handleNotification("item/agentMessage/delta", {
|
||||
itemId: "assistant-item-4",
|
||||
delta:
|
||||
"CI is still cooking. I’m staying on the current run rather than jumping around, because the first red job will tell us exactly whether anything else needs work.",
|
||||
});
|
||||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "codex",
|
||||
turnId: "test-turn",
|
||||
item: {
|
||||
type: "assistant_message",
|
||||
text: "I’m in the waiting phase now. The next read is intentionally delayed so we get meaningful CI state instead of churn.",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "timeline",
|
||||
provider: "codex",
|
||||
turnId: "test-turn",
|
||||
item: {
|
||||
type: "assistant_message",
|
||||
text: "\n\n---\n\nCI is still cooking. I’m staying on the current run rather than jumping around, because the first red job will tell us exactly whether anything else needs work.",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("streams Codex reasoning deltas and does not replay completed reasoning", () => {
|
||||
const session = createSession();
|
||||
const events: AgentStreamEvent[] = [];
|
||||
|
||||
@@ -69,6 +69,7 @@ const APP_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS = 2_000;
|
||||
const APP_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS = 1_000;
|
||||
const CODEX_PROVIDER = "codex" as const;
|
||||
const CODEX_IMAGE_ATTACHMENT_DIR = "paseo-attachments";
|
||||
const ASSISTANT_MESSAGE_BOUNDARY_MARKDOWN = "\n\n---\n\n";
|
||||
const CODEX_PLAN_IMPLEMENTATION_PROMPT_PREFIX =
|
||||
"The user approved the plan. Implement it now. Do not restate or revise the plan unless blocked.";
|
||||
|
||||
@@ -2539,6 +2540,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
private pendingReasoning = new Map<string, string[]>();
|
||||
private pendingCommandOutputDeltas = new Map<string, string[]>();
|
||||
private pendingFileChangeOutputDeltas = new Map<string, string[]>();
|
||||
private pendingAssistantMessageBoundary = false;
|
||||
private terminalCommandByProcessId = new Map<string, string>();
|
||||
private pendingUnlabeledTerminalInteractions = new Set<string>();
|
||||
private emittedTerminalInteractionKeys = new Set<string>();
|
||||
@@ -3633,11 +3635,21 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
if (parsed.kind === "agent_message_delta") {
|
||||
const prev = this.pendingAgentMessages.get(parsed.itemId) ?? "";
|
||||
this.pendingAgentMessages.set(parsed.itemId, prev + parsed.delta);
|
||||
const isFirstDeltaForItem = prev.length === 0;
|
||||
this.emitEvent({
|
||||
type: "timeline",
|
||||
provider: CODEX_PROVIDER,
|
||||
item: { type: "assistant_message", text: parsed.delta },
|
||||
item: {
|
||||
type: "assistant_message",
|
||||
text:
|
||||
isFirstDeltaForItem && this.pendingAssistantMessageBoundary
|
||||
? `${ASSISTANT_MESSAGE_BOUNDARY_MARKDOWN}${parsed.delta}`
|
||||
: parsed.delta,
|
||||
},
|
||||
});
|
||||
if (isFirstDeltaForItem) {
|
||||
this.pendingAssistantMessageBoundary = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parsed.kind === "reasoning_delta") {
|
||||
@@ -3714,6 +3726,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
this.pendingReasoning.clear();
|
||||
this.pendingCommandOutputDeltas.clear();
|
||||
this.pendingFileChangeOutputDeltas.clear();
|
||||
this.pendingAssistantMessageBoundary = false;
|
||||
this.warnedIncompleteEditToolCallIds.clear();
|
||||
}
|
||||
|
||||
@@ -3893,6 +3906,9 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
return;
|
||||
}
|
||||
if (this.consumeStreamedTextCompletion(timelineItem, itemId)) {
|
||||
if (timelineItem.type === "assistant_message") {
|
||||
this.pendingAssistantMessageBoundary = true;
|
||||
}
|
||||
if (itemId) {
|
||||
this.emittedItemCompletedIds.add(itemId);
|
||||
this.emittedItemStartedIds.delete(itemId);
|
||||
@@ -3912,6 +3928,9 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
this.warnOnIncompleteEditToolCall(timelineItem, "item_completed", parsed.item);
|
||||
}
|
||||
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
||||
if (timelineItem.type === "assistant_message") {
|
||||
this.pendingAssistantMessageBoundary = true;
|
||||
}
|
||||
if (itemId) {
|
||||
this.emittedItemCompletedIds.add(itemId);
|
||||
this.emittedItemStartedIds.delete(itemId);
|
||||
|
||||
Reference in New Issue
Block a user