@jskit-ai/assistant-core 0.1.200 → 0.1.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,990 @@
1
+ import {
2
+ codexAppServerTurnStatusIsActive,
3
+ codexAppServerTurnStatusIsComplete,
4
+ codexAppServerTurnStatusIsSuccessfulComplete,
5
+ codexAppServerTurnStatusIsProviderFailure,
6
+ codexAppServerThreadStatus,
7
+ codexAppServerReadThreadStatus,
8
+ normalizeCodexRunText,
9
+ codexAppServerAgentRun,
10
+ codexAppServerPendingUserMessageClientIds,
11
+ codexAppServerPendingUserMessageOwnership,
12
+ codexAppServerTurnOwnsActiveGoal,
13
+ codexAppServerTurnCanReceiveProviderCompletion,
14
+ codexAppServerTurnAwaitsProviderIdentity,
15
+ codexAppServerTurnCanReceiveProviderActivity,
16
+ codexAppServerTurnCanAdoptSuccessor,
17
+ codexAppServerTurnKey,
18
+ CODEX_APP_SERVER_RUN_STATE
19
+ } from "./codexTurnState.js";
20
+ import {
21
+ codexAppServerProviderTurnForOperation,
22
+ codexAppServerThreadTurnId,
23
+ codexAppServerThreadError
24
+ } from "./codexEvents.js";
25
+
26
+ // Original active/finalizing recovery, provider activity and completion decisions.
27
+ // Timers and receipts remain owned by the shared journal/output/settlement instances.
28
+ export function createCodexTurnRecovery({
29
+ journal,
30
+ output,
31
+ settlement,
32
+ promptDeliveries,
33
+ namespace,
34
+ createRuntime,
35
+ createStore,
36
+ acquireProvider,
37
+ turnState,
38
+ turnStateFromAgentRun,
39
+ debugLog,
40
+ debugError,
41
+ captureContext,
42
+ runInContext,
43
+ activeReconcileMs,
44
+ hasRuntime,
45
+ recoverAdmission,
46
+ turnOutcomes,
47
+ outcomeNotice,
48
+ resultDeliveryFailureMessage
49
+ }) {
50
+ const {
51
+ finalizedTurns: codexAppServerFinalizedTurns,
52
+ resultFinalizationKey: codexAppServerResultFinalizationKey,
53
+ readAgentRunForSession: readCodexAppServerAgentRunForSession,
54
+ cleanupUntrackedTurn: cleanupCodexAppServerUntrackedTurn
55
+ } = output;
56
+ const {
57
+ completedTurns: codexAppServerCompletedTurns,
58
+ activeTimers: codexAppServerActiveTimers,
59
+ finalizingTimers: codexAppServerFinalizingTimers,
60
+ turnResultWasProcessed: codexAppServerTurnResultWasProcessed,
61
+ clearActiveTimer: clearCodexAppServerActiveTimer,
62
+ clearFinalizingTimer: clearCodexAppServerFinalizingTimer,
63
+ publishAgentRun: publishCodexAppServerAgentRun,
64
+ markTurnFinalizing: markCodexAppServerTurnFinalizing,
65
+ markTurnIdle: markCodexAppServerTurnIdle,
66
+ resolveTurnId: resolveCodexAppServerTurnId,
67
+ finalizingExpired: codexAppServerFinalizingExpired,
68
+ finalizingRemainingMs: codexAppServerFinalizingRemainingMs,
69
+ turnWasCompleted: codexAppServerTurnWasCompleted,
70
+ runInputSource: codexAppServerRunInputSource,
71
+ writeUserMessageOwnership: writeCodexAppServerUserMessageOwnership,
72
+ recoverAbandonedPromptClaim: recoverAbandonedCodexAppServerPromptClaim,
73
+ failOrphanedPromptDelivery: failOrphanedCodexAppServerPromptDelivery
74
+ } = journal;
75
+ const {
76
+ finalizeAssistantResult: finalizeCodexAppServerAssistantResult,
77
+ recoverFinalResponseBeforeOutcome: recoverCodexAppServerFinalResponseBeforeOutcome,
78
+ adoptSuccessorTurn: adoptCodexAppServerSuccessorTurn
79
+ } = settlement;
80
+ const codexAppServerPromptDeliveries = promptDeliveries;
81
+
82
+ function scheduleCodexAppServerActiveRecovery(sessionId = "", delayMs = activeReconcileMs) {
83
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
84
+ const sessionKey = namespace(normalizedSessionId);
85
+ const projectContext = captureContext();
86
+ if (!normalizedSessionId || codexAppServerActiveTimers.has(sessionKey)) {
87
+ return;
88
+ }
89
+ const timer = setTimeout(() => {
90
+ codexAppServerActiveTimers.delete(sessionKey);
91
+ void runInContext(
92
+ projectContext,
93
+ () => recoverCodexAppServerActiveTurn(normalizedSessionId)
94
+ );
95
+ }, delayMs);
96
+ timer.unref?.();
97
+ codexAppServerActiveTimers.set(sessionKey, timer);
98
+ }
99
+
100
+ function scheduleCodexAppServerFinalizingRecovery(sessionId = "", threadId = "", turnId = "", {
101
+ completedAt = "",
102
+ status = "completed",
103
+ updatedAt = ""
104
+ } = {}) {
105
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
106
+ const normalizedThreadId = normalizeCodexRunText(threadId);
107
+ const normalizedTurnId = normalizeCodexRunText(turnId);
108
+ if (!normalizedSessionId || !normalizedThreadId || !normalizedTurnId) {
109
+ return;
110
+ }
111
+ clearCodexAppServerFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
112
+ const key = codexAppServerResultFinalizationKey(
113
+ normalizedSessionId,
114
+ normalizedThreadId,
115
+ normalizedTurnId
116
+ );
117
+ const projectContext = captureContext();
118
+ const delayMs = codexAppServerFinalizingRemainingMs({
119
+ completedAt,
120
+ state: "finalizing",
121
+ updatedAt
122
+ });
123
+ const timer = setTimeout(() => {
124
+ codexAppServerFinalizingTimers.delete(key);
125
+ void runInContext(
126
+ projectContext,
127
+ () => recoverCodexAppServerFinalizingTurn(
128
+ normalizedSessionId,
129
+ normalizedThreadId,
130
+ normalizedTurnId,
131
+ { status }
132
+ )
133
+ );
134
+ }, delayMs);
135
+ timer.unref?.();
136
+ codexAppServerFinalizingTimers.set(key, timer);
137
+ }
138
+
139
+ async function reconcileCodexAppServerActiveTurn(session = {}, {
140
+ provider: suppliedProvider = null,
141
+ runtime = null
142
+ } = {}) {
143
+ const sessionId = normalizeCodexRunText(session.sessionId);
144
+ const trackedTurn = turnState(session);
145
+ if (!sessionId || !trackedTurn.active || !trackedTurn.threadId || !hasRuntime(session)) {
146
+ return session;
147
+ }
148
+ const provider = suppliedProvider || await acquireProvider({ sessionId, runtime, session });
149
+ if (typeof provider?.readThreadStatus !== "function") {
150
+ if (trackedTurn.state === "finalizing") {
151
+ await recoverCodexAppServerFinalizingTurn(
152
+ sessionId,
153
+ trackedTurn.threadId,
154
+ trackedTurn.turnId,
155
+ {
156
+ status: trackedTurn.status || "completed"
157
+ }
158
+ );
159
+ const runtime = await createRuntime();
160
+ return runtime.getSession(sessionId);
161
+ }
162
+ return session;
163
+ }
164
+ const activeRuntime = runtime || await createRuntime();
165
+ let currentSession = session;
166
+ let currentTurn = trackedTurn;
167
+ let thread = await codexAppServerReadThreadStatus(provider, trackedTurn.threadId);
168
+ let status = codexAppServerThreadStatus(thread);
169
+ let providerTurnId = codexAppServerThreadTurnId(thread);
170
+ const promptDeliveryIsLocal = codexAppServerPromptDeliveries.has(
171
+ namespace(sessionId)
172
+ );
173
+ if (
174
+ currentTurn.state === "starting" &&
175
+ !currentTurn.turnId &&
176
+ !promptDeliveryIsLocal
177
+ ) {
178
+ const ownership = codexAppServerPendingUserMessageOwnership(
179
+ codexAppServerAgentRun(currentSession)
180
+ );
181
+ if (!ownership || typeof provider?.readThread !== "function") {
182
+ return failOrphanedCodexAppServerPromptDelivery(
183
+ activeRuntime,
184
+ currentSession,
185
+ currentTurn
186
+ );
187
+ }
188
+ const providerThread = await provider.readThread(currentTurn.threadId);
189
+ const ownedProviderTurn = codexAppServerProviderTurnForOperation(providerThread, {
190
+ clientMessageId: ownership.clientId
191
+ });
192
+ if (!ownedProviderTurn) {
193
+ return failOrphanedCodexAppServerPromptDelivery(
194
+ activeRuntime,
195
+ currentSession,
196
+ currentTurn
197
+ );
198
+ }
199
+ thread = {
200
+ ...providerThread,
201
+ observedTurn: ownedProviderTurn
202
+ };
203
+ status = codexAppServerThreadStatus(thread);
204
+ providerTurnId = codexAppServerThreadTurnId(thread);
205
+ if (!providerTurnId) {
206
+ return failOrphanedCodexAppServerPromptDelivery(
207
+ activeRuntime,
208
+ currentSession,
209
+ currentTurn
210
+ );
211
+ }
212
+ await markCodexAppServerProviderTurnActive(sessionId, {
213
+ inputSource: ownership.inputSource,
214
+ source: "owned_prompt_recovery",
215
+ status: codexAppServerTurnStatusIsActive(status) ? status : "inProgress",
216
+ threadId: currentTurn.threadId,
217
+ turnId: providerTurnId
218
+ });
219
+ await writeCodexAppServerUserMessageOwnership(
220
+ activeRuntime.store,
221
+ sessionId,
222
+ ownership.clientId,
223
+ {
224
+ eventKind: "codex-app-server-user-message-consumed",
225
+ owned: false
226
+ }
227
+ );
228
+ currentSession = await activeRuntime.getSession(sessionId);
229
+ currentTurn = turnState(currentSession);
230
+ debugLog("appServerPrompt.recovered", {
231
+ clientMessageId: ownership.clientId,
232
+ sessionId,
233
+ status,
234
+ threadId: currentTurn.threadId,
235
+ turnId: currentTurn.turnId
236
+ });
237
+ }
238
+ if (
239
+ providerTurnId &&
240
+ codexAppServerTurnCanAdoptSuccessor(currentTurn, currentTurn.threadId, providerTurnId)
241
+ ) {
242
+ const adoption = await adoptCodexAppServerSuccessorTurn(sessionId, {
243
+ previousTurnId: currentTurn.turnId,
244
+ source: "active_reconciliation",
245
+ status: "inProgress",
246
+ threadId: currentTurn.threadId,
247
+ turnId: providerTurnId
248
+ });
249
+ currentSession = await activeRuntime.getSession(sessionId);
250
+ currentTurn = turnState(currentSession);
251
+ if (!adoption.processed && adoption.reason !== "already_current") {
252
+ if (currentTurn.state === "starting") {
253
+ scheduleCodexAppServerActiveRecovery(sessionId);
254
+ }
255
+ return currentSession;
256
+ }
257
+ }
258
+ if (currentTurn.state === "finalizing") {
259
+ await recoverCodexAppServerFinalizingTurn(sessionId, currentTurn.threadId, currentTurn.turnId, {
260
+ status: currentTurn.status || "completed"
261
+ });
262
+ return activeRuntime.getSession(sessionId);
263
+ }
264
+ if (currentTurn.state === "starting") {
265
+ if (codexAppServerTurnStatusIsActive(status) && providerTurnId) {
266
+ await markCodexAppServerProviderTurnActive(sessionId, {
267
+ source: "active_reconciliation",
268
+ status,
269
+ threadId: currentTurn.threadId,
270
+ turnId: providerTurnId
271
+ });
272
+ return activeRuntime.getSession(sessionId);
273
+ }
274
+ if (codexAppServerTurnStatusIsActive(status) || !status) {
275
+ scheduleCodexAppServerActiveRecovery(sessionId);
276
+ return currentSession;
277
+ }
278
+ if (!promptDeliveryIsLocal && (!providerTurnId || !codexAppServerTurnStatusIsComplete(status))) {
279
+ return failOrphanedCodexAppServerPromptDelivery(activeRuntime, currentSession, currentTurn);
280
+ }
281
+ if (promptDeliveryIsLocal) {
282
+ return currentSession;
283
+ }
284
+ await markCodexAppServerProviderTurnActive(sessionId, {
285
+ source: "active_reconciliation",
286
+ status: "inProgress",
287
+ threadId: currentTurn.threadId,
288
+ turnId: providerTurnId
289
+ });
290
+ currentSession = await activeRuntime.getSession(sessionId);
291
+ currentTurn = turnState(currentSession);
292
+ }
293
+ if (!status || codexAppServerTurnStatusIsActive(status)) {
294
+ if (currentTurn.state === "starting") {
295
+ scheduleCodexAppServerActiveRecovery(sessionId);
296
+ }
297
+ return currentSession;
298
+ }
299
+ if (!codexAppServerTurnStatusIsComplete(status)) {
300
+ return currentSession;
301
+ }
302
+ const completedTurnId = providerTurnId || currentTurn.turnId;
303
+ debugLog("appServerTurn.reconcile.complete", {
304
+ sessionId,
305
+ status,
306
+ threadId: currentTurn.threadId,
307
+ turnId: completedTurnId
308
+ });
309
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
310
+ await stopCodexAppServerTurnWithProviderFailure(sessionId, currentTurn.threadId, completedTurnId, {
311
+ error: codexAppServerThreadError(thread),
312
+ provider,
313
+ status
314
+ });
315
+ } else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
316
+ await completeCodexAppServerTurn(sessionId, currentTurn.threadId, completedTurnId, {
317
+ status,
318
+ verifyInactive: false
319
+ });
320
+ }
321
+ return activeRuntime.getSession(sessionId);
322
+ }
323
+
324
+ async function codexAppServerProviderBlocksTurnRelease(sessionId = "", provider = null, threadId = "", turnId = "", {
325
+ source = ""
326
+ } = {}) {
327
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
328
+ const normalizedThreadId = normalizeCodexRunText(threadId);
329
+ const normalizedTurnId = normalizeCodexRunText(turnId);
330
+ if (!normalizedSessionId || !normalizedThreadId) {
331
+ return false;
332
+ }
333
+ const runtime = await createRuntime();
334
+ const session = await runtime.getSession(normalizedSessionId);
335
+ const turn = turnState(session);
336
+ if (!turn.active || !codexAppServerTurnCanReceiveProviderActivity(turn, normalizedThreadId, normalizedTurnId)) {
337
+ return false;
338
+ }
339
+ try {
340
+ const providerThread = await codexAppServerReadThreadStatus(provider, normalizedThreadId, {
341
+ observeLatestTurn: true
342
+ });
343
+ if (!providerThread) {
344
+ return false;
345
+ }
346
+ const status = codexAppServerThreadStatus(providerThread);
347
+ const providerTurnId = codexAppServerThreadTurnId(providerThread);
348
+ if (
349
+ providerTurnId &&
350
+ providerTurnId !== normalizeCodexRunText(turn.turnId)
351
+ ) {
352
+ debugLog("appServerTurn.releaseAllowedSuccessor", {
353
+ providerTurnId,
354
+ sessionId: normalizedSessionId,
355
+ source: normalizeCodexRunText(source),
356
+ threadId: normalizedThreadId,
357
+ turnId: turn.turnId
358
+ });
359
+ return false;
360
+ }
361
+ if (!codexAppServerTurnStatusIsActive(status)) {
362
+ return false;
363
+ }
364
+ debugLog("appServerTurn.releaseBlockedActive", {
365
+ sessionId: normalizedSessionId,
366
+ source: normalizeCodexRunText(source),
367
+ status,
368
+ threadId: normalizedThreadId,
369
+ turnId: normalizedTurnId || turn.turnId
370
+ });
371
+ scheduleCodexAppServerActiveRecovery(normalizedSessionId);
372
+ return true;
373
+ } catch (error) {
374
+ debugLog("appServerTurn.releaseCheck.error", {
375
+ error: debugError(error),
376
+ sessionId: normalizedSessionId,
377
+ source: normalizeCodexRunText(source),
378
+ threadId: normalizedThreadId,
379
+ turnId: normalizedTurnId || turn.turnId
380
+ });
381
+ return false;
382
+ }
383
+ }
384
+
385
+ async function markCodexAppServerTurnActive(sessionId = "", input = {}) {
386
+ if (input.requireTrackedTurn === true) {
387
+ const runtime = await createRuntime();
388
+ const session = await runtime.getSession(sessionId);
389
+ const turn = turnState(session);
390
+ if (!codexAppServerTurnCanReceiveProviderActivity(turn, input.threadId, input.turnId)) {
391
+ debugLog("appServerTurn.active.ignored", {
392
+ currentState: turn.state,
393
+ currentStatus: turn.status,
394
+ currentThreadId: turn.threadId,
395
+ currentTurnId: turn.turnId,
396
+ sessionId: normalizeCodexRunText(sessionId),
397
+ threadId: normalizeCodexRunText(input.threadId),
398
+ turnId: normalizeCodexRunText(input.turnId)
399
+ });
400
+ return {
401
+ ok: true,
402
+ processed: false,
403
+ reason: "untracked_terminal_turn"
404
+ };
405
+ }
406
+ }
407
+ const status = normalizeCodexRunText(input.status) || "inProgress";
408
+ const result = await publishCodexAppServerAgentRun(sessionId, {
409
+ inputSource: normalizeCodexRunText(input.inputSource),
410
+ phase: input.phase,
411
+ publishReason: "codex-app-server-turn-active",
412
+ observedRun: input.observedRun,
413
+ runState: status === "starting" ? CODEX_APP_SERVER_RUN_STATE.STARTING : CODEX_APP_SERVER_RUN_STATE.ACTIVE,
414
+ status,
415
+ threadId: normalizeCodexRunText(input.threadId),
416
+ turnId: normalizeCodexRunText(input.turnId)
417
+ });
418
+ if (status === "starting") {
419
+ scheduleCodexAppServerActiveRecovery(sessionId);
420
+ } else {
421
+ clearCodexAppServerActiveTimer(sessionId);
422
+ }
423
+ return result;
424
+ }
425
+
426
+ async function markCodexAppServerProviderTurnActive(sessionId = "", input = {}) {
427
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
428
+ const normalizedThreadId = normalizeCodexRunText(input.threadId);
429
+ const normalizedTurnId = normalizeCodexRunText(input.turnId);
430
+ const requestedStatus = normalizeCodexRunText(input.status) || "inProgress";
431
+ const store = await createStore(normalizedSessionId);
432
+ const persistedRun = await readCodexAppServerAgentRunForSession(
433
+ store,
434
+ normalizedSessionId
435
+ );
436
+ const persistedTurn = turnStateFromAgentRun(persistedRun || {});
437
+ const requestedInputSource = normalizeCodexRunText(input.inputSource);
438
+ if (
439
+ persistedTurn.active &&
440
+ persistedTurn.state === "active" &&
441
+ persistedTurn.threadId === normalizedThreadId &&
442
+ persistedTurn.turnId === normalizedTurnId &&
443
+ codexAppServerTurnStatusIsActive(requestedStatus) &&
444
+ (!requestedInputSource || requestedInputSource === persistedTurn.inputSource)
445
+ ) {
446
+ clearCodexAppServerActiveTimer(normalizedSessionId);
447
+ return {
448
+ ok: true,
449
+ processed: false,
450
+ reason: "already_active"
451
+ };
452
+ }
453
+ const runtime = await createRuntime();
454
+ let session = await runtime.getSession(normalizedSessionId);
455
+ let turn = turnState(session);
456
+ if (
457
+ input.observedRun &&
458
+ [CODEX_APP_SERVER_RUN_STATE.FAILED, CODEX_APP_SERVER_RUN_STATE.INTERRUPTED].includes(turn.runState) &&
459
+ turn.threadId === normalizedThreadId &&
460
+ turn.turnId === normalizedTurnId &&
461
+ !codexAppServerTurnResultWasProcessed(session, normalizedThreadId, normalizedTurnId)
462
+ ) {
463
+ const recovered = await markCodexAppServerTurnActive(normalizedSessionId, {
464
+ inputSource: turn.inputSource,
465
+ observedRun: input.observedRun,
466
+ status: requestedStatus,
467
+ threadId: normalizedThreadId,
468
+ turnId: normalizedTurnId
469
+ });
470
+ if (recovered.processed !== false) {
471
+ codexAppServerCompletedTurns.delete(codexAppServerTurnKey(normalizedThreadId, normalizedTurnId));
472
+ codexAppServerFinalizedTurns.delete(codexAppServerResultFinalizationKey(
473
+ normalizedSessionId, normalizedThreadId, normalizedTurnId
474
+ ));
475
+ }
476
+ return recovered;
477
+ }
478
+ if (codexAppServerTurnWasCompleted(session, normalizedThreadId, normalizedTurnId)) {
479
+ debugLog("appServerProviderTurn.completed.ignored", {
480
+ currentState: turn.state,
481
+ currentThreadId: turn.threadId,
482
+ currentTurnId: turn.turnId,
483
+ sessionId: normalizedSessionId,
484
+ threadId: normalizedThreadId,
485
+ turnId: normalizedTurnId
486
+ });
487
+ return {
488
+ ok: true,
489
+ processed: false,
490
+ reason: "completed_turn"
491
+ };
492
+ }
493
+ if (
494
+ turn.state === "finalizing" &&
495
+ codexAppServerTurnCanAdoptSuccessor(turn, normalizedThreadId, normalizedTurnId) &&
496
+ codexAppServerTurnResultWasProcessed(session, normalizedThreadId, turn.turnId)
497
+ ) {
498
+ await finalizeCodexAppServerAssistantResult(
499
+ normalizedSessionId,
500
+ normalizedThreadId,
501
+ turn.turnId,
502
+ {
503
+ status: turn.status || "completed"
504
+ }
505
+ );
506
+ session = await runtime.getSession(normalizedSessionId);
507
+ turn = turnState(session);
508
+ }
509
+ const currentRun = codexAppServerAgentRun(session);
510
+ const pendingInputSource = codexAppServerPendingUserMessageClientIds(currentRun).length > 0
511
+ ? codexAppServerRunInputSource(currentRun)
512
+ : "";
513
+ const goalInputSource = codexAppServerTurnOwnsActiveGoal(turn, normalizedThreadId)
514
+ ? codexAppServerRunInputSource(currentRun)
515
+ : "";
516
+ const inputSource = normalizeCodexRunText(input.inputSource) || pendingInputSource || goalInputSource;
517
+ if (codexAppServerTurnCanReceiveProviderActivity(turn, normalizedThreadId, normalizedTurnId)) {
518
+ return markCodexAppServerTurnActive(normalizedSessionId, {
519
+ inputSource,
520
+ status: requestedStatus,
521
+ threadId: normalizedThreadId,
522
+ turnId: normalizedTurnId
523
+ });
524
+ }
525
+ if (codexAppServerTurnCanAdoptSuccessor(turn, normalizedThreadId, normalizedTurnId)) {
526
+ return adoptCodexAppServerSuccessorTurn(normalizedSessionId, {
527
+ previousTurnId: turn.turnId,
528
+ source: normalizeCodexRunText(input.source) || "provider_activity",
529
+ status: requestedStatus,
530
+ threadId: normalizedThreadId,
531
+ turnId: normalizedTurnId
532
+ });
533
+ }
534
+ if (turn.active || !normalizedTurnId) {
535
+ debugLog("appServerProviderTurn.active.ignored", {
536
+ currentState: turn.state,
537
+ currentStatus: turn.status,
538
+ currentThreadId: turn.threadId,
539
+ currentTurnId: turn.turnId,
540
+ sessionId: normalizedSessionId,
541
+ threadId: normalizedThreadId,
542
+ turnId: normalizedTurnId
543
+ });
544
+ return {
545
+ ok: true,
546
+ processed: false,
547
+ reason: turn.active ? "active_turn_mismatch" : "missing_turn"
548
+ };
549
+ }
550
+ return markCodexAppServerTurnActive(normalizedSessionId, {
551
+ inputSource: inputSource || "terminal",
552
+ status: requestedStatus,
553
+ threadId: normalizedThreadId,
554
+ turnId: normalizedTurnId
555
+ });
556
+ }
557
+
558
+ async function recoverCodexAppServerFinalizingTurn(sessionId = "", threadId = "", turnId = "", {
559
+ status = "completed"
560
+ } = {}) {
561
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
562
+ const normalizedThreadId = normalizeCodexRunText(threadId);
563
+ const normalizedTurnId = normalizeCodexRunText(turnId);
564
+ const runtime = await createRuntime();
565
+ const session = await runtime.getSession(normalizedSessionId);
566
+ const turn = turnState(session);
567
+ if (codexAppServerTurnOwnsActiveGoal(turn, normalizedThreadId)) {
568
+ clearCodexAppServerFinalizingTimer(
569
+ normalizedSessionId,
570
+ normalizedThreadId,
571
+ normalizedTurnId
572
+ );
573
+ return {
574
+ ok: true,
575
+ processed: false,
576
+ reason: "goal_continuation_pending"
577
+ };
578
+ }
579
+ const result = await finalizeCodexAppServerAssistantResult(
580
+ normalizedSessionId,
581
+ normalizedThreadId,
582
+ normalizedTurnId,
583
+ {
584
+ recoverFromProvider: true,
585
+ status
586
+ }
587
+ );
588
+ if (result?.processed) {
589
+ return result;
590
+ }
591
+ if (!codexAppServerFinalizingExpired(turn)) {
592
+ scheduleCodexAppServerFinalizingRecovery(normalizedSessionId, normalizedThreadId, normalizedTurnId, {
593
+ completedAt: turn.completedAt,
594
+ status,
595
+ updatedAt: turn.updatedAt
596
+ });
597
+ return result;
598
+ }
599
+ return stopCodexAppServerTurnWithResultDeliveryFailure(
600
+ normalizedSessionId,
601
+ normalizedThreadId,
602
+ normalizedTurnId,
603
+ {
604
+ error: result?.error,
605
+ reason: result?.reason || "missing_assistant_text",
606
+ status
607
+ }
608
+ );
609
+ }
610
+
611
+ async function recoverCodexAppServerActiveTurn(sessionId = "", {
612
+ provider = null,
613
+ retryOnError = true,
614
+ runtime: suppliedRuntime = null
615
+ } = {}) {
616
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
617
+ if (!normalizedSessionId) {
618
+ return null;
619
+ }
620
+ try {
621
+ const runtime = suppliedRuntime || await createRuntime();
622
+ const storedSession = await runtime.getSession(normalizedSessionId);
623
+ const abandonedClaim = await (recoverAdmission || recoverAbandonedCodexAppServerPromptClaim)(
624
+ runtime,
625
+ storedSession,
626
+ {
627
+ promptDeliveryActive: codexAppServerPromptDeliveries.has(
628
+ namespace(normalizedSessionId)
629
+ )
630
+ }
631
+ );
632
+ const session = abandonedClaim.session;
633
+ if (abandonedClaim.recovered) {
634
+ return session;
635
+ }
636
+ const turn = turnState(session);
637
+ if (!["active", "starting"].includes(turn.state) || !turn.threadId) {
638
+ return session;
639
+ }
640
+ const reconciledSession = await reconcileCodexAppServerActiveTurn(session, {
641
+ provider,
642
+ runtime
643
+ });
644
+ const currentTurn = turnState(reconciledSession);
645
+ if (currentTurn.state === "starting" && currentTurn.threadId) {
646
+ scheduleCodexAppServerActiveRecovery(normalizedSessionId);
647
+ }
648
+ return reconciledSession;
649
+ } catch (error) {
650
+ debugLog("appServerTurn.reconcile.error", {
651
+ error: debugError(error),
652
+ sessionId: normalizedSessionId
653
+ });
654
+ if (!retryOnError) {
655
+ throw error;
656
+ }
657
+ const store = await Promise.resolve()
658
+ .then(() => createStore(normalizedSessionId))
659
+ .catch(() => null);
660
+ const run = store
661
+ ? await readCodexAppServerAgentRunForSession(store, normalizedSessionId).catch(() => null)
662
+ : null;
663
+ if (turnStateFromAgentRun(run || {}).state === "starting") {
664
+ scheduleCodexAppServerActiveRecovery(normalizedSessionId);
665
+ }
666
+ return null;
667
+ }
668
+ }
669
+
670
+ async function completeCodexAppServerTurn(sessionId = "", threadId = "", turnId = "", {
671
+ provider = null,
672
+ status = "completed",
673
+ verifyInactive = true
674
+ } = {}) {
675
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
676
+ const normalizedThreadId = normalizeCodexRunText(threadId);
677
+ const normalizedTurnId = normalizeCodexRunText(turnId);
678
+ const normalizedStatus = normalizeCodexRunText(status) || "completed";
679
+ const runtime = await createRuntime();
680
+ const session = await runtime.getSession(normalizedSessionId);
681
+ const existingTurn = turnState(session);
682
+ const continuesOwnedGoal = codexAppServerTurnOwnsActiveGoal(
683
+ existingTurn,
684
+ normalizedThreadId
685
+ );
686
+ if (codexAppServerTurnAwaitsProviderIdentity(existingTurn, normalizedThreadId, normalizedTurnId)) {
687
+ return {
688
+ ok: true,
689
+ processed: false,
690
+ reason: "turn_identity_pending"
691
+ };
692
+ }
693
+ if (!normalizedTurnId) {
694
+ if (!codexAppServerTurnCanReceiveProviderCompletion(existingTurn, normalizedThreadId, "")) {
695
+ cleanupCodexAppServerUntrackedTurn(normalizedThreadId, normalizedTurnId);
696
+ debugLog("appServerTurn.complete.stale", {
697
+ currentState: existingTurn.state,
698
+ currentStatus: existingTurn.status,
699
+ currentThreadId: existingTurn.threadId,
700
+ currentTurnId: existingTurn.turnId,
701
+ sessionId: normalizedSessionId,
702
+ status: normalizedStatus,
703
+ threadId: normalizedThreadId,
704
+ turnId: normalizedTurnId
705
+ });
706
+ return {
707
+ ok: true,
708
+ processed: false,
709
+ reason: "stale_turn_state"
710
+ };
711
+ }
712
+ if (verifyInactive && await codexAppServerProviderBlocksTurnRelease(normalizedSessionId, provider, normalizedThreadId, normalizedTurnId, {
713
+ source: "complete_missing_turn"
714
+ })) {
715
+ return {
716
+ ok: true,
717
+ processed: false,
718
+ reason: "provider_still_active",
719
+ status: "inProgress"
720
+ };
721
+ }
722
+ await markCodexAppServerTurnIdle(normalizedSessionId, {
723
+ status: normalizedStatus,
724
+ threadId: normalizedThreadId,
725
+ turnId: normalizedTurnId
726
+ });
727
+ return {
728
+ ok: true,
729
+ processed: false,
730
+ reason: "missing_turn"
731
+ };
732
+ }
733
+ if (!codexAppServerTurnCanReceiveProviderCompletion(existingTurn, normalizedThreadId, normalizedTurnId)) {
734
+ codexAppServerFinalizedTurns.add(codexAppServerResultFinalizationKey(
735
+ normalizedSessionId,
736
+ normalizedThreadId,
737
+ normalizedTurnId
738
+ ));
739
+ cleanupCodexAppServerUntrackedTurn(normalizedThreadId, normalizedTurnId);
740
+ debugLog("appServerTurn.complete.stale", {
741
+ currentState: existingTurn.state,
742
+ currentStatus: existingTurn.status,
743
+ currentThreadId: existingTurn.threadId,
744
+ currentTurnId: existingTurn.turnId,
745
+ sessionId: normalizedSessionId,
746
+ status: normalizedStatus,
747
+ threadId: normalizedThreadId,
748
+ turnId: normalizedTurnId
749
+ });
750
+ return {
751
+ ok: true,
752
+ processed: false,
753
+ reason: "stale_turn_state"
754
+ };
755
+ }
756
+ if (verifyInactive && await codexAppServerProviderBlocksTurnRelease(normalizedSessionId, provider, normalizedThreadId, normalizedTurnId, {
757
+ source: "complete"
758
+ })) {
759
+ return {
760
+ ok: true,
761
+ processed: false,
762
+ reason: "provider_still_active",
763
+ status: "inProgress"
764
+ };
765
+ }
766
+ if (codexAppServerRunInputSource(codexAppServerAgentRun(session)) === "terminal") {
767
+ codexAppServerCompletedTurns.add(codexAppServerTurnKey(normalizedThreadId, normalizedTurnId));
768
+ cleanupCodexAppServerUntrackedTurn(normalizedThreadId, normalizedTurnId);
769
+ await markCodexAppServerTurnIdle(normalizedSessionId, {
770
+ status: normalizedStatus,
771
+ threadId: normalizedThreadId,
772
+ turnId: normalizedTurnId
773
+ });
774
+ return {
775
+ ok: true,
776
+ processed: true,
777
+ reason: "terminal_turn_completed"
778
+ };
779
+ }
780
+ const alreadyFinalizing = existingTurn.state === "finalizing" &&
781
+ existingTurn.threadId === normalizedThreadId &&
782
+ existingTurn.turnId === normalizedTurnId;
783
+ codexAppServerCompletedTurns.add(codexAppServerTurnKey(normalizedThreadId, normalizedTurnId));
784
+ if (!alreadyFinalizing) {
785
+ await markCodexAppServerTurnFinalizing(normalizedSessionId, {
786
+ status: normalizedStatus,
787
+ threadId: normalizedThreadId,
788
+ turnId: normalizedTurnId
789
+ });
790
+ }
791
+ if (continuesOwnedGoal) {
792
+ // Keep the outer chat turn active so the next provider turn can adopt
793
+ // its chat ownership without exposing this internal final as an answer.
794
+ clearCodexAppServerFinalizingTimer(
795
+ normalizedSessionId,
796
+ normalizedThreadId,
797
+ normalizedTurnId
798
+ );
799
+ return {
800
+ ok: true,
801
+ processed: true,
802
+ reason: "goal_continuation_pending"
803
+ };
804
+ }
805
+ return recoverCodexAppServerFinalizingTurn(
806
+ normalizedSessionId,
807
+ normalizedThreadId,
808
+ normalizedTurnId,
809
+ {
810
+ status: normalizedStatus
811
+ }
812
+ );
813
+ }
814
+
815
+ function codexAppServerStoppedTurnMessage(status = "", error = "") {
816
+ const normalizedStatus = normalizeCodexRunText(status);
817
+ const base = normalizedStatus === "interrupted"
818
+ ? "Codex app-server was interrupted before completing this turn."
819
+ : "Codex app-server failed before completing this turn.";
820
+ const normalizedError = normalizeCodexRunText(error);
821
+ return normalizedError ? `${base} ${normalizedError}` : base;
822
+ }
823
+
824
+ async function stopCodexAppServerTurnWithProviderFailure(sessionId = "", threadId = "", turnId = "", {
825
+ error = "",
826
+ ok = false,
827
+ outcome = turnOutcomes.PROVIDER_FAILURE,
828
+ provider = null,
829
+ status = "failed",
830
+ usageLimitExceeded = false,
831
+ verifyInactive = true
832
+ } = {}) {
833
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
834
+ const normalizedThreadId = normalizeCodexRunText(threadId);
835
+ const normalizedTurnId = normalizeCodexRunText(turnId);
836
+ const normalizedStatus = normalizeCodexRunText(status) || "failed";
837
+ const runtime = await createRuntime();
838
+ const session = await runtime.getSession(normalizedSessionId);
839
+ const turn = turnState(session);
840
+ if (!codexAppServerTurnCanReceiveProviderCompletion(turn, normalizedThreadId, normalizedTurnId)) {
841
+ debugLog("appServerTurn.failure.stale", {
842
+ currentState: turn.state,
843
+ currentStatus: turn.status,
844
+ currentThreadId: turn.threadId,
845
+ currentTurnId: turn.turnId,
846
+ sessionId: normalizedSessionId,
847
+ status: normalizedStatus,
848
+ threadId: normalizedThreadId,
849
+ turnId: normalizedTurnId
850
+ });
851
+ return {
852
+ ok: true,
853
+ processed: false,
854
+ reason: "stale_turn_state"
855
+ };
856
+ }
857
+ const recovered = await recoverCodexAppServerFinalResponseBeforeOutcome(
858
+ normalizedSessionId,
859
+ normalizedThreadId,
860
+ normalizedTurnId,
861
+ normalizedStatus
862
+ );
863
+ if (recovered?.processed) {
864
+ return recovered;
865
+ }
866
+ // History recovery awaited the provider. Confirm activity again before
867
+ // publishing a failure, because the goal may have continued in that gap.
868
+ if (verifyInactive && await codexAppServerProviderBlocksTurnRelease(normalizedSessionId, provider, normalizedThreadId, normalizedTurnId, {
869
+ source: "provider_failure"
870
+ })) {
871
+ return {
872
+ ok: true,
873
+ processed: false,
874
+ reason: "provider_still_active",
875
+ status: "inProgress"
876
+ };
877
+ }
878
+ const message = codexAppServerStoppedTurnMessage(normalizedStatus, error);
879
+ await outcomeNotice(
880
+ runtime,
881
+ normalizedSessionId,
882
+ normalizedThreadId,
883
+ normalizedTurnId,
884
+ outcome,
885
+ error,
886
+ { usageLimitExceeded }
887
+ );
888
+ await markCodexAppServerTurnIdle(normalizedSessionId, {
889
+ error: message,
890
+ status: normalizedStatus,
891
+ threadId: normalizedThreadId,
892
+ turnId: normalizedTurnId,
893
+ turnOutcome: outcome
894
+ });
895
+ cleanupCodexAppServerUntrackedTurn(normalizedThreadId, normalizedTurnId);
896
+ return {
897
+ ok,
898
+ error: message,
899
+ status: normalizedStatus
900
+ };
901
+ }
902
+
903
+ async function stopCodexAppServerTurnWithResultDeliveryFailure(sessionId = "", threadId = "", turnId = "", {
904
+ error = "",
905
+ reason = "",
906
+ status = "completed"
907
+ } = {}) {
908
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
909
+ const normalizedThreadId = normalizeCodexRunText(threadId);
910
+ const normalizedStatus = normalizeCodexRunText(status) || "completed";
911
+ const normalizedTurnId = await resolveCodexAppServerTurnId(normalizedSessionId, normalizedThreadId, turnId);
912
+ const runtime = await createRuntime();
913
+ const currentSession = await runtime.getSession(normalizedSessionId);
914
+ const currentTurn = turnState(currentSession);
915
+ const recovered = await recoverCodexAppServerFinalResponseBeforeOutcome(
916
+ normalizedSessionId,
917
+ normalizedThreadId,
918
+ normalizedTurnId,
919
+ normalizedStatus
920
+ );
921
+ if (recovered?.processed) {
922
+ return recovered;
923
+ }
924
+ if (!codexAppServerTurnCanReceiveProviderCompletion(currentTurn, normalizedThreadId, normalizedTurnId)) {
925
+ codexAppServerFinalizedTurns.add(codexAppServerResultFinalizationKey(
926
+ normalizedSessionId,
927
+ normalizedThreadId,
928
+ normalizedTurnId
929
+ ));
930
+ cleanupCodexAppServerUntrackedTurn(normalizedThreadId, normalizedTurnId);
931
+ debugLog("appServerAgentResult.missing.stale", {
932
+ currentState: currentTurn.state,
933
+ currentStatus: currentTurn.status,
934
+ currentThreadId: currentTurn.threadId,
935
+ currentTurnId: currentTurn.turnId,
936
+ reason: normalizeCodexRunText(reason),
937
+ sessionId: normalizedSessionId,
938
+ status: normalizedStatus,
939
+ threadId: normalizedThreadId,
940
+ turnId: normalizedTurnId
941
+ });
942
+ return {
943
+ ok: true,
944
+ processed: false,
945
+ reason: "stale_turn_state",
946
+ status: currentTurn.status
947
+ };
948
+ }
949
+ const message = resultDeliveryFailureMessage({
950
+ error
951
+ });
952
+ await outcomeNotice(
953
+ runtime,
954
+ normalizedSessionId,
955
+ normalizedThreadId,
956
+ normalizedTurnId,
957
+ turnOutcomes.RESPONSE_DELIVERY_FAILURE
958
+ );
959
+ await markCodexAppServerTurnIdle(normalizedSessionId, {
960
+ error: message,
961
+ status: normalizedStatus,
962
+ threadId: normalizedThreadId,
963
+ turnId: normalizedTurnId,
964
+ turnOutcome: turnOutcomes.RESPONSE_DELIVERY_FAILURE
965
+ });
966
+ cleanupCodexAppServerUntrackedTurn(normalizedThreadId, normalizedTurnId);
967
+ debugLog("appServerAgentResult.missing", {
968
+ error: normalizeCodexRunText(error),
969
+ reason: normalizeCodexRunText(reason),
970
+ sessionId: normalizedSessionId,
971
+ threadId: normalizedThreadId,
972
+ turnId: normalizedTurnId
973
+ });
974
+ return {
975
+ ok: false,
976
+ error: message,
977
+ status: normalizedStatus
978
+ };
979
+ }
980
+
981
+ return {
982
+ scheduleActiveRecovery: scheduleCodexAppServerActiveRecovery,
983
+ markTurnActive: markCodexAppServerTurnActive,
984
+ markProviderTurnActive: markCodexAppServerProviderTurnActive,
985
+ recoverFinalizingTurn: recoverCodexAppServerFinalizingTurn,
986
+ recoverActiveTurn: recoverCodexAppServerActiveTurn,
987
+ completeTurn: completeCodexAppServerTurn,
988
+ stopTurnWithProviderFailure: stopCodexAppServerTurnWithProviderFailure
989
+ };
990
+ }