@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,663 @@
1
+ import {
2
+ codexAppServerTurnStatusIsSuccessfulComplete,
3
+ codexAppServerTurnStatusIsProviderFailure,
4
+ normalizeCodexRunText,
5
+ codexAppServerErrorMessage,
6
+ codexAppServerSteerFailure,
7
+ codexAppServerMessageRequiresNewTurn,
8
+ codexAppServerMessageDeferred,
9
+ codexAppServerMessageText,
10
+ codexAppServerMessageDisplayText,
11
+ codexPromptInputFromRequest
12
+ } from "./codexTurnState.js";
13
+ import {
14
+ codexAppServerRequestIsInvalid,
15
+ codexAppServerThreadIsMissing,
16
+ codexLocalImageInput,
17
+ ensureCodexAppServerThread,
18
+ sendCodexAppServerPrompt
19
+ } from "./codexProvider.js";
20
+ import crypto from "node:crypto";
21
+ import { isPlainObject } from "./normalize.js";
22
+
23
+ // Original Main message admission, preparation, dispatch and steering.
24
+ // Prompt delivery and receipt state remain the same run-owner references.
25
+ export function createCodexMessageCommands({
26
+ output,
27
+ journal,
28
+ recovery,
29
+ threadStatus,
30
+ selection,
31
+ observer,
32
+ delivery,
33
+ promptDeliveries: codexAppServerPromptDeliveries,
34
+ createRuntime,
35
+ debugError,
36
+ debugLog,
37
+ errorPrefix,
38
+ messageIdPrefix,
39
+ messageMetadata,
40
+ namespace,
41
+ providerSessions,
42
+ recoverAdmission,
43
+ steerFailedCode,
44
+ turnAlreadyRunningError,
45
+ turnState
46
+ }) {
47
+ const {
48
+ submitAssistantResult: submitCodexAppServerAssistantResult,
49
+ splitReasoningTurn: splitCodexAppServerReasoningTurn
50
+ } = output;
51
+ const {
52
+ recoverAbandonedPromptClaim: recoverAbandonedCodexAppServerPromptClaim,
53
+ claimMessageStart: claimCodexAppServerMessageStart,
54
+ markTurnIdle: markCodexAppServerTurnIdle,
55
+ writeUserMessageOwnership: writeCodexAppServerUserMessageOwnership
56
+ } = journal;
57
+ const {
58
+ stopTurnWithProviderFailure: stopCodexAppServerTurnWithProviderFailure,
59
+ completeTurn: completeCodexAppServerTurn,
60
+ markTurnActive: markCodexAppServerTurnActive
61
+ } = recovery;
62
+ const {
63
+ reconcileThreadStatus: reconcileCodexAppServerThreadStatus
64
+ } = threadStatus;
65
+ const {
66
+ codexAppServerMessageContext,
67
+ codexAppServerSessionObserverOptions,
68
+ rememberCodexAppServerPreparedThread
69
+ } = selection;
70
+ const {
71
+ subscribeEvents: subscribeCodexAppServerEvents
72
+ } = observer;
73
+ const {
74
+ writeDeliveredUserMessage: writeCodexAppServerDeliveredUserMessage,
75
+ pendingUserMessages: codexAppServerPendingUserMessages
76
+ } = delivery;
77
+
78
+ async function selectCodexAppServerMessageTurn(sessionId, { messageId = "", startedAt = Date.now() } = {}, context = {}) {
79
+ if (providerSessions) context = codexAppServerMessageContext(sessionId, context);
80
+ const { runtime, session } = context;
81
+ let currentSession = session;
82
+ let turn = turnState(currentSession);
83
+ if (
84
+ turn.state === "starting" &&
85
+ !turn.turnId &&
86
+ !codexAppServerPromptDeliveries.has(namespace(sessionId))
87
+ ) {
88
+ const abandonedClaim = await (recoverAdmission || recoverAbandonedCodexAppServerPromptClaim)(
89
+ runtime,
90
+ currentSession,
91
+ { promptDeliveryActive: false }
92
+ );
93
+ currentSession = abandonedClaim.session;
94
+ turn = turnState(currentSession);
95
+ }
96
+ if (turn.status === "observation_lost") {
97
+ if (turn.active) {
98
+ throw Object.assign(new Error("Codex observation was lost and a stop is not yet verified. Retry after the service has stopped."), {
99
+ code: `${errorPrefix}codex_observation_lost`
100
+ });
101
+ }
102
+ const recovered = await submitCodexAppServerAssistantResult(sessionId, turn.threadId, turn.turnId, { recoverFromProvider: true });
103
+ if (recovered.reason === "error") throw new Error(recovered.error);
104
+ return { value: codexAppServerMessageRequiresNewTurn({ reason: "observation_lost", threadId: turn.threadId, turnId: turn.turnId }), session: currentSession };
105
+ }
106
+ const threadId = normalizeCodexRunText(turn.threadId) || context.threadId(currentSession);
107
+ if (!threadId) {
108
+ debugLog("appServerMessage.newTurn", {
109
+ messageId,
110
+ reason: "thread_missing",
111
+ sessionId,
112
+ threadId: "",
113
+ turnId: ""
114
+ });
115
+ return { value: codexAppServerMessageRequiresNewTurn({
116
+ reason: "thread_missing"
117
+ }), session: currentSession };
118
+ }
119
+ const acquired = await context.acquireProvider(currentSession);
120
+ const provider = acquired.provider;
121
+ if (acquired.reused) {
122
+ debugLog("appServerMessage.providerReused", {
123
+ messageId,
124
+ sessionId,
125
+ threadId,
126
+ turnId: normalizeCodexRunText(turn.turnId)
127
+ });
128
+ }
129
+ const providerReadyAt = Date.now();
130
+ debugLog("appServerMessage.providerReady", {
131
+ durationMs: providerReadyAt - startedAt,
132
+ messageId,
133
+ sessionId
134
+ });
135
+ try {
136
+ await reconcileCodexAppServerThreadStatus(sessionId, provider, threadId, {
137
+ source: "message_delivery"
138
+ });
139
+ } catch (error) {
140
+ if (turn.active || !codexAppServerThreadIsMissing(error, threadId)) {
141
+ throw error;
142
+ }
143
+ debugLog("appServerMessage.newTurn", {
144
+ error: debugError(error),
145
+ messageId,
146
+ reason: "provider_thread_missing",
147
+ sessionId,
148
+ threadId,
149
+ turnId: normalizeCodexRunText(turn.turnId)
150
+ });
151
+ return { value: codexAppServerMessageRequiresNewTurn({
152
+ reason: "provider_thread_missing",
153
+ threadId,
154
+ turnId: normalizeCodexRunText(turn.turnId)
155
+ }), session: currentSession };
156
+ }
157
+ debugLog("appServerMessage.reconciled", {
158
+ durationMs: Date.now() - providerReadyAt,
159
+ messageId,
160
+ sessionId
161
+ });
162
+ currentSession = await runtime.getSession(sessionId);
163
+ turn = turnState(currentSession);
164
+ const turnId = normalizeCodexRunText(turn.turnId);
165
+ const goalAwaitingTurn = !turn.active && turn.goalStatus === "active" && turn.goalThreadId === threadId;
166
+ if (!turn.active && !goalAwaitingTurn) {
167
+ debugLog("appServerMessage.newTurn", {
168
+ messageId,
169
+ reason: "provider_idle",
170
+ sessionId,
171
+ threadId,
172
+ turnId
173
+ });
174
+ return { value: codexAppServerMessageRequiresNewTurn({
175
+ reason: "provider_idle",
176
+ threadId,
177
+ turnId
178
+ }), session: currentSession };
179
+ }
180
+ if (goalAwaitingTurn || !turnId || turn.state === "finalizing") {
181
+ return { value: {
182
+ code: steerFailedCode,
183
+ delivered: false,
184
+ error: "The active assistant turn is not ready to accept this message yet.",
185
+ ok: false,
186
+ operationOutcome: "active_turn_not_ready",
187
+ refreshRecommended: true,
188
+ retryable: true,
189
+ threadId,
190
+ turnId
191
+ }, session: currentSession };
192
+ }
193
+ return { session: currentSession, provider, threadId, turnId };
194
+ }
195
+
196
+ async function prepareCodexAppServerMessageThread(sessionId, input, prepared, preparation) {
197
+ const facility = preparation.threadPreparation(input, prepared);
198
+ const { messageId, startedAt, workdir } = prepared;
199
+ const operation = async currentSession => {
200
+ const source = await facility.provider(currentSession);
201
+ const connection = source.provider ? null : providerSessions.context(
202
+ normalizeCodexRunText(sessionId), source.providerOptions
203
+ );
204
+ const providerAlreadyAvailable = source.provider
205
+ ? source.providerAlreadyAvailable
206
+ : providerSessions.owner.providers.get(connection.providerKey)?.isAvailable?.() === true;
207
+ if (!providerAlreadyAvailable && facility.running) await facility.running();
208
+ let stageStartedAt = Date.now();
209
+ const provider = source.provider || await providerSessions.owner.ensureSession(connection, source.mainThreadId);
210
+ debugLog("appServerPrompt.stage", {
211
+ durationMs: Date.now() - stageStartedAt, messageId, sessionId, stage: "provider"
212
+ });
213
+ stageStartedAt = Date.now();
214
+ const observerOptions = source.observerOptions || codexAppServerSessionObserverOptions(sessionId, source.providerOptions);
215
+ const thread = await ensureCodexAppServerThread({
216
+ ...(source.preparation || facility.preparation(currentSession)),
217
+ observeThread: threadId => subscribeCodexAppServerEvents(sessionId, provider, threadId, observerOptions),
218
+ provider,
219
+ workdir: source.workdir ?? workdir
220
+ });
221
+ if (source.requirePaginatedHistory && thread.thread.raw?.historyMode !== "paginated") {
222
+ throw new Error("Codex did not create the required paginated history. Update Codex before starting this conversation.");
223
+ }
224
+ debugLog("appServerPrompt.stage", {
225
+ durationMs: Date.now() - stageStartedAt, messageId, sessionId, stage: "thread"
226
+ });
227
+ return { currentSession, provider, providerAlreadyAvailable, providerOptions: source.providerOptions,
228
+ thread, observerOptions, connection, ...(source.settings ? { settings: source.settings } : {}) };
229
+ };
230
+ try {
231
+ // Send's provider acquisition stays inside the application startup gate.
232
+ const threadPreparation = facility.gate
233
+ ? await facility.gate(operation)
234
+ : await operation(prepared.session);
235
+ const { currentSession: preparedSession, provider, providerAlreadyAvailable, providerOptions,
236
+ thread, observerOptions, connection, ...settings } = threadPreparation;
237
+ debugLog("appServerPrompt.stage", {
238
+ durationMs: Date.now() - startedAt, messageId, sessionId, stage: "startup-ready"
239
+ });
240
+ if (!Object.hasOwn(facility, "failureThreadId")) prepared.activeThreadId = thread.threadId;
241
+ subscribeCodexAppServerEvents(sessionId, provider, thread.threadId, observerOptions);
242
+ if (connection) rememberCodexAppServerPreparedThread(sessionId, providerOptions, prepared, thread);
243
+ if (facility.ready) await facility.ready();
244
+ return { preparedSession, provider, providerAlreadyAvailable, providerOptions, thread, ...settings };
245
+ } finally {
246
+ // Standalone preparation historically retained the exact binding on
247
+ // failure as well as success; Main records it after the startup gate.
248
+ if (Object.hasOwn(facility, "failureThreadId")) prepared.activeThreadId = facility.failureThreadId;
249
+ }
250
+ }
251
+
252
+ async function startCodexAppServerMessage(sessionId, input = {}, options = {}, prepare) {
253
+ const startedAt = Date.now();
254
+ const agentSettings = isPlainObject(options.agentSettings)
255
+ ? options.agentSettings
256
+ : isPlainObject(input.agentSettings) ? input.agentSettings : {};
257
+ const actorContext = options.actorContext || null;
258
+ const messageId = normalizeCodexRunText(input.messageId) ||
259
+ `${messageIdPrefix}${crypto.randomUUID()}`;
260
+ const userRequest = codexPromptInputFromRequest(input);
261
+ if (!userRequest && options.preparedInput === undefined) {
262
+ return { value: {
263
+ ok: false,
264
+ error: "Codex message is empty."
265
+ } };
266
+ }
267
+
268
+ const context = await prepare.readContext(options);
269
+ if (context.ok === false) {
270
+ return { value: context };
271
+ }
272
+ const { runtime } = context;
273
+ const turnMetadata = { ...input.turnMetadata, ...await messageMetadata.actor?.(actorContext) };
274
+ debugLog("appServerPrompt.start", {
275
+ durationMs: Date.now() - startedAt,
276
+ messageId,
277
+ sessionId
278
+ });
279
+
280
+ const claim = await claimCodexAppServerMessageStart(runtime, sessionId, messageId);
281
+ if (!claim?.claimed) {
282
+ debugLog("appServerPrompt.claimObserved", {
283
+ code: String(claim?.response?.value?.code || ""),
284
+ messageId,
285
+ operationOutcome: String(claim?.response?.value?.operationOutcome || ""),
286
+ sessionId
287
+ });
288
+ return claim?.response || { value: {
289
+ ok: false,
290
+ error: turnAlreadyRunningError
291
+ } };
292
+ }
293
+
294
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
295
+ const promptDeliveryKey = namespace(normalizedSessionId);
296
+ codexAppServerPromptDeliveries.add(promptDeliveryKey);
297
+ const prepared = {
298
+ ...context, agentSettings, actorContext, messageId, userRequest, startedAt,
299
+ activeThreadId: ""
300
+ };
301
+ let providerFailure = "";
302
+ let turnFailureHandled = false;
303
+ try {
304
+ Object.assign(prepared, await prepareCodexAppServerMessageThread(sessionId, input, prepared, prepare));
305
+ const { provider, providerAlreadyAvailable, thread } = prepared;
306
+ await markCodexAppServerTurnActive(sessionId, {
307
+ status: "starting",
308
+ threadId: thread.threadId
309
+ });
310
+ const prompt = await prepare.prepareMessage(input, prepared, { starting: true });
311
+ const started = await startCodexAppServerTurn(sessionId, input, {
312
+ runtime, provider, threadId: thread.threadId, promptDeliveryKey, messageId, renderedPrompt: prompt.renderedPrompt, turnMetadata,
313
+ preparedInput: options.preparedInput,
314
+ get turnSettings() { return prompt.turnSettings; }
315
+ });
316
+ if (started.turnFailureHandled) {
317
+ turnFailureHandled = true;
318
+ throw started.error;
319
+ }
320
+ const { delivery, deliveredTurnId } = started;
321
+ providerFailure = started.providerFailure;
322
+ await prepare.finishMessage(input, prepared, { delivery });
323
+ const currentSession = await runtime.getSession(sessionId);
324
+ debugLog("appServerPrompt.delivered", {
325
+ messageId,
326
+ sessionId,
327
+ threadId: thread.threadId,
328
+ turnId: deliveredTurnId
329
+ });
330
+ return { value: {
331
+ ...(providerFailure ? { error: providerFailure } : {}),
332
+ ok: !providerFailure,
333
+ connectionReused: providerAlreadyAvailable,
334
+ turnMetadata,
335
+ turnId: delivery.turn?.id || ""
336
+ }, session: currentSession, nativeIdentity: { threadId: thread.threadId, turnId: deliveredTurnId } };
337
+ } catch (error) {
338
+ if (!turnFailureHandled) {
339
+ await markCodexAppServerTurnIdle(sessionId, {
340
+ error: codexAppServerErrorMessage(error, "Codex app-server prompt delivery failed."),
341
+ status: "failed",
342
+ threadId: prepared.activeThreadId
343
+ }).catch(() => null);
344
+ }
345
+ return await prepare.finishMessage(input, prepared, { error });
346
+ } finally {
347
+ codexAppServerPromptDeliveries.delete(promptDeliveryKey);
348
+ }
349
+ }
350
+
351
+ async function sendCodexAppServerMessage(sessionId, input = {}, options = {}, prepare) {
352
+ const startedAt = Date.now();
353
+ const turnOwnership = options.turnOwnership || null;
354
+ const message = codexAppServerMessageText(input);
355
+ const displayMessage = codexAppServerMessageDisplayText(input, message);
356
+ const messageId = normalizeCodexRunText(input?.messageId);
357
+ if (!message && options.preparedInput === undefined) {
358
+ return { value: {
359
+ code: steerFailedCode,
360
+ error: "Codex message input is empty.",
361
+ ok: false,
362
+ operationOutcome: "message_empty",
363
+ refreshRecommended: false
364
+ } };
365
+ }
366
+ const context = await prepare.readContext(options);
367
+ if (context.ok === false) {
368
+ return { value: context };
369
+ }
370
+ const { runtime, session } = context;
371
+ if (
372
+ messageId &&
373
+ typeof runtime.store.conversationMessageIdExists === "function" &&
374
+ await runtime.store.conversationMessageIdExists(sessionId, messageId)
375
+ ) {
376
+ return { value: {
377
+ delivered: true,
378
+ duplicate: true,
379
+ ok: true,
380
+ operationOutcome: "message_already_delivered"
381
+ }, session };
382
+ }
383
+ const actorContext = options.actorContext || null;
384
+ const turnMetadata = { ...input.turnMetadata, ...await messageMetadata.actor?.(actorContext) };
385
+ debugLog("appServerMessage.contextReady", {
386
+ durationMs: Date.now() - startedAt,
387
+ messageId,
388
+ sessionId
389
+ });
390
+ const selected = await selectCodexAppServerMessageTurn(sessionId, { messageId, startedAt }, context.selection);
391
+ if (Object.hasOwn(selected, "value")) return selected;
392
+ const { provider, threadId, turnId } = selected;
393
+ const prepared = { ...context, turnOwnership, messageId, actorContext };
394
+ const policy = await prepare.prepareMessage(input, prepared, { starting: false, selected });
395
+ if (policy) return policy;
396
+ const clientUserMessageId = messageId || `${messageIdPrefix}${crypto.randomUUID()}`;
397
+ return steerCodexAppServerTurn(sessionId, input, {
398
+ runtime, provider, threadId, turnId, message, displayMessage, messageId, clientUserMessageId, turnMetadata,
399
+ preparedInput: options.preparedInput
400
+ });
401
+ }
402
+
403
+ async function dispatchCodexAppServerMessage(sessionId, input = {}, options = {}, prepare) {
404
+ const result = await sendCodexAppServerMessage(sessionId, input, options, prepare);
405
+ if (result.value?.newTurnRequired !== true) {
406
+ return result;
407
+ }
408
+ const message = codexAppServerMessageText(input);
409
+ const messageId = normalizeCodexRunText(input?.messageId) ||
410
+ `${messageIdPrefix}${crypto.randomUUID()}`;
411
+ const started = await startCodexAppServerMessage(sessionId, isPlainObject(input) ? {
412
+ ...input,
413
+ message,
414
+ messageId
415
+ } : {
416
+ message,
417
+ messageId
418
+ }, {
419
+ agentSettings: input?.agentSettings || {},
420
+ runtime: options.runtime || null,
421
+ session: options.session || null,
422
+ actorContext: options.actorContext || null,
423
+ preparedInput: options.preparedInput
424
+ }, prepare);
425
+ // The original unavailable-worktree return starts its policy operation,
426
+ // releases the prompt marker in finally, then awaits the returned value.
427
+ started.value = await started.value;
428
+ if (started.value?.ok === false || started.value?.delivered === false) {
429
+ return started;
430
+ }
431
+ const runtime = await createRuntime(sessionId);
432
+ const displayMessage = codexAppServerMessageDisplayText(input, message);
433
+ const conversationTurn = await writeCodexAppServerDeliveredUserMessage(
434
+ runtime,
435
+ sessionId,
436
+ displayMessage || message,
437
+ messageId,
438
+ started.value.turnMetadata || null,
439
+ input?.displayAttachments,
440
+ started.nativeIdentity
441
+ );
442
+ return {
443
+ ...started,
444
+ value: {
445
+ ...started.value,
446
+ conversationTurn,
447
+ conversationTurns: [conversationTurn],
448
+ delivered: true,
449
+ deliveryMode: "new_turn",
450
+ newTurnRequired: false
451
+ }
452
+ };
453
+ }
454
+
455
+ async function startCodexAppServerTurn(sessionId, input = {}, context = {}) {
456
+ const { runtime, provider, threadId, promptDeliveryKey, messageId, renderedPrompt, turnMetadata } = context;
457
+ let providerFailure = "";
458
+ let delivery = null;
459
+ const clientUserMessageId = messageId;
460
+ await writeCodexAppServerUserMessageOwnership(runtime.store, sessionId, clientUserMessageId, {
461
+ eventKind: "codex-app-server-user-message-owned",
462
+ owned: true
463
+ });
464
+ try {
465
+ await input.onPromptSending?.({ threadId: threadId, displayAttachments: input.displayAttachments, turnMetadata });
466
+ const response = sendCodexAppServerPrompt({
467
+ clientUserMessageId,
468
+ prompt: renderedPrompt,
469
+ attachments: input.attachments,
470
+ provider,
471
+ threadId
472
+ }, context);
473
+ const receipt = codexAppServerPendingUserMessages.get(
474
+ `${promptDeliveryKey}\0${clientUserMessageId}`
475
+ )?.receipt;
476
+ // An exact, persisted user-message receipt confirms delivery too.
477
+ // Finish bookkeeping once; a late RPC reply must not hold admission
478
+ // or change a turn that has already continued or completed.
479
+ delivery = receipt
480
+ ? await Promise.race([response, receipt.promise.then((turn) => ({ turn }))])
481
+ : await response;
482
+ } catch (error) {
483
+ if (Number.isInteger(error?.code) && error.code < 0) await input.onPromptRejected?.();
484
+ await writeCodexAppServerUserMessageOwnership(runtime.store, sessionId, clientUserMessageId, {
485
+ eventKind: "codex-app-server-user-message-released",
486
+ owned: false
487
+ });
488
+ await markCodexAppServerTurnIdle(sessionId, {
489
+ error: codexAppServerErrorMessage(error, "Codex app-server prompt delivery failed."),
490
+ status: "failed",
491
+ threadId: threadId
492
+ });
493
+ return { error, turnFailureHandled: true };
494
+ }
495
+ const deliveredTurnId = normalizeCodexRunText(delivery.turn?.id);
496
+ const deliveredTurnStatus = normalizeCodexRunText(delivery.turn?.status || delivery.turn?.raw?.status);
497
+ if (!deliveredTurnId) {
498
+ throw new Error("Codex app-server accepted the prompt without returning a turn id.");
499
+ }
500
+ await markCodexAppServerTurnActive(sessionId, {
501
+ requireTrackedTurn: true,
502
+ status: "inProgress",
503
+ threadId: threadId,
504
+ turnId: deliveredTurnId
505
+ });
506
+ if (codexAppServerTurnStatusIsProviderFailure(deliveredTurnStatus)) {
507
+ providerFailure = `Codex turn ${deliveredTurnStatus}.`;
508
+ await stopCodexAppServerTurnWithProviderFailure(sessionId, threadId, deliveredTurnId, {
509
+ provider,
510
+ status: deliveredTurnStatus
511
+ });
512
+ } else if (codexAppServerTurnStatusIsSuccessfulComplete(deliveredTurnStatus)) {
513
+ const completion = await completeCodexAppServerTurn(sessionId, threadId, deliveredTurnId, {
514
+ provider,
515
+ status: deliveredTurnStatus
516
+ });
517
+ if (completion?.ok === false) {
518
+ providerFailure = normalizeCodexRunText(completion.error) || "Codex completed, but its response could not be processed.";
519
+ }
520
+ }
521
+ return { delivery, deliveredTurnId, providerFailure };
522
+ }
523
+
524
+ async function steerCodexAppServerTurn(sessionId, input = {}, context = {}) {
525
+ const { runtime, provider, threadId, turnId, message, displayMessage, messageId, clientUserMessageId, turnMetadata } = context;
526
+ let currentSession;
527
+ await writeCodexAppServerUserMessageOwnership(runtime.store, sessionId, clientUserMessageId, {
528
+ eventKind: "codex-app-server-user-message-owned",
529
+ owned: true
530
+ });
531
+ debugLog("appServerMessage.activeTurn.start", {
532
+ messageId,
533
+ sessionId,
534
+ threadId,
535
+ turnId
536
+ });
537
+ async function recoverAfterSteerFailure(error = null) {
538
+ await writeCodexAppServerUserMessageOwnership(runtime.store, sessionId, clientUserMessageId, {
539
+ eventKind: "codex-app-server-user-message-released",
540
+ owned: false
541
+ });
542
+ await reconcileCodexAppServerThreadStatus(sessionId, provider, threadId, {
543
+ source: "message_delivery_steer_race"
544
+ }).catch(() => null);
545
+ currentSession = await runtime.getSession(sessionId);
546
+ const currentTurn = turnState(currentSession);
547
+ const sameTurnIsActive = currentTurn.active === true &&
548
+ normalizeCodexRunText(currentTurn.threadId) === threadId &&
549
+ normalizeCodexRunText(currentTurn.turnId) === turnId &&
550
+ currentTurn.state !== "finalizing";
551
+ if (sameTurnIsActive) {
552
+ return codexAppServerRequestIsInvalid(error, "turn/steer")
553
+ ? { value: codexAppServerMessageDeferred({ threadId, turnId }, steerFailedCode), session: currentSession }
554
+ : null;
555
+ }
556
+ debugLog("appServerMessage.newTurn", {
557
+ error: debugError(error),
558
+ messageId,
559
+ reason: "active_turn_completed_before_delivery",
560
+ sessionId,
561
+ threadId,
562
+ turnId
563
+ });
564
+ return { value: codexAppServerMessageRequiresNewTurn({
565
+ reason: "active_turn_completed_before_delivery",
566
+ threadId,
567
+ turnId
568
+ }), session: currentSession };
569
+ }
570
+ let result;
571
+ try {
572
+ await input.onPromptSending?.({ threadId, displayAttachments: input.displayAttachments, turnMetadata });
573
+ const response = provider.steerTurn(
574
+ threadId,
575
+ turnId,
576
+ context.preparedInput === undefined
577
+ ? input.attachments?.some((attachment) => attachment.contentType?.startsWith("image/"))
578
+ ? [message, ...codexLocalImageInput(input.attachments)]
579
+ : message
580
+ : context.preparedInput,
581
+ {
582
+ clientUserMessageId
583
+ }
584
+ );
585
+ const receipt = codexAppServerPendingUserMessages.get(
586
+ `${namespace(sessionId)}\0${clientUserMessageId}`
587
+ )?.receipt;
588
+ result = receipt ? await Promise.race([response, receipt.promise]) : await response;
589
+ } catch (error) {
590
+ if (Number.isInteger(error?.code) && error.code < 0) await input.onPromptRejected?.();
591
+ const recovered = await recoverAfterSteerFailure(error);
592
+ if (recovered) {
593
+ return recovered;
594
+ }
595
+ debugLog("appServerMessage.activeTurn.error", {
596
+ error: debugError(error),
597
+ messageId,
598
+ sessionId,
599
+ threadId,
600
+ turnId
601
+ });
602
+ throw error;
603
+ }
604
+ const steerFailure = codexAppServerSteerFailure(result, steerFailedCode);
605
+ if (steerFailure) {
606
+ const recovered = await recoverAfterSteerFailure(result);
607
+ if (recovered) {
608
+ return recovered;
609
+ }
610
+ debugLog("appServerMessage.activeTurn.failed", {
611
+ error: steerFailure.error,
612
+ messageId,
613
+ operationOutcome: steerFailure.operationOutcome,
614
+ sessionId,
615
+ threadId,
616
+ turnId
617
+ });
618
+ return { value: {
619
+ ...steerFailure,
620
+ result,
621
+ threadId,
622
+ turnId
623
+ } };
624
+ }
625
+ const conversationTurn = await writeCodexAppServerDeliveredUserMessage(
626
+ runtime,
627
+ sessionId,
628
+ displayMessage || message,
629
+ messageId,
630
+ turnMetadata,
631
+ input?.displayAttachments,
632
+ { threadId, turnId }
633
+ );
634
+ splitCodexAppServerReasoningTurn(threadId, turnId);
635
+ currentSession = await runtime.getSession(sessionId);
636
+ debugLog("appServerMessage.activeTurn.done", {
637
+ conversationTurnId: normalizeCodexRunText(conversationTurn?.turnId || conversationTurn?.id),
638
+ messageId,
639
+ sessionId,
640
+ threadId,
641
+ turnId
642
+ });
643
+ return { value: {
644
+ conversationTurn,
645
+ conversationTurns: [conversationTurn],
646
+ delivered: true,
647
+ deliveryMode: "active_turn",
648
+ newTurnRequired: false,
649
+ ok: true,
650
+ operationOutcome: "delivered_to_active_turn",
651
+ result,
652
+ threadId,
653
+ turnId
654
+ }, session: currentSession };
655
+ }
656
+
657
+ return {
658
+ selectMessageTurn: selectCodexAppServerMessageTurn,
659
+ dispatchMessage: dispatchCodexAppServerMessage,
660
+ startTurn: startCodexAppServerTurn,
661
+ steerTurn: steerCodexAppServerTurn
662
+ };
663
+ }