@jskit-ai/assistant-core 0.1.199 → 0.1.201

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,1082 @@
1
+ import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, normalizeCodexRunText, codexAppServerErrorMessage, codexAppServerSteerFailure, codexAppServerInterruptFailure, codexAppServerConversationTurnIsActive } from "./codexTurnState.js";
2
+ import { CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS, createCodexAppServerDetachedTurnWatcher } from "./codexDetachedTurn.js";
3
+ import { assertCodexAppServerHelperAccountIdentity, readCodexAppServerAccountIdentity, sendPreparedCodexAppServerHelperTurn } from "./codexHelperExecution.js";
4
+ import { codexAppServerRequestIsInvalid, codexAppServerThreadHasReadableHistory, codexAppServerThreadIsMissing, codexLocalImageInput, sendCodexAppServerPrompt } from "./codexProvider.js";
5
+ import { codexAppServerProjectHookTrustConfig, readCodexToolFreeConfiguration } from "./codexConfiguration.js";
6
+ import { codexAppServerAssistantItemText, codexAppServerContentText, classifyCodexAppServerEvent, codexAppServerNotificationThreadId, codexAppServerNotificationTurnId, codexAppServerNotificationTurnStatus, codexAppServerErrorText, codexAppServerThreadRawValue, codexAppServerProviderThreadAssistantSegments, codexAppServerProviderTurnId, codexAppServerProviderTurnStatus, codexAppServerProviderTurnItems, codexAppServerProviderTurnClientIds, codexAppServerThreadTurnId, codexAppServerThreadError } from "./codexEvents.js";
7
+
8
+ // Internal command facet of the original run owner; the same native conversation
9
+ // and turn-start collections retain their original lifetime and public references.
10
+ export function createCodexConversationCommands({
11
+ checkpoint,
12
+ codexAppServerConversationMessageId,
13
+ codexAppServerStreamMessage,
14
+ conversationPreparation,
15
+ ensureCodexAppServerProviderForActiveTurn,
16
+ errorPrefix,
17
+ helperLifecycle,
18
+ helperOwnershipError,
19
+ interruptFailedCode,
20
+ namespace,
21
+ providerSessions,
22
+ steerFailedCode
23
+ }) {
24
+ const codexAppServerConversations = new Map();
25
+ const codexAppServerConversationTurnStarts = new Map();
26
+
27
+ function codexAppServerConversation(sessionId = "", conversationId = "") {
28
+ return codexAppServerConversations.get(
29
+ namespace(sessionId)
30
+ )?.get(conversationId) || null;
31
+ }
32
+
33
+ function hasActiveConversation(sessionId) {
34
+ const conversations = codexAppServerConversations.get(
35
+ namespace(sessionId)
36
+ );
37
+ return Boolean(conversations && [...conversations.values()].some((conversation) => (
38
+ codexAppServerConversationTurnIsActive(conversation.status) ||
39
+ conversation.goal?.status === "active" && !conversation.provider.observationFailure
40
+ )));
41
+ }
42
+
43
+ async function withCodexAppServerConversationTurnStart(sessionId, input, operation) {
44
+ const messageId = normalizeCodexRunText(input.messageId);
45
+ const deliveryKey = messageId
46
+ ? `${namespace(sessionId)}\0${normalizeCodexRunText(input.conversationId)}\0${messageId}`
47
+ : "";
48
+ const existing = deliveryKey ? codexAppServerConversationTurnStarts.get(deliveryKey) : null;
49
+ if (existing) {
50
+ return existing;
51
+ }
52
+ const start = operation();
53
+ if (deliveryKey) {
54
+ codexAppServerConversationTurnStarts.set(deliveryKey, start);
55
+ }
56
+ try {
57
+ return await start;
58
+ } finally {
59
+ if (deliveryKey && codexAppServerConversationTurnStarts.get(deliveryKey) === start) {
60
+ codexAppServerConversationTurnStarts.delete(deliveryKey);
61
+ }
62
+ }
63
+ }
64
+
65
+ function observeCodexConversation(sessionId, conversationId, provider) {
66
+ const key = namespace(sessionId);
67
+ const conversations = codexAppServerConversations.get(key) || new Map();
68
+ let state = conversations.get(conversationId);
69
+ if (state?.provider === provider && state.unsubscribe) return state;
70
+ state?.unsubscribe?.();
71
+ state = { conversationId, provider, persistent: true, status: "ready", runId: "", error: "" };
72
+ state.unsubscribe = provider.subscribe((notification) => {
73
+ if (codexAppServerNotificationThreadId(notification) !== conversationId) return;
74
+ const turnId = codexAppServerNotificationTurnId(notification);
75
+ if (turnId) state.runId = turnId;
76
+ const status = codexAppServerNotificationTurnStatus(notification);
77
+ if (status) state.status = status;
78
+ const classification = classifyCodexAppServerEvent(notification);
79
+ if (notification.method === "item/agentMessage/delta") {
80
+ state.onEvent?.({ type: "text", text: notification.params.delta,
81
+ messageId: notification.params.itemId, threadId: conversationId, turnId,
82
+ message: codexAppServerStreamMessage(classification) });
83
+ } else if (["assistant_started", "live_progress", "final_assistant_result"].includes(classification.kind)) {
84
+ state.onEvent?.({ type: "message", threadId: conversationId, turnId,
85
+ message: { ...codexAppServerStreamMessage(classification),
86
+ ...(classification.kind !== "assistant_started" ? { text: classification.text } : {}) } });
87
+ }
88
+ if (notification.method === "thread/goal/updated") state.goal = notification.params.goal;
89
+ if (notification.method === "thread/goal/cleared") state.goal = null;
90
+ if (notification.method === "turn/completed") {
91
+ state.onEvent = null;
92
+ const completedTurnId = state.runId;
93
+ const completedStatus = state.status;
94
+ void checkpoint(sessionId, {
95
+ outerTurnId: `codex:${conversationId}:${completedTurnId}`,
96
+ threadId: conversationId,
97
+ turnId: completedTurnId,
98
+ status: completedStatus
99
+ }).catch(() => {});
100
+ }
101
+ });
102
+ conversations.set(conversationId, state);
103
+ codexAppServerConversations.set(key, conversations);
104
+ return state;
105
+ }
106
+
107
+ async function readPersistentCodexConversation(sessionId, input, context) {
108
+ const conversationId = normalizeCodexRunText(input.conversationId);
109
+ const previous = codexAppServerConversation(sessionId, conversationId);
110
+ const state = observeCodexConversation(sessionId, conversationId, context.provider);
111
+ const thread = await context.provider.readThreadStatus(conversationId);
112
+ if (codexAppServerThreadRawValue(thread).historyMode !== "paginated") {
113
+ throw Object.assign(new Error("This conversation uses an unsupported Codex history format. Start a fresh conversation."), {
114
+ code: `${errorPrefix}codex_history_unsupported`
115
+ });
116
+ }
117
+ const turns = [];
118
+ let cursor;
119
+ do {
120
+ const page = await context.provider.listThreadTurns(conversationId, {
121
+ limit: 100, itemsView: "full", sortDirection: "asc", ...(cursor ? { cursor } : {})
122
+ });
123
+ turns.push(...page.data);
124
+ cursor = page.nextCursor;
125
+ } while (cursor);
126
+ const latest = turns.at(-1);
127
+ const runId = codexAppServerProviderTurnId(latest || {});
128
+ let status = codexAppServerThreadStatus(thread);
129
+ if (!codexAppServerConversationTurnIsActive(status) && latest) status = codexAppServerProviderTurnStatus(latest);
130
+ let goal = (await context.provider.readGoal(conversationId)).goal;
131
+ // A new backend observer cannot assume it saw the work preceding connection.
132
+ // Stop it once; browser reloads reuse the existing backend observer.
133
+ if (previous?.provider !== context.provider && (codexAppServerConversationTurnIsActive(status) || goal?.status === "active")) {
134
+ Object.assign(state, { runId, status: "inProgress", goal });
135
+ try {
136
+ await context.provider.stopThreadForObservationLoss(conversationId, "");
137
+ } catch (error) {
138
+ await context.provider.failObservation(error);
139
+ if (codexAppServerConversationTurnIsActive(state.status)) throw error;
140
+ }
141
+ status = "interrupted";
142
+ goal = (await context.provider.readGoal(conversationId)).goal;
143
+ }
144
+ if (goal?.status === "active") status = "inProgress";
145
+ Object.assign(state, { runId, status, goal });
146
+ const messages = [];
147
+ for (const turn of turns) {
148
+ const turnId = codexAppServerProviderTurnId(turn);
149
+ const complete = !codexAppServerConversationTurnIsActive(codexAppServerProviderTurnStatus(turn));
150
+ const items = codexAppServerProviderTurnItems(turn);
151
+ const finalIds = new Set(codexAppServerProviderThreadAssistantSegments({ turns: [turn] }, turnId).map(({ itemId }) => itemId));
152
+ for (const [index, item] of items.entries()) {
153
+ let role;
154
+ if (item.type === "reasoning") role = "thinking";
155
+ else if (item.type === "agentMessage") {
156
+ const progress = item.phase === "commentary" || (!item.phase && !finalIds.has(item.id) && (complete || index < items.length - 1));
157
+ role = progress ? "commentary" : "assistant";
158
+ }
159
+ else continue;
160
+ const text = role === "thinking"
161
+ ? normalizeCodexRunText(codexAppServerContentText(item.summary)) || codexAppServerContentText(item.content)
162
+ : codexAppServerAssistantItemText(item);
163
+ if (text) messages.push({
164
+ id: codexAppServerConversationMessageId(conversationId, turnId, role, item.id || text),
165
+ ...(item.id && role !== "thinking" ? { outputId: codexAppServerConversationMessageId(conversationId, turnId, "assistant-item", item.id) } : {}),
166
+ role,
167
+ text,
168
+ complete
169
+ });
170
+ }
171
+ }
172
+ const latestText = latest ? codexAppServerProviderThreadAssistantSegments({ turns: [latest] }, runId)
173
+ .map(({ text }) => text).join("\n\n") : "";
174
+ return {
175
+ conversationId,
176
+ ok: true,
177
+ status,
178
+ runId,
179
+ messages,
180
+ goal,
181
+ admitted: Boolean(input.messageId && turns.some((turn) => codexAppServerProviderTurnClientIds(turn).includes(input.messageId))),
182
+ rawText: latestText.trim(),
183
+ error: state.error || codexAppServerThreadError(thread)
184
+ };
185
+ }
186
+
187
+ function codexAppServerEphemeralConversationSnapshot(state = {}) {
188
+ return {
189
+ conversationId: normalizeCodexRunText(state.conversationId),
190
+ error: normalizeCodexRunText(state.error),
191
+ message: normalizeCodexRunText(state.message),
192
+ ok: true,
193
+ outcome: state.outcome || null,
194
+ progressUpdates: Array.isArray(state.progressUpdates)
195
+ ? state.progressUpdates.map((update = {}) => ({
196
+ id: normalizeCodexRunText(update.id),
197
+ text: normalizeCodexRunText(update.text)
198
+ })).filter((update) => update.id && update.text)
199
+ : [],
200
+ rawText: normalizeCodexRunText(state.rawText),
201
+ messageId: normalizeCodexRunText(state.messageId),
202
+ runId: normalizeCodexRunText(state.runId),
203
+ status: normalizeCodexRunText(state.status) || "ready",
204
+ turnMetadata: state.turnMetadata || null
205
+ };
206
+ }
207
+
208
+ async function codexAppServerConversationResult(operation) {
209
+ try {
210
+ return await operation();
211
+ } catch (error) {
212
+ return conversationPreparation.failure(error);
213
+ }
214
+ }
215
+
216
+ async function prepareCodexAppServerConversationThread(provider, preparation, helperIsolation) {
217
+ if (preparation.inspection) {
218
+ const enforcement = await helperIsolation.inspect(provider, preparation.inspection);
219
+ return preparation.prepared(enforcement);
220
+ }
221
+ const config = preparation.projectHooks
222
+ ? await codexAppServerProjectHookTrustConfig(provider, preparation.workdir)
223
+ : null;
224
+ return preparation.settings(config);
225
+ }
226
+
227
+ async function acquireCodexAppServerConversationContext(sessionId, prepared, input = {}) {
228
+ if (prepared.result) return prepared.result;
229
+ const { context } = prepared;
230
+ if (context.assistantScope) {
231
+ const providerOptions = prepared.providerOptions;
232
+ const provider = await providerSessions.owner.ensureSession(
233
+ providerSessions.context(normalizeCodexRunText(sessionId), providerOptions)
234
+ );
235
+ const result = prepared.project(provider);
236
+ result.isolationConfig = await readCodexToolFreeConfiguration(
237
+ provider, context.workdir, conversationPreparation.isolation.assertCompatibility
238
+ );
239
+ return result;
240
+ }
241
+ let helperRestore = null;
242
+ if (prepared.helperTurn) {
243
+ const restoration = await prepared.helperRestoration;
244
+ helperRestore = helperLifecycle.assertRestored(
245
+ await helperLifecycle.restoreThreads(restoration.projectRuntimeRoot, restoration.context)
246
+ );
247
+ } else {
248
+ const threadId = normalizeCodexRunText(input.threadId || input.codexSessionId || input.conversationId);
249
+ if (threadId) {
250
+ await helperLifecycle.assertOrdinaryThreadAvailable(prepared.projectRuntimeRoot, threadId);
251
+ }
252
+ }
253
+ const managedIdentity = prepared.managedIdentity;
254
+ const restoredAdmissionError = conversationPreparation.admissionError(sessionId);
255
+ if (restoredAdmissionError) throw restoredAdmissionError;
256
+ const activeProvider = prepared.helperTurn
257
+ ? null
258
+ : await ensureCodexAppServerProviderForActiveTurn(context.session, managedIdentity);
259
+ const activeProviderAdmissionError = conversationPreparation.admissionError(sessionId);
260
+ if (activeProviderAdmissionError) throw activeProviderAdmissionError;
261
+ const providerOptions = activeProvider ? null : await prepared.providerOptions;
262
+ const providerOptionsAdmissionError = conversationPreparation.admissionError(sessionId);
263
+ if (providerOptionsAdmissionError) throw providerOptionsAdmissionError;
264
+ const provider = activeProvider?.provider || await providerSessions.owner.ensureSession(
265
+ providerSessions.context(normalizeCodexRunText(sessionId), providerOptions)
266
+ );
267
+ const providerAdmissionError = conversationPreparation.admissionError(sessionId);
268
+ if (providerAdmissionError) throw providerAdmissionError;
269
+ return prepared.project(provider, activeProvider?.providerOptions || providerOptions, helperRestore);
270
+ }
271
+
272
+ async function codexAppServerConversationContext(sessionId, input, options) {
273
+ return acquireCodexAppServerConversationContext(sessionId,
274
+ await conversationPreparation.context(sessionId, input, options), input);
275
+ }
276
+
277
+ async function codexAppServerEphemeralScopeContext(sessionId, input, scope) {
278
+ return acquireCodexAppServerConversationContext(sessionId,
279
+ await conversationPreparation.scope(sessionId, input, scope), input);
280
+ }
281
+
282
+ async function readPreparedPersistentCodexConversation(sessionId, input, context) {
283
+ const result = await readPersistentCodexConversation(sessionId, input, context);
284
+ return { ...result, ...conversationPreparation.response(result.rawText) };
285
+ }
286
+
287
+ async function createCodexAppServerConversation(sessionId, input = {}, options = {}) {
288
+ return codexAppServerConversationResult(async () => {
289
+ const context = await codexAppServerConversationContext(sessionId, input, options);
290
+ if (context.ok === false) return context;
291
+ const execution = conversationPreparation.execution(sessionId, input, options, context, null, "create");
292
+ return createPreparedCodexAppServerConversation(sessionId, input, {
293
+ provider: context.provider,
294
+ executionProfile: execution.executionProfile,
295
+ helperIsolation: conversationPreparation.isolation,
296
+ threadPreparation: () => execution.threadPreparation
297
+ });
298
+ });
299
+ }
300
+
301
+ async function startCodexAppServerConversationTurn(sessionId, input = {}, options = {}) {
302
+ const messageId = normalizeCodexRunText(input.messageId);
303
+ return withCodexAppServerConversationTurnStart(sessionId, input, () => codexAppServerConversationResult(async () => {
304
+ const conversationId = normalizeCodexRunText(input.conversationId);
305
+ const prompt = normalizeCodexRunText(input.message || input.prompt);
306
+ if (!conversationId || !prompt) {
307
+ return {
308
+ code: `${errorPrefix}agent_conversation_turn_input_required`,
309
+ error: "Assistant conversation turns require a conversation and message.",
310
+ ok: false
311
+ };
312
+ }
313
+ const context = await codexAppServerConversationContext(sessionId, input, options);
314
+ if (context.ok === false) return context;
315
+ const conversationState = codexAppServerConversation(sessionId, conversationId);
316
+ const execution = conversationPreparation.execution(sessionId, input, options, context, conversationState, "start", { conversationId, prompt });
317
+ if (input.ephemeral === true && !conversationState) {
318
+ return {
319
+ ...conversationPreparation.expired(conversationId, input),
320
+ code: `${errorPrefix}temporary_conversation_expired`,
321
+ ok: false,
322
+ };
323
+ }
324
+ // Do not evaluate native preparation until the original duplicate/active
325
+ // checks and Helper isolation capability checks have run.
326
+ return startPreparedCodexAppServerConversationTurn(sessionId, input, {
327
+ executionProfile: execution.executionProfile,
328
+ helperIsolation: conversationPreparation.isolation,
329
+ prepareTurn: execution.prepareTurn,
330
+ authorized: execution.authorized,
331
+ readOnly: execution.readOnly,
332
+ actorMetadata: execution.actorMetadata,
333
+ onEvent: execution.onEvent,
334
+ projectResult: execution.projectResult,
335
+ conversationId,
336
+ messageId,
337
+ prompt,
338
+ provider: context.provider,
339
+ conversationState,
340
+ threadPreparation: () => execution.threadPreparation
341
+ });
342
+ }));
343
+ }
344
+
345
+ async function readCodexAppServerConversation(sessionId, input = {}, options = {}) {
346
+ return codexAppServerConversationResult(async () => {
347
+ const conversationId = normalizeCodexRunText(input.conversationId);
348
+ if (!conversationId) {
349
+ return {
350
+ code: `${errorPrefix}agent_conversation_id_required`,
351
+ error: "Assistant conversation id is required.",
352
+ ok: false
353
+ };
354
+ }
355
+ const conversationState = codexAppServerConversation(sessionId, conversationId);
356
+ if (conversationState && !input.persistent) {
357
+ return codexAppServerEphemeralConversationSnapshot(conversationState);
358
+ }
359
+ if (input.ephemeral === true) {
360
+ return conversationPreparation.expired(conversationId, input);
361
+ }
362
+ if (input.persistent && conversationState?.persistent && conversationState.provider.isAvailable?.() &&
363
+ !conversationState.provider.observationFailure && !conversationPreparation.admissionError(sessionId)) {
364
+ // Poll the already observed native thread. Execution environment setup
365
+ // belongs to Send and reconnection, not every history read.
366
+ return readPreparedPersistentCodexConversation(sessionId, input, { provider: conversationState.provider });
367
+ }
368
+ const context = await codexAppServerConversationContext(sessionId, input, options);
369
+ if (context.ok === false) {
370
+ return context;
371
+ }
372
+ if (input.persistent) return readPreparedPersistentCodexConversation(sessionId, input, context);
373
+ const thread = await context.provider.readThread(conversationId);
374
+ const runId = normalizeCodexRunText(input.runId) || codexAppServerThreadTurnId(thread);
375
+ const status = codexAppServerThreadStatus(thread);
376
+ const text = runId
377
+ ? codexAppServerProviderThreadAssistantSegments(thread, runId)
378
+ .map((segment) => segment.text)
379
+ .join("\n\n")
380
+ : "";
381
+ return {
382
+ conversationId,
383
+ error: codexAppServerThreadError(thread),
384
+ ok: true,
385
+ runId,
386
+ status,
387
+ ...conversationPreparation.response(text)
388
+ };
389
+ });
390
+ }
391
+
392
+ async function waitForCodexAppServerConversationTurn(sessionId, input = {}, {
393
+ assistantScope = null,
394
+ onEvent = null
395
+ } = {}) {
396
+ return codexAppServerConversationResult(async () => {
397
+ const conversationId = normalizeCodexRunText(input.conversationId);
398
+ const runId = normalizeCodexRunText(input.runId);
399
+ if (!conversationId || !runId) {
400
+ return {
401
+ code: `${errorPrefix}agent_conversation_run_required`,
402
+ error: "Waiting for an assistant conversation requires conversation and run ids.",
403
+ ok: false
404
+ };
405
+ }
406
+ const scoped = assistantScope && codexAppServerConversation(sessionId, conversationId);
407
+ if (scoped?.executionProfile) {
408
+ if (scoped.runId !== runId || !scoped.completion) throw new Error("The scoped helper turn is unavailable.");
409
+ const result = await scoped.completion;
410
+ return { ...result, conversationId, ok: true, runId };
411
+ }
412
+ const context = await codexAppServerConversationContext(sessionId, input, {
413
+ assistantScope
414
+ });
415
+ if (context.ok === false) {
416
+ return context;
417
+ }
418
+ const result = await waitForPreparedCodexAppServerConversationTurn(sessionId, input, {
419
+ provider: context.provider,
420
+ onEvent
421
+ });
422
+ return { ...result, ...conversationPreparation.response(result.rawText) };
423
+ });
424
+ }
425
+
426
+ async function stopCodexAppServerConversation(sessionId, input = {}, options = {}) {
427
+ const conversationId = normalizeCodexRunText(input.conversationId);
428
+ const conversationState = codexAppServerConversation(sessionId, conversationId);
429
+ if (conversationState?.provider.observationFailure) {
430
+ await conversationState.provider.failObservation(conversationState.provider.observationFailure);
431
+ if (codexAppServerConversationTurnIsActive(conversationState.status)) {
432
+ throw new Error("Codex's stop is not yet confirmed.");
433
+ }
434
+ return { conversationId, ok: true, runId: conversationState.runId, status: "interrupted" };
435
+ }
436
+ if (input.ephemeral === true && !conversationState) {
437
+ return {
438
+ conversationExpired: true,
439
+ conversationId,
440
+ ok: true,
441
+ runId: normalizeCodexRunText(input.runId),
442
+ status: "interrupted"
443
+ };
444
+ }
445
+ if (input.persistent) {
446
+ const context = await codexAppServerConversationContext(sessionId, input, options);
447
+ if (context.ok === false) return context;
448
+ return stopPersistentCodexConversation({
449
+ conversationId,
450
+ conversationState,
451
+ provider: context.provider
452
+ });
453
+ }
454
+ const result = await interruptDetachedCodexAppServerConversationTurn(sessionId, {
455
+ agentSettings: input.agentSettings,
456
+ threadId: input.conversationId,
457
+ turnId: input.runId
458
+ }, options, conversationState);
459
+ return {
460
+ ...result,
461
+ conversationId,
462
+ runId: normalizeCodexRunText(input.runId)
463
+ };
464
+ }
465
+
466
+ async function deleteScopedCodexAppServerConversation(sessionId, input = {}, options = {}) {
467
+ const conversationId = normalizeCodexRunText(input.conversationId);
468
+ const sessionKey = namespace(sessionId);
469
+ const conversations = codexAppServerConversations.get(sessionKey);
470
+ const conversationExpired = input.ephemeral === true && !conversations?.has(conversationId) &&
471
+ !(options.assistantScope && input.executionProfile);
472
+ let result;
473
+ if (conversationExpired) {
474
+ result = {
475
+ conversationExpired: true
476
+ };
477
+ } else {
478
+ result = await deleteDetachedCodexAppServerConversationThread(sessionId, {
479
+ agentSettings: input.agentSettings,
480
+ threadId: input.conversationId
481
+ }, options, { conversationId, sessionKey, conversations });
482
+ if (result.ok === false) {
483
+ return { ...result, conversationId };
484
+ }
485
+ }
486
+ let providerExit = null;
487
+ if (options.assistantScope) {
488
+ const context = await codexAppServerEphemeralScopeContext(
489
+ sessionId,
490
+ input,
491
+ options.assistantScope
492
+ );
493
+ if (context.ok === false) {
494
+ return context;
495
+ }
496
+ const connection = providerSessions.context(normalizeCodexRunText(sessionId), context.providerOptions);
497
+ providerExit = await providerSessions.owner.stopProvider(connection.providerKey, context.providerOptions, {
498
+ preserveProcessExitProof: false,
499
+ requireStopped: true,
500
+ get runtimeHost() { return connection.runtimeHost; }
501
+ });
502
+ }
503
+ return {
504
+ ...result,
505
+ conversationId,
506
+ ok: conversationExpired ? providerExit?.ok !== false : result?.ok !== false,
507
+ ...(providerExit ? { providerExit } : {})
508
+ };
509
+ }
510
+
511
+ async function closeCodexAppServerConversations(sessionId) {
512
+ const key = namespace(sessionId);
513
+ for (const state of codexAppServerConversations.get(key)?.values() || []) {
514
+ if (state.persistent) {
515
+ await stopCodexAppServerConversation(sessionId, { conversationId: state.conversationId, persistent: true });
516
+ state.unsubscribe?.();
517
+ } else {
518
+ await deleteScopedCodexAppServerConversation(sessionId, { conversationId: state.conversationId }).catch(() => null);
519
+ }
520
+ }
521
+ codexAppServerConversations.delete(key);
522
+ }
523
+
524
+ async function codexAppServerConversationControl(sessionId, input, {
525
+ assistantScope = null,
526
+ runtime = null,
527
+ session = null
528
+ } = {}, operation, dispatch) {
529
+ return codexAppServerConversationResult(async () => {
530
+ const prepared = conversationPreparation.control(sessionId, input, operation);
531
+ if (prepared.result) return prepared.result;
532
+ const { admission, threadId, turnId } = prepared;
533
+ try {
534
+ let context = null;
535
+ try {
536
+ context = await codexAppServerConversationContext(sessionId, input, { assistantScope, runtime, session });
537
+ } catch (error) {
538
+ const unavailable = prepared.unavailable();
539
+ if (unavailable) return unavailable;
540
+ throw error;
541
+ }
542
+ if (context.ok === false) return context;
543
+ const unavailable = prepared.unavailable();
544
+ if (unavailable) return unavailable;
545
+ const policyResult = helperLifecycle?.inspectControl(operation, input, {
546
+ sessionId, threadId, turnId,
547
+ get retiredThreadIds() { return context.helperRestore?.retiredThreadIds; },
548
+ get projectRuntimeRoot() { return context.runtime?.stateRoot; },
549
+ get provider() { return context.provider; },
550
+ get workdir() { return context.workdir; }
551
+ });
552
+ // The original Helper-retirement return releases admission before its
553
+ // promise settles; only the native dispatch below is awaited here.
554
+ if (policyResult !== undefined) return policyResult;
555
+ if (operation === "interrupt") {
556
+ const unavailable = prepared.unavailable();
557
+ if (unavailable) return unavailable;
558
+ } else if (typeof context.provider.deleteThread !== "function") {
559
+ return {
560
+ code: `${errorPrefix}codex_detached_thread_delete_unavailable`,
561
+ error: "Codex app-server thread deletion is not available.",
562
+ ok: false,
563
+ statusCode: 409,
564
+ threadId
565
+ };
566
+ }
567
+ return await dispatch({ provider: context.provider, threadId, turnId });
568
+ } finally {
569
+ admission.release();
570
+ }
571
+ });
572
+ }
573
+
574
+ async function interruptDetachedCodexAppServerConversationTurn(sessionId, input = {}, options = {}, conversationState = null) {
575
+ return interruptCodexAppServerConversation(conversationState, dispatch =>
576
+ codexAppServerConversationControl(sessionId, input, options, "interrupt", dispatch));
577
+ }
578
+
579
+ async function deleteDetachedCodexAppServerConversationThread(sessionId, input = {}, options = {}, conversation = null) {
580
+ return deleteCodexAppServerConversation(conversation, dispatch =>
581
+ codexAppServerConversationControl(sessionId, input, options, "delete", dispatch));
582
+ }
583
+
584
+ async function createPreparedCodexAppServerConversation(sessionId, input, {
585
+ provider,
586
+ executionProfile,
587
+ helperIsolation,
588
+ threadPreparation
589
+ } = {}) {
590
+ const prepareThread = () => prepareCodexAppServerConversationThread(provider, threadPreparation(), helperIsolation);
591
+ const thread = executionProfile
592
+ ? (await helperIsolation.start(provider, prepareThread)).thread
593
+ : await provider.startThread({
594
+ ...await prepareThread(),
595
+ ...(input.ephemeral === true ? { ephemeral: true } : {})
596
+ });
597
+ const conversationId = normalizeCodexRunText(thread.id || thread.response?.thread?.id);
598
+ if (!conversationId) {
599
+ throw new Error("Codex app-server did not return a conversation id.");
600
+ }
601
+ if (input.persistent === true) observeCodexConversation(sessionId, conversationId, provider);
602
+ if (input.ephemeral === true) {
603
+ const sessionKey = namespace(sessionId);
604
+ const conversations = codexAppServerConversations.get(sessionKey) || new Map();
605
+ conversations.set(conversationId, {
606
+ conversationId,
607
+ executionProfile,
608
+ provider,
609
+ error: "",
610
+ message: "",
611
+ messageId: "",
612
+ nextProgressSequence: 0,
613
+ outcome: null,
614
+ progressUpdates: [],
615
+ rawText: "",
616
+ runId: "",
617
+ status: "ready",
618
+ turnMetadata: null,
619
+ watcher: null
620
+ });
621
+ codexAppServerConversations.set(sessionKey, conversations);
622
+ }
623
+ return {
624
+ conversationId,
625
+ ok: true,
626
+ status: "ready"
627
+ };
628
+ }
629
+
630
+ async function steerCodexAppServerConversationTurn(input, {
631
+ conversationId,
632
+ conversationState,
633
+ messageId,
634
+ prompt,
635
+ provider
636
+ } = {}) {
637
+ const images = codexLocalImageInput(input.attachments || []);
638
+ const result = await provider.steerTurn(
639
+ conversationId,
640
+ conversationState.runId,
641
+ images.length ? [prompt, ...images] : prompt,
642
+ { clientUserMessageId: messageId }
643
+ );
644
+ const failure = codexAppServerSteerFailure(result, steerFailedCode);
645
+ if (failure) return { ...failure, ok: false };
646
+ conversationState.messageId = messageId;
647
+ return {
648
+ conversationId,
649
+ messageId,
650
+ ok: true,
651
+ runId: conversationState.runId,
652
+ status: "inProgress",
653
+ deliveryMode: "steer"
654
+ };
655
+ }
656
+
657
+ async function acquireCodexAppServerDetachedThread(provider, requestedThreadId, threadSettings, input = {}) {
658
+ let thread = null;
659
+ if (requestedThreadId) {
660
+ thread = await provider.resumeThread(requestedThreadId, threadSettings);
661
+ }
662
+ if (!thread) {
663
+ thread = await provider.startThread({
664
+ ...threadSettings,
665
+ ...(input.ephemeral === true ? { ephemeral: true } : {})
666
+ });
667
+ }
668
+ return thread;
669
+ }
670
+
671
+ function codexAppServerDetachedThreadId(thread, requestedThreadId) {
672
+ const threadId = normalizeCodexRunText(thread.id || thread.response?.thread?.id || requestedThreadId);
673
+ if (!threadId) {
674
+ throw new Error("Codex app-server did not return a detached chat thread id.");
675
+ }
676
+ return threadId;
677
+ }
678
+
679
+ async function dispatchCodexAppServerDetachedTurn({ provider, prompt, threadId }, authorized) {
680
+ const delivery = await sendCodexAppServerPrompt({ provider, prompt, threadId }, authorized);
681
+ const turnId = normalizeCodexRunText(delivery.turn?.id);
682
+ const status = normalizeCodexRunText(delivery.turn?.status || delivery.turn?.raw?.status);
683
+ return { delivery, status, turnId };
684
+ }
685
+
686
+ async function runDetachedCodexAppServerTurn({
687
+ provider,
688
+ threadId,
689
+ timeoutMs,
690
+ onEvent: emitDetachedEvent,
691
+ onFailure
692
+ } = {}, operation) {
693
+ const watcher = createCodexAppServerDetachedTurnWatcher(provider, threadId, {
694
+ onEvent: (classification) => {
695
+ emitDetachedEvent({
696
+ classification,
697
+ threadId,
698
+ turnId: classification.turnId,
699
+ type: "notification"
700
+ });
701
+ },
702
+ timeoutMs
703
+ });
704
+ const waitForResult = watcher.wait();
705
+ void waitForResult.catch(() => null);
706
+ const throwWatcherFailure = async (fallbackError, status = "", {
707
+ waitForDetail = false
708
+ } = {}) => {
709
+ if (waitForDetail) {
710
+ watcher.failAfterDetailGrace(fallbackError);
711
+ } else {
712
+ watcher.failNow(fallbackError);
713
+ }
714
+ const error = await waitForResult.then(
715
+ () => fallbackError,
716
+ (watcherError) => watcherError
717
+ );
718
+ throw await onFailure(error, status);
719
+ };
720
+ const { delivery, status, turnId } = await operation(throwWatcherFailure);
721
+ watcher.setTurnId(turnId);
722
+ emitDetachedEvent({
723
+ status,
724
+ threadId,
725
+ turnId,
726
+ type: "turn"
727
+ });
728
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
729
+ const providerError = codexAppServerErrorText(
730
+ delivery.turn?.raw?.error ||
731
+ delivery.turn?.response?.turn?.error ||
732
+ delivery.turn?.error
733
+ );
734
+ const error = new Error(providerError || `Codex app-server turn ${status}.`);
735
+ await throwWatcherFailure(error, status, {
736
+ waitForDetail: true
737
+ });
738
+ }
739
+ if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
740
+ await watcher.completeNow(status);
741
+ }
742
+ let result = null;
743
+ try {
744
+ result = await waitForResult;
745
+ } catch (error) {
746
+ throw await onFailure(error, status);
747
+ }
748
+ return result;
749
+ }
750
+
751
+ async function startPreparedCodexAppServerConversationTurn(sessionId, input, {
752
+ conversationId,
753
+ messageId,
754
+ prompt,
755
+ provider,
756
+ conversationState,
757
+ executionProfile,
758
+ actorMetadata,
759
+ onEvent,
760
+ projectResult,
761
+ helperIsolation,
762
+ threadPreparation,
763
+ prepareTurn,
764
+ authorized,
765
+ readOnly
766
+ } = {}) {
767
+ if (conversationState && messageId && conversationState.messageId === messageId) {
768
+ return codexAppServerEphemeralConversationSnapshot(conversationState);
769
+ }
770
+ if (conversationState && codexAppServerConversationTurnIsActive(conversationState.status)) {
771
+ if (input.steer === true) {
772
+ return steerCodexAppServerConversationTurn(input, {
773
+ conversationId,
774
+ conversationState,
775
+ messageId,
776
+ prompt,
777
+ provider
778
+ });
779
+ }
780
+ return {
781
+ code: `${errorPrefix}temporary_conversation_turn_active`,
782
+ error: "Temporary AI is already working on this conversation.",
783
+ ok: false
784
+ };
785
+ }
786
+ if (!conversationState || input.persistent === true) {
787
+ const prepareThread = () => prepareCodexAppServerConversationThread(provider, threadPreparation(), helperIsolation);
788
+ if (executionProfile) await helperIsolation.resume(provider, conversationId, prepareThread);
789
+ else await provider.resumeThread(conversationId, await prepareThread());
790
+ }
791
+ let admittedAccountIdentitySignature = "";
792
+ if (executionProfile) {
793
+ admittedAccountIdentitySignature = await readCodexAppServerAccountIdentity(provider, helperOwnershipError);
794
+ }
795
+ let watcher = null;
796
+ let waitForResult = null;
797
+ if (conversationState && !input.persistent) {
798
+ Object.assign(conversationState, {
799
+ error: "",
800
+ message: "",
801
+ messageId,
802
+ outcome: null,
803
+ progressUpdates: [],
804
+ rawText: "",
805
+ runId: "",
806
+ status: "starting",
807
+ turnMetadata: actorMetadata
808
+ });
809
+ watcher = createCodexAppServerDetachedTurnWatcher(provider, conversationId, {
810
+ includeThreadHistory: false,
811
+ timeoutMs: executionProfile?.limits.timeoutMs || 0,
812
+ onEvent(classification = {}) {
813
+ const current = codexAppServerConversation(sessionId, conversationId);
814
+ if (!current || (classification.turnId && current.runId && classification.turnId !== current.runId)) {
815
+ return;
816
+ }
817
+ onEvent?.(classification, current);
818
+ }
819
+ });
820
+ waitForResult = watcher.wait();
821
+ void waitForResult.catch(() => null);
822
+ conversationState.watcher = watcher;
823
+ }
824
+ let delivery = null;
825
+ if (input.persistent && conversationState) conversationState.onEvent = onEvent;
826
+ try {
827
+ await input.onPromptSending?.({ threadId: conversationId });
828
+ if (executionProfile) {
829
+ const prepared = prepareTurn();
830
+ delivery = await sendPreparedCodexAppServerHelperTurn(provider, prepared, helperIsolation);
831
+ } else {
832
+ delivery = await sendCodexAppServerPrompt({
833
+ clientUserMessageId: input.messageId,
834
+ outputSchema: input.outputSchema,
835
+ prompt,
836
+ attachments: input.attachments,
837
+ provider,
838
+ readOnly,
839
+ threadId: conversationId
840
+ }, authorized);
841
+ }
842
+ } catch (error) {
843
+ if (input.persistent && conversationState) conversationState.onEvent = null;
844
+ watcher?.failNow(error);
845
+ await waitForResult?.catch(() => null);
846
+ if (conversationState) {
847
+ Object.assign(conversationState, {
848
+ error: codexAppServerErrorMessage(error, "Temporary AI message could not be sent."),
849
+ status: "failed",
850
+ watcher: null
851
+ });
852
+ }
853
+ throw error;
854
+ }
855
+ const runId = normalizeCodexRunText(delivery.turn?.id);
856
+ const status = normalizeCodexRunText(delivery.turn?.status || delivery.turn?.raw?.status);
857
+ if (!runId) {
858
+ watcher?.failNow(new Error("Codex app-server accepted a conversation turn without returning its id."));
859
+ await waitForResult?.catch(() => null);
860
+ throw new Error("Codex app-server accepted a conversation turn without returning its id.");
861
+ }
862
+ if (conversationState && !input.persistent) {
863
+ let nativeCompleted = false;
864
+ Object.assign(conversationState, {
865
+ runId,
866
+ status: !executionProfile && codexAppServerTurnStatusIsSuccessfulComplete(status) ? status : "inProgress"
867
+ });
868
+ watcher.setTurnId(runId);
869
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
870
+ watcher.failNow(new Error(`Codex app-server turn ${status}.`));
871
+ } else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
872
+ await watcher.completeNow(status);
873
+ }
874
+ conversationState.completion = waitForResult.then(async (result = {}) => {
875
+ const current = codexAppServerConversation(sessionId, conversationId);
876
+ if (!current || current.runId !== runId) {
877
+ return;
878
+ }
879
+ nativeCompleted = true;
880
+ if (!executionProfile) current.status = result.status || "completed";
881
+ if (executionProfile) {
882
+ const completingRunId = current.runId;
883
+ await assertCodexAppServerHelperAccountIdentity(provider, admittedAccountIdentitySignature, helperOwnershipError);
884
+ if (codexAppServerConversation(sessionId, conversationId) !== current ||
885
+ current.runId !== completingRunId || !codexAppServerConversationTurnIsActive(current.status)) {
886
+ throw new Error("The scoped helper turn is unavailable.");
887
+ }
888
+ }
889
+ const response = projectResult(result);
890
+ Object.assign(current, {
891
+ error: "",
892
+ ...response,
893
+ status: result.status || "completed",
894
+ watcher: null
895
+ });
896
+ return codexAppServerEphemeralConversationSnapshot(current);
897
+ }).catch(async (error) => {
898
+ const current = codexAppServerConversation(sessionId, conversationId);
899
+ if (!current || current.runId !== runId || current.status === "interrupted") {
900
+ throw error;
901
+ }
902
+ if (executionProfile && !nativeCompleted && codexAppServerConversationTurnIsActive(current.status)) {
903
+ try {
904
+ const interrupted = await provider.interruptTurn(conversationId, runId);
905
+ const failure = codexAppServerInterruptFailure(interrupted, interruptFailedCode);
906
+ if (failure) throw new Error(failure.error);
907
+ } catch {
908
+ current.error = "The helper deadline ended, but Stop could not be confirmed. Retry Stop before reusing this helper.";
909
+ current.watcher = null;
910
+ throw new Error(current.error);
911
+ }
912
+ }
913
+ Object.assign(current, {
914
+ error: codexAppServerErrorMessage(error, "Temporary AI turn failed."),
915
+ status: "failed",
916
+ watcher: null
917
+ });
918
+ throw error;
919
+ });
920
+ void conversationState.completion.catch(() => null);
921
+ }
922
+ if (input.persistent && conversationState) Object.assign(conversationState, { runId, status, messageId });
923
+ return { conversationId, messageId, ok: true, runId, status };
924
+ }
925
+
926
+ async function interruptCodexAppServerConversation(conversationState, operation) {
927
+ const result = await operation(async ({ provider, threadId, turnId }) => {
928
+ const result = await provider.interruptTurn(threadId, turnId);
929
+ const interruptFailure = codexAppServerInterruptFailure(result, interruptFailedCode);
930
+ if (interruptFailure) {
931
+ return {
932
+ ...interruptFailure,
933
+ result,
934
+ threadId,
935
+ turnId
936
+ };
937
+ }
938
+ return {
939
+ ok: true,
940
+ result,
941
+ status: "interrupted",
942
+ threadId,
943
+ turnId
944
+ };
945
+ });
946
+ if (result.ok !== false && conversationState) {
947
+ conversationState.status = "interrupted";
948
+ conversationState.watcher?.failNow(new Error("Temporary AI turn was stopped."));
949
+ conversationState.watcher = null;
950
+ }
951
+ return result;
952
+ }
953
+
954
+ async function deleteCodexAppServerConversation(conversation, operation) {
955
+ const result = await operation(async ({ provider, threadId }) => {
956
+ try {
957
+ const result = await provider.deleteThread(threadId);
958
+ return {
959
+ ok: true,
960
+ result,
961
+ status: "deleted",
962
+ threadId
963
+ };
964
+ } catch (error) {
965
+ if (
966
+ codexAppServerRequestIsInvalid(error, "thread/delete") &&
967
+ !await codexAppServerThreadHasReadableHistory(provider, threadId)
968
+ ) {
969
+ return {
970
+ ok: true,
971
+ status: "notFound",
972
+ threadId
973
+ };
974
+ }
975
+ throw error;
976
+ }
977
+ });
978
+ if (result.ok !== false && conversation) {
979
+ const { conversationId, sessionKey, conversations } = conversation;
980
+ conversations?.get(conversationId)?.watcher?.failNow(new Error("Temporary AI conversation was closed."));
981
+ conversations?.get(conversationId)?.unsubscribe?.();
982
+ conversations?.delete(conversationId);
983
+ if (conversations?.size === 0) {
984
+ codexAppServerConversations.delete(sessionKey);
985
+ }
986
+ }
987
+ return result;
988
+ }
989
+
990
+ async function stopPersistentCodexConversation({
991
+ conversationId,
992
+ conversationState,
993
+ provider
994
+ } = {}) {
995
+ try {
996
+ await provider.readThreadStatus(conversationId);
997
+ await provider.stopThreadForObservationLoss(conversationId, "");
998
+ } catch (error) {
999
+ if (!codexAppServerThreadIsMissing(error, conversationId) || error.message.toLowerCase().startsWith("thread not loaded:")) throw error;
1000
+ }
1001
+ if (conversationState) {
1002
+ conversationState.status = "interrupted";
1003
+ if (conversationState.goal?.status === "active") conversationState.goal = { ...conversationState.goal, status: "paused" };
1004
+ conversationState.watcher?.failNow(new Error("Temporary AI turn was stopped."));
1005
+ conversationState.watcher = null;
1006
+ }
1007
+ return { ok: true, conversationId, status: "interrupted" };
1008
+ }
1009
+
1010
+ async function waitForPreparedCodexAppServerConversationTurn(sessionId, input, {
1011
+ provider,
1012
+ onEvent = null
1013
+ } = {}) {
1014
+ const conversationId = normalizeCodexRunText(input.conversationId);
1015
+ const runId = normalizeCodexRunText(input.runId);
1016
+ const watcher = createCodexAppServerDetachedTurnWatcher(provider, conversationId, {
1017
+ onEvent,
1018
+ timeoutMs: Number(input.timeoutMs || 0) > 0
1019
+ ? Number(input.timeoutMs)
1020
+ : input.persistent === true ? 0 : CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS
1021
+ });
1022
+ const conversationState = codexAppServerConversation(sessionId, conversationId);
1023
+ if (input.persistent && conversationState) conversationState.watcher = watcher;
1024
+ const waitForResult = watcher.wait();
1025
+ void waitForResult.catch(() => {});
1026
+ watcher.setTurnId(runId);
1027
+ try {
1028
+ const current = await provider.readThread(conversationId);
1029
+ const status = codexAppServerThreadStatus(current);
1030
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
1031
+ watcher.failNow(new Error(
1032
+ codexAppServerThreadError(current) || `Codex app-server turn ${status}.`
1033
+ ));
1034
+ } else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
1035
+ await watcher.completeNow(status);
1036
+ }
1037
+ const result = await waitForResult;
1038
+ return {
1039
+ conversationId,
1040
+ ok: true,
1041
+ runId,
1042
+ status: result.status || status || "completed",
1043
+ rawText: result.text
1044
+ };
1045
+ } catch (error) {
1046
+ watcher.failNow(error);
1047
+ throw error;
1048
+ } finally {
1049
+ if (conversationState?.watcher === watcher) conversationState.watcher = null;
1050
+ }
1051
+ }
1052
+
1053
+ return {
1054
+ codexAppServerConversations,
1055
+ codexAppServerConversationTurnStarts,
1056
+ codexAppServerConversation,
1057
+ hasActiveConversation,
1058
+ withCodexAppServerConversationTurnStart,
1059
+ observeCodexConversation,
1060
+ readPersistentCodexConversation,
1061
+ codexAppServerEphemeralConversationSnapshot,
1062
+ prepareCodexAppServerConversationThread,
1063
+ codexAppServerConversationContext,
1064
+ codexAppServerEphemeralScopeContext,
1065
+ createCodexAppServerConversation,
1066
+ startCodexAppServerConversationTurn,
1067
+ readCodexAppServerConversation,
1068
+ waitForCodexAppServerConversationTurn,
1069
+ stopCodexAppServerConversation,
1070
+ deleteScopedCodexAppServerConversation,
1071
+ closeCodexAppServerConversations,
1072
+ interruptDetachedCodexAppServerConversationTurn,
1073
+ deleteDetachedCodexAppServerConversationThread,
1074
+ steerCodexAppServerConversationTurn,
1075
+ acquireCodexAppServerDetachedThread,
1076
+ codexAppServerDetachedThreadId,
1077
+ dispatchCodexAppServerDetachedTurn,
1078
+ runDetachedCodexAppServerTurn,
1079
+ interruptCodexAppServerConversation,
1080
+ stopPersistentCodexConversation
1081
+ };
1082
+ }