

























@@ -0,0 +1,248 @@
1+import {
2+codexExecutionToolName,
3+describeNotificationActivity,
4+isAssistantCompletionReleaseNotification,
5+isCodexTurnAbortMarkerNotification,
6+isNativeToolProgressNotification,
7+isPendingOpenClawDynamicToolCompletionNotification,
8+isRawAssistantCompletionNotification,
9+isRawReasoningCompletionNotification,
10+isRawToolOutputCompletionNotification,
11+isReasoningItemCompletionNotification,
12+isRetryableErrorNotification,
13+isTurnNotification,
14+readCodexNotificationItem,
15+readNotificationItemId,
16+shouldDisarmAssistantCompletionIdleWatch,
17+updateActiveTurnItemIds,
18+} from "./attempt-notifications.js";
19+import { CODEX_POST_REASONING_SOURCE_REPLY_IDLE_TIMEOUT_MS } from "./attempt-timeouts.js";
20+import type { CodexAttemptTurnWatchController } from "./attempt-turn-watches.js";
21+import type { CodexServerNotification } from "./protocol.js";
22+23+type CodexExecutionPhase =
24+| { phase: "turn_accepted" }
25+| { phase: "assistant_output_started" }
26+| { phase: "tool_execution_started"; itemId?: string; tool: string };
27+28+export function reportCodexExecutionNotification(params: {
29+notification: CodexServerNotification;
30+emitExecutionPhaseOnce: (key: string, info: CodexExecutionPhase) => void;
31+}): void {
32+const { notification } = params;
33+if (notification.method === "turn/started") {
34+params.emitExecutionPhaseOnce("turn_accepted", { phase: "turn_accepted" });
35+return;
36+}
37+if (notification.method === "item/agentMessage/delta") {
38+params.emitExecutionPhaseOnce("assistant_output_started", {
39+phase: "assistant_output_started",
40+});
41+return;
42+}
43+if (notification.method !== "item/started") {
44+return;
45+}
46+const item = readCodexNotificationItem(notification.params);
47+const tool = item ? codexExecutionToolName(item) : undefined;
48+if (!item || !tool) {
49+return;
50+}
51+params.emitExecutionPhaseOnce(`tool:${item.id}`, {
52+phase: "tool_execution_started",
53+ tool,
54+itemId: item.id,
55+});
56+}
57+58+export function isTerminalCodexTurnNotificationForTurn(params: {
59+notification: CodexServerNotification;
60+threadId: string;
61+turnId: string;
62+currentPromptTexts: string[];
63+}): boolean {
64+if (!isTurnNotification(params.notification.params, params.threadId, params.turnId)) {
65+return false;
66+}
67+return (
68+params.notification.method === "turn/completed" ||
69+isCodexTurnAbortMarkerNotification(params.notification, {
70+currentPromptTexts: params.currentPromptTexts,
71+})
72+);
73+}
74+75+export function applyCodexTurnNotificationState(params: {
76+notification: CodexServerNotification;
77+threadId: string;
78+turnId: string;
79+currentPromptTexts: string[];
80+sourceReplyDeliveryMode: string | undefined;
81+turnWatches: CodexAttemptTurnWatchController;
82+activeTurnItemIds: Set<string>;
83+activeAppServerTurnRequests: number;
84+pendingOpenClawDynamicToolCompletionIds: Set<string>;
85+turnCrossedToolHandoff: boolean;
86+postToolRawAssistantCompletionIdleTimeoutMs: number;
87+onScheduleTerminalDynamicToolReleaseCheck: () => void;
88+onReportExecutionNotification: (notification: CodexServerNotification) => void;
89+}): {
90+isCurrentTurnNotification: boolean;
91+isTurnAbortMarker: boolean;
92+isTurnTerminal: boolean;
93+turnCrossedToolHandoff: boolean;
94+} {
95+const { notification, turnWatches } = params;
96+const isCurrentTurnNotification = isTurnNotification(
97+notification.params,
98+params.threadId,
99+params.turnId,
100+);
101+const isTurnCompletion = notification.method === "turn/completed" && isCurrentTurnNotification;
102+let turnCrossedToolHandoff = params.turnCrossedToolHandoff;
103+104+if (isCurrentTurnNotification) {
105+turnWatches.touchActivity(`notification:${notification.method}`, {
106+details: describeNotificationActivity(notification),
107+attemptProgress: true,
108+});
109+params.onReportExecutionNotification(notification);
110+updateActiveTurnItemIds(notification, params.activeTurnItemIds);
111+if (notification.method === "item/completed" && params.activeTurnItemIds.size === 0) {
112+params.onScheduleTerminalDynamicToolReleaseCheck();
113+}
114+}
115+116+const unblockedAssistantCompletionRelease =
117+isCurrentTurnNotification &&
118+turnWatches.isAssistantCompletionIdleWatchArmed() &&
119+notification.method === "item/completed" &&
120+params.activeTurnItemIds.size === 0;
121+const trackedDynamicToolCompletion = isPendingOpenClawDynamicToolCompletionNotification(
122+notification,
123+params.pendingOpenClawDynamicToolCompletionIds,
124+);
125+const rawToolOutputCompletion = isRawToolOutputCompletionNotification(notification);
126+if (
127+isCurrentTurnNotification &&
128+(rawToolOutputCompletion || isNativeToolProgressNotification(notification))
129+) {
130+turnCrossedToolHandoff = true;
131+}
132+const assistantCompletionCanRelease = isAssistantCompletionReleaseNotification(
133+notification,
134+turnCrossedToolHandoff,
135+);
136+const postToolRawAssistantCompletionNeedsTerminalGuard =
137+isCurrentTurnNotification &&
138+turnCrossedToolHandoff &&
139+isRawAssistantCompletionNotification(notification) &&
140+params.activeTurnItemIds.size === 0;
141+const rawResponseItemCompletedWithNoActiveItems =
142+isCurrentTurnNotification &&
143+notification.method === "rawResponseItem/completed" &&
144+params.activeTurnItemIds.size === 0 &&
145+params.activeAppServerTurnRequests === 0 &&
146+!assistantCompletionCanRelease &&
147+!postToolRawAssistantCompletionNeedsTerminalGuard;
148+const shouldArmPostReasoningSourceReplyWatch =
149+isCurrentTurnNotification &&
150+isReasoningItemCompletionNotification(notification) &&
151+params.activeTurnItemIds.size === 0 &&
152+params.sourceReplyDeliveryMode === "message_tool_only";
153+const shouldArmPostRawReasoningSourceReplyWatch =
154+rawResponseItemCompletedWithNoActiveItems &&
155+isRawReasoningCompletionNotification(notification) &&
156+params.sourceReplyDeliveryMode === "message_tool_only";
157+const shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem =
158+isCurrentTurnNotification &&
159+notification.method === "item/completed" &&
160+params.activeTurnItemIds.size === 0 &&
161+!trackedDynamicToolCompletion &&
162+!assistantCompletionCanRelease &&
163+!shouldArmPostReasoningSourceReplyWatch;
164+165+if (isCurrentTurnNotification && notification.method === "error") {
166+if (isRetryableErrorNotification(notification.params)) {
167+turnWatches.disarmCompletionIdleWatch();
168+} else {
169+turnWatches.armCompletionIdleWatch({ pinnedByTerminalError: true });
170+}
171+turnWatches.disarmAssistantCompletionIdleWatch();
172+} else if (isTurnCompletion) {
173+turnWatches.disarmAssistantCompletionIdleWatch();
174+} else if (isCurrentTurnNotification && assistantCompletionCanRelease) {
175+turnWatches.armAssistantCompletionIdleWatch(describeNotificationActivity(notification));
176+} else if (postToolRawAssistantCompletionNeedsTerminalGuard) {
177+turnWatches.armCompletionIdleWatch({
178+timeoutMs: params.postToolRawAssistantCompletionIdleTimeoutMs,
179+});
180+} else if (shouldArmPostReasoningSourceReplyWatch || shouldArmPostRawReasoningSourceReplyWatch) {
181+turnWatches.armCompletionIdleWatch({
182+timeoutMs: CODEX_POST_REASONING_SOURCE_REPLY_IDLE_TIMEOUT_MS,
183+});
184+} else if (unblockedAssistantCompletionRelease) {
185+turnWatches.armAssistantCompletionIdleWatch(describeNotificationActivity(notification));
186+} else if (shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem) {
187+// If a non-assistant current-turn item is the last active item and the
188+// bridge then goes quiet, reset the short completion-idle guard from that
189+// final completion so the remaining silent-turn gap fails fast.
190+turnWatches.armCompletionIdleWatch();
191+} else if (rawResponseItemCompletedWithNoActiveItems) {
192+turnWatches.armCompletionIdleWatch();
193+} else if (isCurrentTurnNotification && rawToolOutputCompletion) {
194+// Raw OpenAI response streams can report the tool-output handoff without
195+// a matching app-server `item/completed`; keep the post-tool guard alive.
196+turnWatches.armCompletionIdleWatch();
197+} else if (isCurrentTurnNotification && shouldDisarmAssistantCompletionIdleWatch(notification)) {
198+turnWatches.disarmAssistantCompletionIdleWatch();
199+}
200+201+if (
202+turnWatches.isCompletionIdleWatchArmed() &&
203+!turnWatches.isCompletionIdleWatchPinnedByTerminalError() &&
204+notification.method !== "turn/completed" &&
205+isCurrentTurnNotification &&
206+!trackedDynamicToolCompletion &&
207+!rawToolOutputCompletion &&
208+!postToolRawAssistantCompletionNeedsTerminalGuard &&
209+!rawResponseItemCompletedWithNoActiveItems &&
210+!shouldArmPostReasoningSourceReplyWatch &&
211+!shouldArmPostRawReasoningSourceReplyWatch &&
212+!shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem
213+) {
214+// The short completion-idle watchdog guards blind gaps after Codex
215+// accepts a turn or after OpenClaw hands a turn-scoped request result
216+// back to Codex. Bookkeeping that closes the just-served OpenClaw
217+// dynamic tool item is still part of that handoff, so keep the short
218+// watchdog armed for that notification.
219+turnWatches.disarmCompletionIdleWatch();
220+}
221+222+if (trackedDynamicToolCompletion) {
223+const itemId = readNotificationItemId(notification);
224+if (itemId) {
225+params.pendingOpenClawDynamicToolCompletionIds.delete(itemId);
226+params.onScheduleTerminalDynamicToolReleaseCheck();
227+}
228+}
229+230+const isTurnAbortMarker =
231+isCurrentTurnNotification &&
232+isCodexTurnAbortMarkerNotification(notification, {
233+currentPromptTexts: params.currentPromptTexts,
234+});
235+const isTurnTerminal = isTerminalCodexTurnNotificationForTurn({
236+ notification,
237+threadId: params.threadId,
238+turnId: params.turnId,
239+currentPromptTexts: params.currentPromptTexts,
240+});
241+242+return {
243+ isCurrentTurnNotification,
244+ isTurnAbortMarker,
245+ isTurnTerminal,
246+ turnCrossedToolHandoff,
247+};
248+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。