



























@@ -278,6 +278,11 @@ describe("CodexAppServerEventProjector", () => {
278278const { onAssistantMessageStart, onPartialReply, projector } =
279279await createProjectorWithAssistantHooks();
280280281+await projector.handleNotification(
282+forCurrentTurn("item/started", {
283+item: { type: "agentMessage", id: "msg-1", phase: "final_answer", text: "" },
284+}),
285+);
281286await projector.handleNotification(agentMessageDelta("hel"));
282287await projector.handleNotification(agentMessageDelta("lo"));
283288await projector.handleNotification(
@@ -305,7 +310,10 @@ describe("CodexAppServerEventProjector", () => {
305310const result = projector.buildResult(buildEmptyToolTelemetry());
306311307312expect(onAssistantMessageStart).toHaveBeenCalledTimes(1);
308-expect(onPartialReply).not.toHaveBeenCalled();
313+expect(onPartialReply.mock.calls.map((call) => call[0])).toEqual([
314+{ text: "hel", delta: "hel" },
315+{ text: "hello", delta: "lo" },
316+]);
309317expect(result.assistantTexts).toEqual(["hello"]);
310318expect(result.messagesSnapshot.map((message) => message.role)).toEqual(["user", "assistant"]);
311319expect(result.lastAssistant?.content).toEqual([{ type: "text", text: "hello" }]);
@@ -321,7 +329,13 @@ describe("CodexAppServerEventProjector", () => {
321329});
322330323331it("streams final-answer assistant deltas into partial replies", async () => {
324-const { onPartialReply, projector } = await createProjectorWithAssistantHooks();
332+const onAgentEvent = vi.fn();
333+const onPartialReply = vi.fn();
334+const projector = await createProjector({
335+ ...(await createParams()),
336+ onAgentEvent,
337+ onPartialReply,
338+});
325339326340await projector.handleNotification(
327341forCurrentTurn("item/started", {
@@ -341,6 +355,79 @@ describe("CodexAppServerEventProjector", () => {
341355{ text: "hel", delta: "hel" },
342356{ text: "hello", delta: "lo" },
343357]);
358+expect(
359+onAgentEvent.mock.calls
360+.map((call) => call[0])
361+.filter((event) => event.stream === "assistant"),
362+).toEqual([
363+{ stream: "assistant", data: { text: "hel", delta: "hel" } },
364+{ stream: "assistant", data: { text: "hello", delta: "lo" } },
365+]);
366+});
367+368+it("streams assistant deltas when the app-server omits the item phase", async () => {
369+// Newer Codex app-servers (>= 0.139) stream agentMessage deltas without a
370+// "final_answer" phase. These surface on the replaceable agent-event path;
371+// legacy append-oriented partial callbacks stay quiet.
372+const onAgentEvent = vi.fn();
373+const onPartialReply = vi.fn();
374+const params = await createParams();
375+const projector = await createProjector({
376+ ...params,
377+ onAgentEvent,
378+ onPartialReply,
379+});
380+381+await projector.handleNotification(agentMessageDelta("hel", "msg-final"));
382+await projector.handleNotification(agentMessageDelta("lo", "msg-final"));
383+384+expect(onPartialReply).not.toHaveBeenCalled();
385+expect(onAgentEvent.mock.calls.map((call) => call[0])).toEqual([
386+{ stream: "assistant", data: { text: "hel", delta: "hel", replaceable: true } },
387+{ stream: "assistant", data: { text: "hello", delta: "lo", replaceable: true } },
388+]);
389+});
390+391+it("marks partial replacement when an unphased intermediate item is superseded by a final item", async () => {
392+const onAgentEvent = vi.fn();
393+const onPartialReply = vi.fn();
394+const params = await createParams();
395+const projector = await createProjector({
396+ ...params,
397+ onAgentEvent,
398+ onPartialReply,
399+});
400+401+await projector.handleNotification(agentMessageDelta("coordination ", "msg-intermediate"));
402+await projector.handleNotification(agentMessageDelta("draft", "msg-intermediate"));
403+await projector.handleNotification(
404+forCurrentTurn("item/started", {
405+item: { type: "agentMessage", id: "msg-final", phase: "final_answer", text: "" },
406+}),
407+);
408+await projector.handleNotification(agentMessageDelta("final ", "msg-final"));
409+await projector.handleNotification(agentMessageDelta("answer", "msg-final"));
410+411+expect(onPartialReply).not.toHaveBeenCalled();
412+expect(
413+onAgentEvent.mock.calls
414+.map((call) => call[0])
415+.filter((event) => event.stream === "assistant"),
416+).toEqual([
417+{
418+stream: "assistant",
419+data: { text: "coordination ", delta: "coordination ", replaceable: true },
420+},
421+{
422+stream: "assistant",
423+data: { text: "coordination draft", delta: "draft", replaceable: true },
424+},
425+{
426+stream: "assistant",
427+data: { text: "final ", delta: "", replace: true, replaceable: true },
428+},
429+{ stream: "assistant", data: { text: "final answer", delta: "answer", replaceable: true } },
430+]);
344431});
345432346433it("suppresses mirrored user prompt when the inbound message was already persisted", async () => {
@@ -1041,6 +1128,8 @@ describe("CodexAppServerEventProjector", () => {
10411128const result = projector.buildResult(buildEmptyToolTelemetry());
1042112910431130expect(onAssistantMessageStart).toHaveBeenCalledTimes(1);
1131+// Phase-less snapshots stay on the replaceable agent-event path so legacy
1132+// append-only channel previews do not render superseded coordination text.
10441133expect(onPartialReply).not.toHaveBeenCalled();
10451134expect(result.assistantTexts).toEqual([
10461135"release fixes first. please drop affected PRs, failing checks, and blockers here.",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。