@jskit-ai/assistant-core 0.2.7 → 0.2.9

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 (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +13 -10
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -0,0 +1,610 @@
1
+ import { codexAppServerTurnStatusIsActive, codexAppServerThreadStatus, codexAppServerReadThreadStatus, codexAppServerAgentRun, codexAppServerTurnOwnsActiveGoal, codexAppServerTurnCanReceiveProviderCompletion, codexAppServerTurnAwaitsProviderIdentity, codexAppServerTurnCanReceiveProviderActivity, codexAppServerTurnKey } from "./codexTurnState.js";
2
+ import { codexAppServerThreadTurnId } from "./codexThreadSnapshot.js";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+
5
+ /**
6
+ * How a tracked turn ends: it completes, or it stops after a provider failure or a reply that could not be
7
+ * delivered. Each ending checks that the provider has released the turn first.
8
+ * @param {object} dependencies
9
+ * @param {import("./codexRunJournal.js").CodexRunJournal} dependencies.journal
10
+ * @param {import("./codexOutput.js").CodexAppServerOutput} dependencies.output
11
+ * @param {import("./codexResultSettlement.js").CodexResultSettlement} dependencies.settlement
12
+ * @param {import("./codexTurn.js").CodexCreateRuntime} dependencies.createRuntime
13
+ * @param {import("./codexTurn.js").CodexDebugLog} dependencies.debugLog
14
+ * @param {import("./codexTurn.js").CodexDebugError} dependencies.debugError
15
+ * @param {import("./codexTurn.js").CodexTurnStateReader} dependencies.turnState
16
+ * @param {number} dependencies.failureDetailGraceMs
17
+ * @param {import("./codexTurn.js").CodexTurnOutcomes} dependencies.turnOutcomes
18
+ * @param {(...details: any[]) => Promise<unknown>} dependencies.outcomeNotice
19
+ * @param {(details: { error?: string }) => string} dependencies.resultDeliveryFailureMessage
20
+ * @param {import("./codexTurn.js").CodexCaptureContext} dependencies.captureContext
21
+ * @param {import("./codexTurn.js").CodexRunInContext} dependencies.runInContext
22
+ * @param {import("./codexNotificationQueue.js").CodexAppServerNotificationQueue} dependencies.notificationQueue
23
+ * @param {boolean} dependencies.finalizingGraceAfterHistoryRead
24
+ * @param {(sessionId: string, delayMs?: number) => void} dependencies.scheduleActiveRecovery
25
+ */
26
+ export function createCodexTurnEndings({
27
+ journal,
28
+ output,
29
+ settlement,
30
+ createRuntime,
31
+ debugLog,
32
+ debugError,
33
+ turnState,
34
+ failureDetailGraceMs,
35
+ turnOutcomes,
36
+ outcomeNotice,
37
+ resultDeliveryFailureMessage,
38
+ captureContext,
39
+ runInContext,
40
+ notificationQueue,
41
+ finalizingGraceAfterHistoryRead,
42
+ scheduleActiveRecovery
43
+ }) {
44
+ const retainFinalizingTimer = finalizingGraceAfterHistoryRead || failureDetailGraceMs > 0;
45
+
46
+ async function codexAppServerProviderBlocksTurnRelease(sessionId = "", provider = null, threadId = "", turnId = "", {
47
+ source = ""
48
+ } = {}) {
49
+ const normalizedSessionId = normalizeText(sessionId);
50
+ const normalizedThreadId = normalizeText(threadId);
51
+ const normalizedTurnId = normalizeText(turnId);
52
+ if (!normalizedSessionId || !normalizedThreadId) {
53
+ return false;
54
+ }
55
+ const runtime = await createRuntime();
56
+ const session = await runtime.getSession(normalizedSessionId);
57
+ const turn = turnState(session);
58
+ if (!turn.active || !codexAppServerTurnCanReceiveProviderActivity(turn, normalizedThreadId, normalizedTurnId)) {
59
+ return false;
60
+ }
61
+ try {
62
+ const providerThread = await codexAppServerReadThreadStatus(provider, normalizedThreadId, {
63
+ observeLatestTurn: true
64
+ });
65
+ if (!providerThread) {
66
+ return false;
67
+ }
68
+ const status = codexAppServerThreadStatus(providerThread);
69
+ const providerTurnId = codexAppServerThreadTurnId(providerThread);
70
+ if (
71
+ providerTurnId &&
72
+ providerTurnId !== normalizeText(turn.turnId)
73
+ ) {
74
+ debugLog("appServerTurn.releaseAllowedSuccessor", {
75
+ providerTurnId,
76
+ sessionId: normalizedSessionId,
77
+ source: normalizeText(source),
78
+ threadId: normalizedThreadId,
79
+ turnId: turn.turnId
80
+ });
81
+ return false;
82
+ }
83
+ if (!codexAppServerTurnStatusIsActive(status)) {
84
+ return false;
85
+ }
86
+ debugLog("appServerTurn.releaseBlockedActive", {
87
+ sessionId: normalizedSessionId,
88
+ source: normalizeText(source),
89
+ status,
90
+ threadId: normalizedThreadId,
91
+ turnId: normalizedTurnId || turn.turnId
92
+ });
93
+ scheduleActiveRecovery(normalizedSessionId);
94
+ return true;
95
+ } catch (error) {
96
+ debugLog("appServerTurn.releaseCheck.error", {
97
+ error: debugError(error),
98
+ sessionId: normalizedSessionId,
99
+ source: normalizeText(source),
100
+ threadId: normalizedThreadId,
101
+ turnId: normalizedTurnId || turn.turnId
102
+ });
103
+ return false;
104
+ }
105
+ }
106
+
107
+ async function completeCodexAppServerTurn(sessionId = "", threadId = "", turnId = "", {
108
+ provider = null,
109
+ status = "completed",
110
+ verifyInactive = true
111
+ } = {}) {
112
+ const normalizedSessionId = normalizeText(sessionId);
113
+ const normalizedThreadId = normalizeText(threadId);
114
+ const normalizedTurnId = normalizeText(turnId);
115
+ const normalizedStatus = normalizeText(status) || "completed";
116
+ const runtime = await createRuntime();
117
+ const session = await runtime.getSession(normalizedSessionId);
118
+ const existingTurn = turnState(session);
119
+ const continuesOwnedGoal = codexAppServerTurnOwnsActiveGoal(
120
+ existingTurn,
121
+ normalizedThreadId
122
+ );
123
+ if (codexAppServerTurnAwaitsProviderIdentity(existingTurn, normalizedThreadId, normalizedTurnId)) {
124
+ return {
125
+ ok: true,
126
+ processed: false,
127
+ reason: "turn_identity_pending"
128
+ };
129
+ }
130
+ if (!normalizedTurnId) {
131
+ if (!codexAppServerTurnCanReceiveProviderCompletion(existingTurn, normalizedThreadId, "")) {
132
+ output.cleanupUntrackedTurn(normalizedThreadId, normalizedTurnId);
133
+ debugLog("appServerTurn.complete.stale", {
134
+ currentState: existingTurn.state,
135
+ currentStatus: existingTurn.status,
136
+ currentThreadId: existingTurn.threadId,
137
+ currentTurnId: existingTurn.turnId,
138
+ sessionId: normalizedSessionId,
139
+ status: normalizedStatus,
140
+ threadId: normalizedThreadId,
141
+ turnId: normalizedTurnId
142
+ });
143
+ return {
144
+ ok: true,
145
+ processed: false,
146
+ reason: "stale_turn_state"
147
+ };
148
+ }
149
+ if (verifyInactive && await codexAppServerProviderBlocksTurnRelease(normalizedSessionId, provider, normalizedThreadId, normalizedTurnId, {
150
+ source: "complete_missing_turn"
151
+ })) {
152
+ return {
153
+ ok: true,
154
+ processed: false,
155
+ reason: "provider_still_active",
156
+ status: "inProgress"
157
+ };
158
+ }
159
+ await journal.markTurnIdle(normalizedSessionId, {
160
+ status: normalizedStatus,
161
+ threadId: normalizedThreadId,
162
+ turnId: normalizedTurnId
163
+ });
164
+ return {
165
+ ok: true,
166
+ processed: false,
167
+ reason: "missing_turn"
168
+ };
169
+ }
170
+ if (!codexAppServerTurnCanReceiveProviderCompletion(existingTurn, normalizedThreadId, normalizedTurnId)) {
171
+ output.finalizedTurns.add(output.resultFinalizationKey(
172
+ normalizedSessionId,
173
+ normalizedThreadId,
174
+ normalizedTurnId
175
+ ));
176
+ output.cleanupUntrackedTurn(normalizedThreadId, normalizedTurnId);
177
+ debugLog("appServerTurn.complete.stale", {
178
+ currentState: existingTurn.state,
179
+ currentStatus: existingTurn.status,
180
+ currentThreadId: existingTurn.threadId,
181
+ currentTurnId: existingTurn.turnId,
182
+ sessionId: normalizedSessionId,
183
+ status: normalizedStatus,
184
+ threadId: normalizedThreadId,
185
+ turnId: normalizedTurnId
186
+ });
187
+ return {
188
+ ok: true,
189
+ processed: false,
190
+ reason: "stale_turn_state"
191
+ };
192
+ }
193
+ if (verifyInactive && await codexAppServerProviderBlocksTurnRelease(normalizedSessionId, provider, normalizedThreadId, normalizedTurnId, {
194
+ source: "complete"
195
+ })) {
196
+ return {
197
+ ok: true,
198
+ processed: false,
199
+ reason: "provider_still_active",
200
+ status: "inProgress"
201
+ };
202
+ }
203
+ if (journal.runInputSource(codexAppServerAgentRun(session)) === "terminal") {
204
+ journal.completedTurns.add(codexAppServerTurnKey(normalizedThreadId, normalizedTurnId));
205
+ output.cleanupUntrackedTurn(normalizedThreadId, normalizedTurnId);
206
+ await journal.markTurnIdle(normalizedSessionId, {
207
+ status: normalizedStatus,
208
+ threadId: normalizedThreadId,
209
+ turnId: normalizedTurnId
210
+ });
211
+ return {
212
+ ok: true,
213
+ processed: true,
214
+ reason: "terminal_turn_completed"
215
+ };
216
+ }
217
+ const alreadyFinalizing = existingTurn.state === "finalizing" &&
218
+ existingTurn.threadId === normalizedThreadId &&
219
+ existingTurn.turnId === normalizedTurnId;
220
+ journal.completedTurns.add(codexAppServerTurnKey(normalizedThreadId, normalizedTurnId));
221
+ if (!alreadyFinalizing) {
222
+ await journal.markTurnFinalizing(normalizedSessionId, {
223
+ status: normalizedStatus,
224
+ threadId: normalizedThreadId,
225
+ turnId: normalizedTurnId
226
+ });
227
+ }
228
+ if (continuesOwnedGoal) {
229
+ // Keep the outer chat turn active so the next provider turn can adopt
230
+ // its chat ownership without exposing this internal final as an answer.
231
+ journal.clearFinalizingTimer(
232
+ normalizedSessionId,
233
+ normalizedThreadId,
234
+ normalizedTurnId
235
+ );
236
+ return {
237
+ ok: true,
238
+ processed: true,
239
+ reason: "goal_continuation_pending"
240
+ };
241
+ }
242
+ return recoverCodexAppServerFinalizingTurn(
243
+ normalizedSessionId,
244
+ normalizedThreadId,
245
+ normalizedTurnId,
246
+ {
247
+ status: normalizedStatus
248
+ }
249
+ );
250
+ }
251
+
252
+ function codexAppServerStoppedTurnMessage(status = "", error = "", outcome = "") {
253
+ const normalizedStatus = normalizeText(status);
254
+ let base = "Codex app-server failed before completing this turn.";
255
+ if (normalizedStatus === "interrupted") {
256
+ base = outcome === turnOutcomes.CONTROL_RECONFIGURATION
257
+ ? "Codex app-server was interrupted while its controls were restored."
258
+ : "Codex app-server was interrupted before completing this turn.";
259
+ }
260
+ const normalizedError = normalizeText(error);
261
+ return normalizedError ? `${base} ${normalizedError}` : base;
262
+ }
263
+
264
+ async function stopCodexAppServerTurnWithProviderFailure(sessionId = "", threadId = "", turnId = "", {
265
+ error = "",
266
+ ok = false,
267
+ outcome = "",
268
+ provider = null,
269
+ status = "failed",
270
+ usageLimitExceeded = false,
271
+ verifyInactive = true,
272
+ deferFailureDetails = false,
273
+ failureDetailReady = false
274
+ } = {}) {
275
+ const observedAt = Date.now();
276
+ const normalizedSessionId = normalizeText(sessionId);
277
+ const normalizedThreadId = normalizeText(threadId);
278
+ const normalizedTurnId = normalizeText(turnId);
279
+ const normalizedStatus = normalizeText(status) || "failed";
280
+ let resolvedOutcome = normalizeText(outcome);
281
+ if (!resolvedOutcome) {
282
+ if (normalizedStatus === "interrupted") {
283
+ const recorded = provider ? provider.interruptionOutcome(normalizedThreadId, normalizedTurnId) : "";
284
+ resolvedOutcome = [turnOutcomes.USER_CANCELLED, turnOutcomes.CONTROL_RECONFIGURATION].includes(recorded)
285
+ ? recorded : turnOutcomes.INTERRUPTED;
286
+ } else {
287
+ resolvedOutcome = turnOutcomes.PROVIDER_FAILURE;
288
+ }
289
+ }
290
+ const runtime = await createRuntime();
291
+ const staleFailure = async () => {
292
+ const turn = turnState(await runtime.getSession(normalizedSessionId));
293
+ if (codexAppServerTurnCanReceiveProviderCompletion(turn, normalizedThreadId, normalizedTurnId)) {
294
+ return null;
295
+ }
296
+ debugLog("appServerTurn.failure.stale", {
297
+ currentState: turn.state,
298
+ currentStatus: turn.status,
299
+ currentThreadId: turn.threadId,
300
+ currentTurnId: turn.turnId,
301
+ sessionId: normalizedSessionId,
302
+ status: normalizedStatus,
303
+ threadId: normalizedThreadId,
304
+ turnId: normalizedTurnId
305
+ });
306
+ return {
307
+ ok: true,
308
+ processed: false,
309
+ reason: "stale_turn_state"
310
+ };
311
+ };
312
+ const staleBeforeRecovery = await staleFailure();
313
+ if (staleBeforeRecovery) {
314
+ return staleBeforeRecovery;
315
+ }
316
+ const key = output.resultFinalizationKey(normalizedSessionId, normalizedThreadId, normalizedTurnId);
317
+ const pendingTimer = journal.finalizingTimers.get(key);
318
+ const pendingDetail = pendingTimer?.failureDetail;
319
+ if (failureDetailGraceMs > 0 && !failureDetailReady && (deferFailureDetails || pendingDetail) &&
320
+ ![turnOutcomes.USER_CANCELLED, turnOutcomes.CONTROL_RECONFIGURATION].includes(resolvedOutcome)) {
321
+ if (!pendingDetail) {
322
+ scheduleCodexAppServerFinalizingRecovery(normalizedSessionId, normalizedThreadId, normalizedTurnId, {
323
+ failureDetail: { error, ok, outcome: resolvedOutcome, provider, status: normalizedStatus,
324
+ usageLimitExceeded, verifyInactive,
325
+ expiresAt: Math.min(observedAt + failureDetailGraceMs,
326
+ pendingTimer?.expiresAt ?? Infinity) }
327
+ });
328
+ }
329
+ return { ok: true, processed: false, reason: "failure_detail_pending", status: normalizedStatus };
330
+ }
331
+ if (resolvedOutcome === turnOutcomes.PROVIDER_FAILURE) {
332
+ // Saved output is still useful, but cannot turn a known provider failure
333
+ // into successful completion. Persist it without settling the turn;
334
+ // the failure notice and checkpoint below own the terminal outcome.
335
+ await output.submitAssistantResult(normalizedSessionId, normalizedThreadId, normalizedTurnId, {
336
+ recoverFromProvider: true
337
+ });
338
+ } else {
339
+ const recovered = await settlement.recoverFinalResponseBeforeOutcome(
340
+ normalizedSessionId,
341
+ normalizedThreadId,
342
+ normalizedTurnId,
343
+ normalizedStatus
344
+ );
345
+ if (recovered?.processed) {
346
+ return recovered;
347
+ }
348
+ }
349
+ // History recovery awaited the provider. Confirm ownership and activity
350
+ // again before publishing a failure: a successor, Stop or goal
351
+ // continuation may own the run after that gap.
352
+ const staleAfterRecovery = await staleFailure();
353
+ if (staleAfterRecovery) {
354
+ return staleAfterRecovery;
355
+ }
356
+ if (verifyInactive && await codexAppServerProviderBlocksTurnRelease(normalizedSessionId, provider, normalizedThreadId, normalizedTurnId, {
357
+ source: "provider_failure"
358
+ })) {
359
+ return {
360
+ ok: true,
361
+ processed: false,
362
+ reason: "provider_still_active",
363
+ status: "inProgress"
364
+ };
365
+ }
366
+ const message = codexAppServerStoppedTurnMessage(normalizedStatus, error, resolvedOutcome);
367
+ await outcomeNotice(
368
+ runtime,
369
+ normalizedSessionId,
370
+ normalizedThreadId,
371
+ normalizedTurnId,
372
+ resolvedOutcome,
373
+ error,
374
+ { usageLimitExceeded }
375
+ );
376
+ await journal.markTurnIdle(normalizedSessionId, {
377
+ error: message,
378
+ status: normalizedStatus,
379
+ threadId: normalizedThreadId,
380
+ turnId: normalizedTurnId,
381
+ turnOutcome: resolvedOutcome
382
+ });
383
+ output.cleanupUntrackedTurn(normalizedThreadId, normalizedTurnId);
384
+ return {
385
+ ok,
386
+ error: message,
387
+ status: normalizedStatus
388
+ };
389
+ }
390
+
391
+ async function stopCodexAppServerTurnWithResultDeliveryFailure(sessionId = "", threadId = "", turnId = "", {
392
+ error = "",
393
+ reason = "",
394
+ status = "completed"
395
+ } = {}) {
396
+ const normalizedSessionId = normalizeText(sessionId);
397
+ const normalizedThreadId = normalizeText(threadId);
398
+ const normalizedStatus = normalizeText(status) || "completed";
399
+ const normalizedTurnId = await journal.resolveTurnId(normalizedSessionId, normalizedThreadId, turnId);
400
+ const recovered = await settlement.recoverFinalResponseBeforeOutcome(
401
+ normalizedSessionId,
402
+ normalizedThreadId,
403
+ normalizedTurnId,
404
+ normalizedStatus
405
+ );
406
+ if (recovered?.processed) {
407
+ return recovered;
408
+ }
409
+ // The history read can outlive this turn: a successor or Stop may own the
410
+ // run by now, so the outcome is decided against the run read after it.
411
+ const runtime = await createRuntime();
412
+ const currentTurn = turnState(await runtime.getSession(normalizedSessionId));
413
+ if (!codexAppServerTurnCanReceiveProviderCompletion(currentTurn, normalizedThreadId, normalizedTurnId)) {
414
+ output.finalizedTurns.add(output.resultFinalizationKey(
415
+ normalizedSessionId,
416
+ normalizedThreadId,
417
+ normalizedTurnId
418
+ ));
419
+ output.cleanupUntrackedTurn(normalizedThreadId, normalizedTurnId);
420
+ debugLog("appServerAgentResult.missing.stale", {
421
+ currentState: currentTurn.state,
422
+ currentStatus: currentTurn.status,
423
+ currentThreadId: currentTurn.threadId,
424
+ currentTurnId: currentTurn.turnId,
425
+ reason: normalizeText(reason),
426
+ sessionId: normalizedSessionId,
427
+ status: normalizedStatus,
428
+ threadId: normalizedThreadId,
429
+ turnId: normalizedTurnId
430
+ });
431
+ return {
432
+ ok: true,
433
+ processed: false,
434
+ reason: "stale_turn_state",
435
+ status: currentTurn.status
436
+ };
437
+ }
438
+ const message = resultDeliveryFailureMessage({
439
+ error
440
+ });
441
+ await outcomeNotice(
442
+ runtime,
443
+ normalizedSessionId,
444
+ normalizedThreadId,
445
+ normalizedTurnId,
446
+ turnOutcomes.RESPONSE_DELIVERY_FAILURE
447
+ );
448
+ await journal.markTurnIdle(normalizedSessionId, {
449
+ error: message,
450
+ status: normalizedStatus,
451
+ threadId: normalizedThreadId,
452
+ turnId: normalizedTurnId,
453
+ turnOutcome: turnOutcomes.RESPONSE_DELIVERY_FAILURE
454
+ });
455
+ output.cleanupUntrackedTurn(normalizedThreadId, normalizedTurnId);
456
+ debugLog("appServerAgentResult.missing", {
457
+ error: normalizeText(error),
458
+ reason: normalizeText(reason),
459
+ sessionId: normalizedSessionId,
460
+ threadId: normalizedThreadId,
461
+ turnId: normalizedTurnId
462
+ });
463
+ return {
464
+ ok: false,
465
+ error: message,
466
+ status: normalizedStatus
467
+ };
468
+ }
469
+
470
+ function scheduleCodexAppServerFinalizingRecovery(sessionId = "", threadId = "", turnId = "", {
471
+ completedAt = "",
472
+ status = "completed",
473
+ updatedAt = "",
474
+ failureDetail = null
475
+ } = {}) {
476
+ const normalizedSessionId = normalizeText(sessionId);
477
+ const normalizedThreadId = normalizeText(threadId);
478
+ const normalizedTurnId = normalizeText(turnId);
479
+ if (!normalizedSessionId || !normalizedThreadId || !normalizedTurnId) {
480
+ return;
481
+ }
482
+ journal.clearFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
483
+ const key = output.resultFinalizationKey(normalizedSessionId, normalizedThreadId, normalizedTurnId);
484
+ const projectContext = captureContext();
485
+ const delayMs = failureDetail
486
+ ? Math.max(0, failureDetail.expiresAt - Date.now())
487
+ : journal.finalizingRemainingMs({ completedAt, state: "finalizing", updatedAt });
488
+ // One record per pending deadline: `recovery` is the in-flight expiry
489
+ // (retained-timer policy only), `expiresAt` the first deadline that a later
490
+ // failure-detail grace may not exceed, `failureDetail` a deferred provider
491
+ // failure, and `startedAt` the post-read clock that rereads must keep.
492
+ const entry = {
493
+ timer: null,
494
+ recovery: null,
495
+ expiresAt: failureDetailGraceMs > 0 ? Date.now() + delayMs : null,
496
+ failureDetail,
497
+ startedAt: !failureDetail && finalizingGraceAfterHistoryRead ? completedAt : ""
498
+ };
499
+ entry.timer = setTimeout(() => {
500
+ if (failureDetail) {
501
+ notificationQueue.run({ projectContext, provider: failureDetail.provider, sessionId: normalizedSessionId }, async () => {
502
+ if (journal.finalizingTimers.get(key) !== entry) return;
503
+ try {
504
+ return await stopCodexAppServerTurnWithProviderFailure(normalizedSessionId, normalizedThreadId, normalizedTurnId,
505
+ { ...failureDetail, failureDetailReady: true });
506
+ } finally {
507
+ if (journal.finalizingTimers.get(key) === entry) {
508
+ journal.clearFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
509
+ }
510
+ }
511
+ });
512
+ return;
513
+ }
514
+ // The post-read policy keeps its first clock while expiry rereads history.
515
+ if (!retainFinalizingTimer) journal.finalizingTimers.delete(key);
516
+ const recovery = runInContext(
517
+ projectContext,
518
+ () => recoverCodexAppServerFinalizingTurn(
519
+ normalizedSessionId,
520
+ normalizedThreadId,
521
+ normalizedTurnId,
522
+ { status, timerRecovery: finalizingGraceAfterHistoryRead }
523
+ )
524
+ );
525
+ if (retainFinalizingTimer) {
526
+ entry.recovery = recovery;
527
+ void recovery.then(
528
+ () => { if (entry.recovery === recovery) entry.recovery = null; },
529
+ error => {
530
+ if (entry.recovery === recovery) entry.recovery = null;
531
+ debugLog("appServerFinalizingRecovery.error", {
532
+ error: debugError(error), sessionId: normalizedSessionId,
533
+ threadId: normalizedThreadId, turnId: normalizedTurnId
534
+ });
535
+ }
536
+ );
537
+ } else {
538
+ void recovery;
539
+ }
540
+ }, delayMs);
541
+ entry.timer.unref();
542
+ journal.finalizingTimers.set(key, entry);
543
+ }
544
+
545
+ async function recoverCodexAppServerFinalizingTurn(sessionId = "", threadId = "", turnId = "", {
546
+ status = "completed", timerRecovery = false
547
+ } = {}) {
548
+ const normalizedSessionId = normalizeText(sessionId);
549
+ const normalizedThreadId = normalizeText(threadId);
550
+ const normalizedTurnId = normalizeText(turnId);
551
+ const key = output.resultFinalizationKey(normalizedSessionId, normalizedThreadId, normalizedTurnId);
552
+ const priorTimer = retainFinalizingTimer ? journal.finalizingTimers.get(key) : null;
553
+ if (!timerRecovery && priorTimer?.recovery) return priorTimer.recovery;
554
+ const runtime = await createRuntime();
555
+ const session = await runtime.getSession(normalizedSessionId);
556
+ const turn = turnState(session);
557
+ if (codexAppServerTurnOwnsActiveGoal(turn, normalizedThreadId)) {
558
+ journal.clearFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
559
+ return { ok: true, processed: false, reason: "goal_continuation_pending" };
560
+ }
561
+ const result = await settlement.finalizeAssistantResult(normalizedSessionId, normalizedThreadId, normalizedTurnId,
562
+ { recoverFromProvider: true, status });
563
+ if (result?.processed) {
564
+ return result;
565
+ }
566
+ // A later failure owns the same first deadline, even when this successful
567
+ // history read was already in flight under the completion-based clock.
568
+ const currentTimer = journal.finalizingTimers.get(key);
569
+ if (currentTimer?.failureDetail) return result;
570
+ if (retainFinalizingTimer && priorTimer && !currentTimer) return result;
571
+ let graceTurn = turn;
572
+ if (finalizingGraceAfterHistoryRead) {
573
+ const readReturnedAt = new Date().toISOString();
574
+ const currentTurn = turnState(await runtime.getSession(normalizedSessionId));
575
+ const timer = journal.finalizingTimers.get(key);
576
+ if (timer?.failureDetail || (priorTimer && !timer)) return result;
577
+ if (currentTurn.state !== "finalizing" || currentTurn.threadId !== normalizedThreadId ||
578
+ currentTurn.turnId !== normalizedTurnId) {
579
+ journal.clearFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
580
+ return { ...result, reason: "stale_turn_state" };
581
+ }
582
+ if (codexAppServerTurnOwnsActiveGoal(currentTurn, normalizedThreadId)) {
583
+ journal.clearFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
584
+ return { ok: true, processed: false, reason: "goal_continuation_pending" };
585
+ }
586
+ if (!timerRecovery && timer?.recovery) return timer.recovery;
587
+ // This local clock never changes the native completion timestamp.
588
+ graceTurn = { ...currentTurn, completedAt: timer?.startedAt || readReturnedAt };
589
+ }
590
+ if (!journal.finalizingExpired(graceTurn)) {
591
+ scheduleCodexAppServerFinalizingRecovery(normalizedSessionId, normalizedThreadId, normalizedTurnId, {
592
+ completedAt: graceTurn.completedAt,
593
+ status,
594
+ updatedAt: graceTurn.updatedAt
595
+ });
596
+ return result;
597
+ }
598
+ return stopCodexAppServerTurnWithResultDeliveryFailure(normalizedSessionId, normalizedThreadId, normalizedTurnId,
599
+ { error: result?.error, reason: result?.reason || "missing_assistant_text", status });
600
+ }
601
+
602
+ return Object.freeze({
603
+ completeTurn: completeCodexAppServerTurn,
604
+ stopTurnWithProviderFailure: stopCodexAppServerTurnWithProviderFailure,
605
+ stopTurnWithResultDeliveryFailure: stopCodexAppServerTurnWithResultDeliveryFailure,
606
+ recoverFinalizingTurn: recoverCodexAppServerFinalizingTurn
607
+ });
608
+ }
609
+
610
+ /** @typedef {ReturnType<typeof createCodexTurnEndings>} CodexTurnEndings */