@jskit-ai/assistant-core 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +18 -12
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -1,12 +1,63 @@
1
- import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, normalizeCodexRunText, codexAppServerErrorMessage, codexAppServerSteerFailure, codexAppServerInterruptFailure, codexAppServerConversationTurnIsActive, codexAppServerNativeOuterTurnId } 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 { codexAppServerDeliveredTurn, codexAppServerRequestIsInvalid, codexAppServerThreadHasReadableHistory, codexAppServerThreadIsMissing, codexLocalImageInput, sendCodexAppServerPrompt } from "./codexProvider.js";
5
- import { codexAppServerProjectHookTrustConfig, readCodexToolFreeConfiguration } from "./codexConfiguration.js";
6
- import { codexAppServerAssistantItemText, codexAppServerContentText, classifyCodexAppServerEvent, codexAppServerNotificationThreadId, codexAppServerNotificationTurnId, codexAppServerNotificationTurnStatus, 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.
1
+ import { codexAppServerThreadStatus, codexAppServerConversationTurnIsActive, codexAppServerNativeOuterTurnId } from "./codexTurnState.js";
2
+ import { ignoreCodexDetachedTurnEvent } from "./codexDetachedTurn.js";
3
+ import { readCodexToolFreeConfiguration } from "./codexIsolation.js";
4
+ import { codexAppServerAssistantItemText, codexAppServerContentText, classifyCodexAppServerEvent, codexAppServerNotificationThreadId, codexAppServerNotificationTurnId, codexAppServerNotificationTurnStatus } from "./codexEvents.js";
5
+ import {
6
+ codexAppServerProviderThreadAssistantSegments,
7
+ codexAppServerThreadRawValue,
8
+ codexAppServerProviderTurnId,
9
+ codexAppServerProviderTurnStatus,
10
+ codexAppServerProviderTurnItems,
11
+ codexAppServerProviderTurnClientIds,
12
+ codexAppServerThreadTurnId,
13
+ codexAppServerThreadError
14
+ } from "./codexThreadSnapshot.js";
15
+ import {
16
+ codexAppServerEphemeralConversationSnapshot,
17
+ createCodexConversationTurns,
18
+ ignoreCodexConversationEvent,
19
+ prepareCodexAppServerConversationThread
20
+ } from "./codexConversationTurns.js";
21
+ import {
22
+ acquireCodexAppServerDetachedThread,
23
+ codexAppServerDetachedThreadId,
24
+ createCodexDetachedConversation,
25
+ dispatchCodexAppServerDetachedTurn,
26
+ runDetachedCodexAppServerTurn
27
+ } from "./codexDetachedConversation.js";
28
+ import { createCodexConversationStops } from "./codexConversationStops.js";
29
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
30
+
31
+ /** @typedef {Map<string, Map<string, object>>} CodexConversationRegistry Each session's native conversations, by id. */
32
+ /** @typedef {(sessionId?: string, conversationId?: string) => object | null} CodexConversationLookup */
33
+ /** @typedef {(state: object) => void} CodexConversationStateUpdate */
34
+ /** @typedef {(sessionId: string, conversationId: string, provider: import("./codexProvider.js").CodexAppServerAgentProvider) => object} CodexConversationObserver */
35
+ /** @typedef {{ release(): void }} CodexProviderBorrowHandle */
36
+ /** @typedef {(sessionId: string, input: object, options: object, borrow?: CodexProviderBorrowHandle | null) => Promise<any>} CodexConversationContextReader */
37
+ /** @typedef {(operation: () => Promise<any>) => Promise<any>} CodexConversationResult */
38
+ /** @typedef {(borrowing: boolean, operation: (borrow: CodexProviderBorrowHandle | null) => Promise<any>) => Promise<any>} CodexProviderBorrow */
39
+
40
+ /**
41
+ * The scoped and Temporary conversation commands of a run owner: native conversations this server tracks, their
42
+ * observation, and creating, starting, reading, waiting for, stopping and deleting them. The conversation and
43
+ * turn-start collections keep the run owner's lifetime.
44
+ * @param {object} dependencies
45
+ * @param {import("./codexTurn.js").CodexCheckpoint} dependencies.checkpoint
46
+ * @param {typeof import("./codexTurnState.js").codexAppServerConversationMessageId} dependencies.codexAppServerConversationMessageId
47
+ * @param {import("./codexOutput.js").CodexAppServerOutput["streamMessage"]} dependencies.codexAppServerStreamMessage
48
+ * @param {object} dependencies.conversationPreparation The application's conversation preparation.
49
+ * @param {import("./codexProviderSelection.js").CodexProviderSelection["ensureCodexAppServerProviderForActiveTurn"]} dependencies.ensureCodexAppServerProviderForActiveTurn
50
+ * @param {string} dependencies.errorPrefix
51
+ * @param {import("./codexHelperThreads.js").CodexHelperThreadLifecycle} dependencies.helperLifecycle
52
+ * @param {string} dependencies.helperOwnershipError
53
+ * @param {string} dependencies.interruptFailedCode
54
+ * @param {import("./codexTurn.js").CodexSessionNamespace} dependencies.namespace
55
+ * @param {import("./codexTurn.js").CodexProviderSessions} dependencies.providerSessions
56
+ * @param {import("./codexProviderSelection.js").CodexProviderSelection["ensureSessionProvider"]} dependencies.ensureSessionProvider
57
+ * @param {import("./codexProviderSelection.js").CodexProviderSelection["borrowSessionProvider"]} dependencies.borrowSessionProvider
58
+ * @param {string} dependencies.steerFailedCode
59
+ * @param {import("./codexTurn.js").CodexTurnOutcomes} dependencies.turnOutcomes
60
+ */
10
61
  export function createCodexConversationCommands({
11
62
  checkpoint,
12
63
  codexAppServerConversationMessageId,
@@ -26,6 +77,53 @@ export function createCodexConversationCommands({
26
77
  }) {
27
78
  const codexAppServerConversations = new Map();
28
79
  const codexAppServerConversationTurnStarts = new Map();
80
+ // Turns, detached runs and stops live in their own modules. Each receives the
81
+ // registry above and the registry and context functions declared below.
82
+ const {
83
+ createPreparedCodexAppServerConversation,
84
+ startPreparedCodexAppServerConversationTurn,
85
+ waitForPreparedCodexAppServerConversationTurn
86
+ } = createCodexConversationTurns({
87
+ codexAppServerConversations,
88
+ codexAppServerConversation,
89
+ observeCodexConversation,
90
+ holdWhileActive,
91
+ namespace,
92
+ errorPrefix,
93
+ helperOwnershipError,
94
+ steerFailedCode,
95
+ interruptFailedCode
96
+ });
97
+ const { runDetachedCodexAppServerConversation } = createCodexDetachedConversation({
98
+ conversationPreparation,
99
+ codexAppServerConversationResult,
100
+ codexAppServerConversationContext,
101
+ helperLifecycle,
102
+ helperOwnershipError
103
+ });
104
+ const {
105
+ stopCodexAppServerConversation,
106
+ stopPersistentCodexConversation,
107
+ interruptDetachedCodexAppServerConversationTurn,
108
+ deleteScopedCodexAppServerConversation,
109
+ deleteDetachedCodexAppServerConversationThread,
110
+ closeCodexAppServerConversations
111
+ } = createCodexConversationStops({
112
+ codexAppServerConversations,
113
+ codexAppServerConversation,
114
+ codexAppServerConversationContext,
115
+ codexAppServerConversationResult,
116
+ withProviderBorrow,
117
+ forgetObservation,
118
+ holdWhileActive,
119
+ conversationPreparation,
120
+ helperLifecycle,
121
+ providerSessions,
122
+ namespace,
123
+ errorPrefix,
124
+ interruptFailedCode,
125
+ turnOutcomes
126
+ });
29
127
 
30
128
  // Reads and Temporary chat commands borrow their provider instead of claiming
31
129
  // it: the operation returns its borrow when it finishes, and an unused
@@ -54,7 +152,7 @@ export function createCodexConversationCommands({
54
152
  }
55
153
 
56
154
  function forgetObservation(state) {
57
- state.unsubscribe?.();
155
+ if (state.unsubscribe) state.unsubscribe();
58
156
  state.unsubscribe = null;
59
157
  holdWhileActive(state);
60
158
  }
@@ -76,9 +174,9 @@ export function createCodexConversationCommands({
76
174
  }
77
175
 
78
176
  async function withCodexAppServerConversationTurnStart(sessionId, input, operation) {
79
- const messageId = normalizeCodexRunText(input.messageId);
177
+ const messageId = normalizeText(input.messageId);
80
178
  const deliveryKey = messageId
81
- ? `${namespace(sessionId)}\0${normalizeCodexRunText(input.conversationId)}\0${messageId}`
179
+ ? `${namespace(sessionId)}\0${normalizeText(input.conversationId)}\0${messageId}`
82
180
  : "";
83
181
  const existing = deliveryKey ? codexAppServerConversationTurnStarts.get(deliveryKey) : null;
84
182
  if (existing) {
@@ -103,7 +201,8 @@ export function createCodexConversationCommands({
103
201
  let state = conversations.get(conversationId);
104
202
  if (state?.provider === provider && state.unsubscribe) return state;
105
203
  if (state) forgetObservation(state);
106
- state = { conversationId, provider, persistent: true, status: "ready", runId: "", error: "", turnOutcome: "" };
204
+ state = { conversationId, provider, persistent: true, status: "ready", runId: "", error: "", turnOutcome: "",
205
+ onEvent: ignoreCodexConversationEvent };
107
206
  state.unsubscribe = provider.subscribe((notification) => {
108
207
  if (codexAppServerNotificationThreadId(notification) !== conversationId) return;
109
208
  const turnId = codexAppServerNotificationTurnId(notification);
@@ -112,18 +211,18 @@ export function createCodexConversationCommands({
112
211
  if (status) state.status = status;
113
212
  const classification = classifyCodexAppServerEvent(notification);
114
213
  if (notification.method === "item/agentMessage/delta") {
115
- state.onEvent?.({ type: "text", text: notification.params.delta,
214
+ state.onEvent({ type: "text", text: notification.params.delta,
116
215
  messageId: notification.params.itemId, threadId: conversationId, turnId,
117
216
  message: codexAppServerStreamMessage(classification) });
118
217
  } else if (["assistant_started", "live_progress", "final_assistant_result"].includes(classification.kind)) {
119
- state.onEvent?.({ type: "message", threadId: conversationId, turnId,
218
+ state.onEvent({ type: "message", threadId: conversationId, turnId,
120
219
  message: { ...codexAppServerStreamMessage(classification),
121
220
  ...(classification.kind !== "assistant_started" ? { text: classification.text } : {}) } });
122
221
  }
123
222
  if (notification.method === "thread/goal/updated") state.goal = notification.params.goal;
124
223
  if (notification.method === "thread/goal/cleared") state.goal = null;
125
224
  if (notification.method === "turn/completed") {
126
- state.onEvent = null;
225
+ state.onEvent = ignoreCodexConversationEvent;
127
226
  const completedTurnId = state.runId;
128
227
  const completedStatus = state.status;
129
228
  // A turn the person stopped reaches the checkpoint as cancelled, as Main turns do.
@@ -145,7 +244,7 @@ export function createCodexConversationCommands({
145
244
  }
146
245
 
147
246
  async function readPersistentCodexConversation(sessionId, input, context) {
148
- const conversationId = normalizeCodexRunText(input.conversationId);
247
+ const conversationId = normalizeText(input.conversationId);
149
248
  const previous = codexAppServerConversation(sessionId, conversationId);
150
249
  const state = observeCodexConversation(sessionId, conversationId, context.provider);
151
250
  const thread = await context.provider.readThreadStatus(conversationId);
@@ -200,7 +299,7 @@ export function createCodexConversationCommands({
200
299
  }
201
300
  else continue;
202
301
  const text = role === "thinking"
203
- ? normalizeCodexRunText(codexAppServerContentText(item.summary)) || codexAppServerContentText(item.content)
302
+ ? normalizeText(codexAppServerContentText(item.summary)) || codexAppServerContentText(item.content)
204
303
  : codexAppServerAssistantItemText(item);
205
304
  if (text) messages.push({
206
305
  id: codexAppServerConversationMessageId(conversationId, turnId, role, item.id || text),
@@ -226,27 +325,6 @@ export function createCodexConversationCommands({
226
325
  };
227
326
  }
228
327
 
229
- function codexAppServerEphemeralConversationSnapshot(state = {}) {
230
- return {
231
- conversationId: normalizeCodexRunText(state.conversationId),
232
- error: normalizeCodexRunText(state.error),
233
- message: normalizeCodexRunText(state.message),
234
- ok: true,
235
- outcome: state.outcome || null,
236
- progressUpdates: Array.isArray(state.progressUpdates)
237
- ? state.progressUpdates.map((update = {}) => ({
238
- id: normalizeCodexRunText(update.id),
239
- text: normalizeCodexRunText(update.text)
240
- })).filter((update) => update.id && update.text)
241
- : [],
242
- rawText: normalizeCodexRunText(state.rawText),
243
- messageId: normalizeCodexRunText(state.messageId),
244
- runId: normalizeCodexRunText(state.runId),
245
- status: normalizeCodexRunText(state.status) || "ready",
246
- turnMetadata: state.turnMetadata || null
247
- };
248
- }
249
-
250
328
  async function codexAppServerConversationResult(operation) {
251
329
  try {
252
330
  return await operation();
@@ -255,17 +333,6 @@ export function createCodexConversationCommands({
255
333
  }
256
334
  }
257
335
 
258
- async function prepareCodexAppServerConversationThread(provider, preparation, helperIsolation) {
259
- if (preparation.inspection) {
260
- const enforcement = await helperIsolation.inspect(provider, preparation.inspection);
261
- return preparation.prepared(enforcement);
262
- }
263
- const config = preparation.projectHooks
264
- ? await codexAppServerProjectHookTrustConfig(provider, preparation.workdir)
265
- : null;
266
- return preparation.settings(config);
267
- }
268
-
269
336
  async function acquireCodexAppServerConversationContext(sessionId, prepared, input = {}, borrow = null) {
270
337
  if (prepared.result) return prepared.result;
271
338
  const { context } = prepared;
@@ -285,7 +352,7 @@ export function createCodexConversationCommands({
285
352
  await helperLifecycle.restoreThreads(restoration.projectRuntimeRoot, restoration.context)
286
353
  );
287
354
  } else {
288
- const threadId = normalizeCodexRunText(input.threadId || input.codexSessionId || input.conversationId);
355
+ const threadId = normalizeText(input.threadId || input.codexSessionId || input.conversationId);
289
356
  if (threadId) {
290
357
  await helperLifecycle.assertOrdinaryThreadAvailable(prepared.projectRuntimeRoot, threadId);
291
358
  }
@@ -343,99 +410,11 @@ export function createCodexConversationCommands({
343
410
  }));
344
411
  }
345
412
 
346
- async function runDetachedCodexAppServerConversation(sessionId, input = {}, options = {}) {
347
- const preparation = conversationPreparation.detached(sessionId, input);
348
- const { admission } = preparation;
349
- if (admission.ok === false) return admission;
350
- try {
351
- return await codexAppServerConversationResult(async () => {
352
- const unavailable = preparation.result;
353
- if (unavailable) return unavailable;
354
- const prompt = preparation.prompt;
355
- const context = await codexAppServerConversationContext(sessionId, input, options);
356
- if (context.ok === false) return context;
357
- const { provider, runtime, workdir } = context;
358
- const executionProfile = input.executionProfile && typeof input.executionProfile === "object" &&
359
- !Array.isArray(input.executionProfile) ? input.executionProfile : null;
360
- const helperTurn = Boolean(executionProfile);
361
- const emitDetachedEvent = event => {
362
- if (typeof options.onEvent === "function") options.onEvent(event);
363
- };
364
- const onRetired = ({ threadId }) => emitDetachedEvent({ threadId, type: "thread-retired" });
365
- if (helperTurn) {
366
- await assertCodexAppServerHelperAccountIdentity(provider, input.expectedAccountIdentitySignature, helperOwnershipError);
367
- }
368
- const execution = preparation.execution(context);
369
- const threadSettings = helperTurn ? null : await prepareCodexAppServerConversationThread(
370
- provider, execution.threadPreparation, conversationPreparation.isolation
371
- );
372
- const requestedThreadId = normalizeCodexRunText(input.threadId || input.codexSessionId);
373
- let helper = null;
374
- let thread;
375
- if (helperTurn) {
376
- const scope = { executionProfile, onRetired, projectRuntimeRoot: normalizeCodexRunText(runtime?.stateRoot),
377
- projectContextRoot: normalizeCodexRunText(runtime?.projectContextRoot), provider, sessionId, requestedThreadId, workdir,
378
- ...(requestedThreadId ? { record: helperLifecycle.threadForOperation({ executionProfile,
379
- projectRuntimeRoot: normalizeCodexRunText(runtime?.stateRoot), provider, sessionId,
380
- threadId: requestedThreadId, workdir }) } : {}) };
381
- helper = await helperLifecycle.prepareDetached(scope, execution.helperPreparation);
382
- thread = helper.thread;
383
- } else {
384
- thread = await acquireCodexAppServerDetachedThread(provider, requestedThreadId, threadSettings, input);
385
- }
386
- const threadId = codexAppServerDetachedThreadId(thread, requestedThreadId);
387
- emitDetachedEvent({ threadId, type: "thread" });
388
- const requestedTimeoutMs = Number(input.timeoutMs || 0);
389
- const profileTimeoutMs = Number(executionProfile?.limits?.timeoutMs || 0);
390
- let turnId = "";
391
- const result = await runDetachedCodexAppServerTurn({
392
- provider, threadId, onEvent: emitDetachedEvent,
393
- timeoutMs: helperTurn
394
- ? Math.min(requestedTimeoutMs > 0 ? requestedTimeoutMs : profileTimeoutMs, profileTimeoutMs)
395
- : requestedTimeoutMs > 0 ? requestedTimeoutMs : CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS,
396
- async onFailure(error, status) {
397
- await helper?.discard();
398
- return execution.failure(error, status);
399
- }
400
- }, async failDispatch => {
401
- let dispatched;
402
- if (helperTurn) {
403
- dispatched = await helper.dispatch(threadId, failDispatch);
404
- } else {
405
- try {
406
- dispatched = await dispatchCodexAppServerDetachedTurn({ provider, prompt, threadId }, execution.authorized);
407
- } catch (error) {
408
- await failDispatch(error);
409
- }
410
- }
411
- turnId = dispatched.turnId;
412
- return dispatched;
413
- });
414
- if (helperTurn) {
415
- try {
416
- await assertCodexAppServerHelperAccountIdentity(provider, input.expectedAccountIdentitySignature, helperOwnershipError);
417
- execution.validateOutput(result.text);
418
- } catch (error) {
419
- await helper.discard();
420
- throw error;
421
- }
422
- await helper.complete();
423
- }
424
- emitDetachedEvent({ status: result.status || "completed", text: result.text,
425
- threadId, turnId: result.turnId || turnId, type: "completed" });
426
- return { ok: true, text: result.text, threadId, turnId: result.turnId || turnId,
427
- ...(helperTurn ? { inputCharacters: prompt.length, outputCharacters: result.text.length, usage: result.usage || null } : {}) };
428
- });
429
- } finally {
430
- admission.release();
431
- }
432
- }
433
-
434
413
  async function startCodexAppServerConversationTurn(sessionId, input = {}, options = {}) {
435
- const messageId = normalizeCodexRunText(input.messageId);
414
+ const messageId = normalizeText(input.messageId);
436
415
  return withCodexAppServerConversationTurnStart(sessionId, input, () => codexAppServerConversationResult(async () => {
437
- const conversationId = normalizeCodexRunText(input.conversationId);
438
- const prompt = normalizeCodexRunText(input.message || input.prompt);
416
+ const conversationId = normalizeText(input.conversationId);
417
+ const prompt = normalizeText(input.message || input.prompt);
439
418
  if (!conversationId || !prompt) {
440
419
  return {
441
420
  code: `${errorPrefix}agent_conversation_turn_input_required`,
@@ -459,8 +438,8 @@ export function createCodexConversationCommands({
459
438
  ok: false,
460
439
  };
461
440
  }
462
- // Do not evaluate native preparation until the original duplicate/active
463
- // checks and Helper isolation capability checks have run.
441
+ // Native preparation runs only after the duplicate and active-turn
442
+ // checks and the Helper isolation capability checks.
464
443
  return startPreparedCodexAppServerConversationTurn(sessionId, input, {
465
444
  executionProfile: execution.executionProfile,
466
445
  helperIsolation: conversationPreparation.isolation,
@@ -468,7 +447,7 @@ export function createCodexConversationCommands({
468
447
  authorized: execution.authorized,
469
448
  readOnly: execution.readOnly,
470
449
  actorMetadata: execution.actorMetadata,
471
- onEvent: execution.onEvent,
450
+ onEvent: execution.onEvent || ignoreCodexConversationEvent,
472
451
  projectResult: execution.projectResult,
473
452
  conversationId,
474
453
  messageId,
@@ -483,7 +462,7 @@ export function createCodexConversationCommands({
483
462
 
484
463
  async function readCodexAppServerConversation(sessionId, input = {}, options = {}) {
485
464
  return codexAppServerConversationResult(async () => {
486
- const conversationId = normalizeCodexRunText(input.conversationId);
465
+ const conversationId = normalizeText(input.conversationId);
487
466
  if (!conversationId) {
488
467
  return {
489
468
  code: `${errorPrefix}agent_conversation_id_required`,
@@ -498,7 +477,7 @@ export function createCodexConversationCommands({
498
477
  if (input.ephemeral === true) {
499
478
  return conversationPreparation.expired(conversationId, input);
500
479
  }
501
- if (input.persistent && conversationState?.persistent && conversationState.provider.isAvailable?.() &&
480
+ if (input.persistent && conversationState?.persistent && conversationState.provider.isAvailable() &&
502
481
  !conversationState.provider.observationFailure && !conversationPreparation.admissionError(sessionId)) {
503
482
  // Poll the already observed native thread. Execution environment setup
504
483
  // belongs to Send and reconnection, not every history read. The read
@@ -518,7 +497,7 @@ export function createCodexConversationCommands({
518
497
  }
519
498
  if (input.persistent) return readPreparedPersistentCodexConversation(sessionId, input, context);
520
499
  const thread = await context.provider.readThread(conversationId);
521
- const runId = normalizeCodexRunText(input.runId) || codexAppServerThreadTurnId(thread);
500
+ const runId = normalizeText(input.runId) || codexAppServerThreadTurnId(thread);
522
501
  const status = codexAppServerThreadStatus(thread);
523
502
  const text = runId
524
503
  ? codexAppServerProviderThreadAssistantSegments(thread, runId)
@@ -539,11 +518,11 @@ export function createCodexConversationCommands({
539
518
 
540
519
  async function waitForCodexAppServerConversationTurn(sessionId, input = {}, {
541
520
  assistantScope = null,
542
- onEvent = null
521
+ onEvent = ignoreCodexDetachedTurnEvent
543
522
  } = {}) {
544
523
  return codexAppServerConversationResult(async () => {
545
- const conversationId = normalizeCodexRunText(input.conversationId);
546
- const runId = normalizeCodexRunText(input.runId);
524
+ const conversationId = normalizeText(input.conversationId);
525
+ const runId = normalizeText(input.runId);
547
526
  if (!conversationId || !runId) {
548
527
  return {
549
528
  code: `${errorPrefix}agent_conversation_run_required`,
@@ -573,724 +552,13 @@ export function createCodexConversationCommands({
573
552
  });
574
553
  }
575
554
 
576
- // A conversation whose provider lost its observation stops through that failure.
577
- async function stopThroughObservationFailure(state) {
578
- await state.provider.failObservation(state.provider.observationFailure);
579
- if (codexAppServerConversationTurnIsActive(state.status)) {
580
- throw new Error("Codex's stop is not yet confirmed.");
581
- }
582
- }
583
-
584
- async function stopCodexAppServerConversation(sessionId, input = {}, options = {}, {
585
- turnOutcome = turnOutcomes.USER_CANCELLED
586
- } = {}) {
587
- const conversationId = normalizeCodexRunText(input.conversationId);
588
- const conversationState = codexAppServerConversation(sessionId, conversationId);
589
- if (conversationState?.provider.observationFailure) {
590
- await stopThroughObservationFailure(conversationState);
591
- return { conversationId, ok: true, runId: conversationState.runId, status: "interrupted" };
592
- }
593
- if (input.ephemeral === true && !conversationState) {
594
- return {
595
- conversationExpired: true,
596
- conversationId,
597
- ok: true,
598
- runId: normalizeCodexRunText(input.runId),
599
- status: "interrupted"
600
- };
601
- }
602
- if (input.persistent) {
603
- return withProviderBorrow(true, async (borrow) => {
604
- const context = await codexAppServerConversationContext(sessionId, input, options, borrow);
605
- if (context.ok === false) return context;
606
- return stopPersistentCodexConversation({
607
- conversationId,
608
- conversationState,
609
- provider: context.provider,
610
- turnOutcome
611
- });
612
- });
613
- }
614
- const result = await interruptDetachedCodexAppServerConversationTurn(sessionId, {
615
- agentSettings: input.agentSettings,
616
- threadId: input.conversationId,
617
- turnId: input.runId
618
- }, options, conversationState);
619
- return {
620
- ...result,
621
- conversationId,
622
- runId: normalizeCodexRunText(input.runId)
623
- };
624
- }
625
-
626
- // A scope's delete stops only what this server runs for it: its live providers,
627
- // and a runtime whose earlier stop could not prove its exit. It never acquires
628
- // a provider, which would start a Codex app-server only to stop it, so it needs
629
- // no account, tool home or workdir. A runtime other providers share stays theirs.
630
- async function stopCodexScopeRuntimes(sessionKey) {
631
- const owner = providerSessions.owner;
632
- const attempted = new Set([...owner.providers]
633
- .filter(([providerKey]) => owner.providerOwners.get(providerKey)?.sessionKey === sessionKey)
634
- .map(([, provider]) => provider));
635
- const stopped = await owner.stopCachedProvidersForSession(sessionKey, { requireStopped: true });
636
- const failed = [...stopped.failed];
637
- const results = [...stopped.results];
638
- for (const record of [...owner.ownedRuntimes.values()]) {
639
- if (record.sessionKey !== sessionKey || attempted.has(record.provider) ||
640
- owner.runtimeIsShared(record.providerKey, record.providerOptions)) continue;
641
- try {
642
- // Its provider is no longer cached, so the runtime's own options name its lock.
643
- results.push(await owner.withLifecycle(() => owner.stopOwnedRuntime(record),
644
- record.providerKey, record.providerOptions));
645
- } catch (error) {
646
- failed.push({ code: normalizeCodexRunText(error?.code), error: codexAppServerErrorMessage(error),
647
- providerKey: record.providerKey, retryable: error?.retryable === true });
648
- }
649
- }
650
- if (failed.length) {
651
- const error = new Error(failed.map((failure) => failure.error).join(" "));
652
- error.code = failed[0].code;
653
- error.retryable = failed.every((failure) => failure.retryable);
654
- error.details = { failed };
655
- throw error;
656
- }
657
- return { ok: true, results, stopped: results.some((result) => result?.stopped === true) };
658
- }
659
-
660
- async function deleteScopedCodexAppServerConversation(sessionId, input = {}, options = {}) {
661
- const conversationId = normalizeCodexRunText(input.conversationId);
662
- const sessionKey = namespace(sessionId);
663
- const conversations = codexAppServerConversations.get(sessionKey);
664
- // An ephemeral native thread lives only in the process that created it, so
665
- // one this server no longer tracks has nothing left to delete.
666
- const conversationExpired = input.ephemeral === true && !conversations?.has(conversationId);
667
- let result;
668
- if (conversationExpired) {
669
- result = {
670
- conversationExpired: true
671
- };
672
- } else {
673
- result = await deleteDetachedCodexAppServerConversationThread(sessionId, {
674
- agentSettings: input.agentSettings,
675
- threadId: input.conversationId
676
- }, options, { conversationId, sessionKey, conversations });
677
- if (result.ok === false) {
678
- return { ...result, conversationId };
679
- }
680
- }
681
- const providerExit = options.assistantScope ? await stopCodexScopeRuntimes(sessionKey) : null;
682
- return {
683
- ...result,
684
- conversationId,
685
- ok: conversationExpired ? providerExit?.ok !== false : result?.ok !== false,
686
- ...(providerExit ? { providerExit } : {})
687
- };
688
- }
689
-
690
- // Close stops each persistent conversation through the provider that observes
691
- // it. A closing session refuses a rebuilt context, and a new connection could
692
- // not see the running turn anyway. One conversation's failure does not skip
693
- // the others; it stays registered so that a retried close stops it.
694
- async function stopObservedCodexConversation(state) {
695
- const { provider } = state;
696
- if (provider.observationFailure) {
697
- await stopThroughObservationFailure(state);
698
- return;
699
- }
700
- // A provider its owner has since closed observes nothing left to stop here:
701
- // the owner stopped or detached its process, and reconnecting it would
702
- // start a process nothing owns.
703
- if (![...providerSessions.owner.providers].some(([, owned]) => owned === provider)) return;
704
- // Closing is not the person stopping the turn; its status alone decides the outcome.
705
- await stopPersistentCodexConversation({
706
- conversationId: state.conversationId, conversationState: state, provider, turnOutcome: ""
707
- });
708
- }
709
-
710
- async function closeCodexAppServerConversations(sessionId) {
711
- const key = namespace(sessionId);
712
- const conversations = codexAppServerConversations.get(key);
713
- const failed = [];
714
- for (const state of [...(conversations?.values() || [])]) {
715
- if (!state.persistent) {
716
- await deleteScopedCodexAppServerConversation(sessionId, { conversationId: state.conversationId }).catch(() => null);
717
- conversations.delete(state.conversationId);
718
- continue;
719
- }
720
- try {
721
- await stopObservedCodexConversation(state);
722
- } catch (error) {
723
- failed.push({ conversationId: state.conversationId, error: codexAppServerErrorMessage(error) });
724
- continue;
725
- }
726
- forgetObservation(state);
727
- conversations.delete(state.conversationId);
728
- }
729
- if (conversations && codexAppServerConversations.get(key) === conversations && !conversations.size) {
730
- codexAppServerConversations.delete(key);
731
- }
732
- if (failed.length) {
733
- const error = new Error(`Codex could not confirm that ${failed.length === 1 ? "a conversation" : `${failed.length} conversations`} stopped: ${
734
- failed.map(({ conversationId, error: reason }) => `${conversationId} (${reason})`).join("; ")}. Retry closing the session.`);
735
- error.code = `${errorPrefix}codex_conversation_close_failed`;
736
- error.retryable = true;
737
- error.details = { failed, sessionId: normalizeCodexRunText(sessionId) };
738
- throw error;
739
- }
740
- }
741
-
742
- async function codexAppServerConversationControl(sessionId, input, {
743
- assistantScope = null,
744
- runtime = null,
745
- session = null
746
- } = {}, operation, dispatch) {
747
- return codexAppServerConversationResult(async () => {
748
- const prepared = conversationPreparation.control(sessionId, input, operation);
749
- if (prepared.result) return prepared.result;
750
- const { admission, threadId, turnId } = prepared;
751
- // Stopping or deleting a thread is cleanup, not a use that keeps Codex running.
752
- const borrow = { release() {} };
753
- try {
754
- let context = null;
755
- try {
756
- context = await codexAppServerConversationContext(sessionId, input, { assistantScope, runtime, session }, borrow);
757
- } catch (error) {
758
- const unavailable = prepared.unavailable();
759
- if (unavailable) return unavailable;
760
- throw error;
761
- }
762
- if (context.ok === false) return context;
763
- const unavailable = prepared.unavailable();
764
- if (unavailable) return unavailable;
765
- const policyResult = helperLifecycle?.inspectControl(operation, input, {
766
- sessionId, threadId, turnId,
767
- get retiredThreadIds() { return context.helperRestore?.retiredThreadIds; },
768
- get projectRuntimeRoot() { return context.runtime?.stateRoot; },
769
- get provider() { return context.provider; },
770
- get workdir() { return context.workdir; }
771
- });
772
- // The original Helper-retirement return releases admission before its
773
- // promise settles; only the native dispatch below is awaited here.
774
- if (policyResult !== undefined) return policyResult;
775
- if (operation === "interrupt") {
776
- const unavailable = prepared.unavailable();
777
- if (unavailable) return unavailable;
778
- } else if (typeof context.provider.deleteThread !== "function") {
779
- return {
780
- code: `${errorPrefix}codex_detached_thread_delete_unavailable`,
781
- error: "Codex app-server thread deletion is not available.",
782
- messageCode: "assistant.codex.threadDeleteUnavailable",
783
- messageParams: {},
784
- ok: false,
785
- statusCode: 409,
786
- threadId
787
- };
788
- }
789
- return await dispatch({ provider: context.provider, threadId, turnId });
790
- } finally {
791
- borrow.release();
792
- admission.release();
793
- }
794
- });
795
- }
796
-
797
- async function interruptDetachedCodexAppServerConversationTurn(sessionId, input = {}, options = {}, conversationState = null) {
798
- return interruptCodexAppServerConversation(conversationState, dispatch =>
799
- codexAppServerConversationControl(sessionId, input, options, "interrupt", dispatch));
800
- }
801
-
802
- async function deleteDetachedCodexAppServerConversationThread(sessionId, input = {}, options = {}, conversation = null) {
803
- return deleteCodexAppServerConversation(conversation, dispatch =>
804
- codexAppServerConversationControl(sessionId, input, options, "delete", dispatch));
805
- }
806
-
807
- async function createPreparedCodexAppServerConversation(sessionId, input, {
808
- provider,
809
- executionProfile,
810
- helperIsolation,
811
- threadPreparation
812
- } = {}) {
813
- const prepareThread = () => prepareCodexAppServerConversationThread(provider, threadPreparation(), helperIsolation);
814
- const thread = executionProfile
815
- ? (await helperIsolation.start(provider, prepareThread)).thread
816
- : await provider.startThread({
817
- ...await prepareThread(),
818
- ...(input.ephemeral === true ? { ephemeral: true } : {})
819
- });
820
- const conversationId = normalizeCodexRunText(thread.id || thread.response?.thread?.id);
821
- if (!conversationId) {
822
- throw new Error("Codex app-server did not return a conversation id.");
823
- }
824
- if (input.persistent === true) observeCodexConversation(sessionId, conversationId, provider);
825
- if (input.ephemeral === true) {
826
- const sessionKey = namespace(sessionId);
827
- const conversations = codexAppServerConversations.get(sessionKey) || new Map();
828
- conversations.set(conversationId, {
829
- conversationId,
830
- executionProfile,
831
- provider,
832
- error: "",
833
- message: "",
834
- messageId: "",
835
- nextProgressSequence: 0,
836
- outcome: null,
837
- progressUpdates: [],
838
- rawText: "",
839
- runId: "",
840
- status: "ready",
841
- turnMetadata: null,
842
- watcher: null
843
- });
844
- codexAppServerConversations.set(sessionKey, conversations);
845
- }
846
- return {
847
- conversationId,
848
- ok: true,
849
- status: "ready"
850
- };
851
- }
852
-
853
- async function steerCodexAppServerConversationTurn(input, {
854
- conversationId,
855
- conversationState,
856
- messageId,
857
- prompt,
858
- provider
859
- } = {}) {
860
- const images = codexLocalImageInput(input.attachments || []);
861
- const result = await provider.steerTurn(
862
- conversationId,
863
- conversationState.runId,
864
- images.length ? [prompt, ...images] : prompt,
865
- { clientUserMessageId: messageId }
866
- );
867
- const failure = codexAppServerSteerFailure(result, steerFailedCode);
868
- if (failure) return { ...failure, ok: false };
869
- conversationState.messageId = messageId;
870
- return {
871
- conversationId,
872
- messageId,
873
- ok: true,
874
- runId: conversationState.runId,
875
- status: "inProgress",
876
- deliveryMode: "steer"
877
- };
878
- }
879
-
880
- async function acquireCodexAppServerDetachedThread(provider, requestedThreadId, threadSettings, input = {}) {
881
- let thread = null;
882
- if (requestedThreadId) {
883
- thread = await provider.resumeThread(requestedThreadId, threadSettings);
884
- }
885
- if (!thread) {
886
- thread = await provider.startThread({
887
- ...threadSettings,
888
- ...(input.ephemeral === true ? { ephemeral: true } : {})
889
- });
890
- }
891
- return thread;
892
- }
893
-
894
- function codexAppServerDetachedThreadId(thread, requestedThreadId) {
895
- const threadId = normalizeCodexRunText(thread.id || thread.response?.thread?.id || requestedThreadId);
896
- if (!threadId) {
897
- throw new Error("Codex app-server did not return a detached chat thread id.");
898
- }
899
- return threadId;
900
- }
901
-
902
- async function dispatchCodexAppServerDetachedTurn({ provider, prompt, threadId }, authorized) {
903
- const delivery = await sendCodexAppServerPrompt({ provider, prompt, threadId }, authorized);
904
- return { delivery, ...codexAppServerDeliveredTurn(delivery) };
905
- }
906
-
907
- async function runDetachedCodexAppServerTurn({
908
- provider,
909
- threadId,
910
- timeoutMs,
911
- onEvent: emitDetachedEvent,
912
- onFailure
913
- } = {}, operation) {
914
- const watcher = createCodexAppServerDetachedTurnWatcher(provider, threadId, {
915
- onEvent: (classification) => {
916
- emitDetachedEvent({
917
- classification,
918
- threadId,
919
- turnId: classification.turnId,
920
- type: "notification"
921
- });
922
- },
923
- timeoutMs
924
- });
925
- const waitForResult = watcher.wait();
926
- void waitForResult.catch(() => null);
927
- const throwWatcherFailure = async (fallbackError, status = "", {
928
- waitForDetail = false
929
- } = {}) => {
930
- if (waitForDetail) {
931
- watcher.failAfterDetailGrace(fallbackError);
932
- } else {
933
- watcher.failNow(fallbackError);
934
- }
935
- const error = await waitForResult.then(
936
- () => fallbackError,
937
- (watcherError) => watcherError
938
- );
939
- throw await onFailure(error, status);
940
- };
941
- const { delivery, status, turnId } = await operation(throwWatcherFailure);
942
- watcher.setTurnId(turnId);
943
- emitDetachedEvent({
944
- status,
945
- threadId,
946
- turnId,
947
- type: "turn"
948
- });
949
- if (codexAppServerTurnStatusIsProviderFailure(status)) {
950
- const error = new Error(codexAppServerDeliveredTurn(delivery).error || `Codex app-server turn ${status}.`);
951
- await throwWatcherFailure(error, status, {
952
- waitForDetail: true
953
- });
954
- }
955
- if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
956
- await watcher.completeNow(status);
957
- }
958
- let result = null;
959
- try {
960
- result = await waitForResult;
961
- } catch (error) {
962
- throw await onFailure(error, status);
963
- }
964
- return result;
965
- }
966
-
967
- async function startPreparedCodexAppServerConversationTurn(sessionId, input, {
968
- conversationId,
969
- messageId,
970
- prompt,
971
- provider,
972
- conversationState,
973
- executionProfile,
974
- actorMetadata,
975
- onEvent,
976
- projectResult,
977
- helperIsolation,
978
- threadPreparation,
979
- prepareTurn,
980
- authorized,
981
- readOnly
982
- } = {}) {
983
- if (conversationState && messageId && conversationState.messageId === messageId) {
984
- return codexAppServerEphemeralConversationSnapshot(conversationState);
985
- }
986
- if (conversationState && codexAppServerConversationTurnIsActive(conversationState.status)) {
987
- if (input.steer === true) {
988
- return steerCodexAppServerConversationTurn(input, {
989
- conversationId,
990
- conversationState,
991
- messageId,
992
- prompt,
993
- provider
994
- });
995
- }
996
- return {
997
- code: `${errorPrefix}temporary_conversation_turn_active`,
998
- error: "Temporary AI is already working on this conversation.",
999
- messageCode: "assistant.codex.temporaryConversationBusy",
1000
- messageParams: {},
1001
- ok: false
1002
- };
1003
- }
1004
- if (!conversationState || input.persistent === true) {
1005
- const prepareThread = () => prepareCodexAppServerConversationThread(provider, threadPreparation(), helperIsolation);
1006
- if (executionProfile) await helperIsolation.resume(provider, conversationId, prepareThread);
1007
- else await provider.resumeThread(conversationId, await prepareThread());
1008
- }
1009
- let admittedAccountIdentitySignature = "";
1010
- if (executionProfile) {
1011
- admittedAccountIdentitySignature = await readCodexAppServerAccountIdentity(provider, helperOwnershipError);
1012
- }
1013
- let watcher = null;
1014
- let waitForResult = null;
1015
- if (conversationState && !input.persistent) {
1016
- Object.assign(conversationState, {
1017
- error: "",
1018
- message: "",
1019
- messageId,
1020
- outcome: null,
1021
- progressUpdates: [],
1022
- rawText: "",
1023
- runId: "",
1024
- status: "starting",
1025
- turnMetadata: actorMetadata
1026
- });
1027
- watcher = createCodexAppServerDetachedTurnWatcher(provider, conversationId, {
1028
- includeThreadHistory: false,
1029
- timeoutMs: executionProfile?.limits.timeoutMs || 0,
1030
- onEvent(classification = {}) {
1031
- const current = codexAppServerConversation(sessionId, conversationId);
1032
- if (!current || (classification.turnId && current.runId && classification.turnId !== current.runId)) {
1033
- return;
1034
- }
1035
- onEvent?.(classification, current);
1036
- }
1037
- });
1038
- waitForResult = watcher.wait();
1039
- void waitForResult.catch(() => null);
1040
- conversationState.watcher = watcher;
1041
- }
1042
- let delivery = null;
1043
- if (input.persistent && conversationState) Object.assign(conversationState, { onEvent, turnOutcome: "" });
1044
- try {
1045
- await input.onPromptSending?.({ threadId: conversationId });
1046
- if (executionProfile) {
1047
- const prepared = prepareTurn();
1048
- delivery = await sendPreparedCodexAppServerHelperTurn(provider, prepared, helperIsolation);
1049
- } else {
1050
- delivery = await sendCodexAppServerPrompt({
1051
- clientUserMessageId: input.messageId,
1052
- outputSchema: input.outputSchema,
1053
- prompt,
1054
- attachments: input.attachments,
1055
- provider,
1056
- readOnly,
1057
- threadId: conversationId
1058
- }, authorized);
1059
- }
1060
- } catch (error) {
1061
- if (input.persistent && conversationState) conversationState.onEvent = null;
1062
- watcher?.failNow(error);
1063
- await waitForResult?.catch(() => null);
1064
- if (conversationState) {
1065
- Object.assign(conversationState, {
1066
- error: codexAppServerErrorMessage(error, "Temporary AI message could not be sent."),
1067
- status: "failed",
1068
- watcher: null
1069
- });
1070
- holdWhileActive(conversationState);
1071
- }
1072
- throw error;
1073
- }
1074
- const { turnId: runId, status } = codexAppServerDeliveredTurn(delivery);
1075
- if (!runId) {
1076
- watcher?.failNow(new Error("Codex app-server accepted a conversation turn without returning its id."));
1077
- await waitForResult?.catch(() => null);
1078
- throw new Error("Codex app-server accepted a conversation turn without returning its id.");
1079
- }
1080
- if (conversationState && !input.persistent) {
1081
- let nativeCompleted = false;
1082
- Object.assign(conversationState, {
1083
- runId,
1084
- status: !executionProfile && codexAppServerTurnStatusIsSuccessfulComplete(status) ? status : "inProgress"
1085
- });
1086
- watcher.setTurnId(runId);
1087
- if (codexAppServerTurnStatusIsProviderFailure(status)) {
1088
- watcher.failNow(new Error(`Codex app-server turn ${status}.`));
1089
- } else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
1090
- await watcher.completeNow(status);
1091
- }
1092
- conversationState.completion = waitForResult.then(async (result = {}) => {
1093
- const current = codexAppServerConversation(sessionId, conversationId);
1094
- if (!current || current.runId !== runId) {
1095
- return;
1096
- }
1097
- nativeCompleted = true;
1098
- if (!executionProfile) current.status = result.status || "completed";
1099
- if (executionProfile) {
1100
- const completingRunId = current.runId;
1101
- await assertCodexAppServerHelperAccountIdentity(provider, admittedAccountIdentitySignature, helperOwnershipError);
1102
- if (codexAppServerConversation(sessionId, conversationId) !== current ||
1103
- current.runId !== completingRunId || !codexAppServerConversationTurnIsActive(current.status)) {
1104
- throw new Error("The scoped helper turn is unavailable.");
1105
- }
1106
- }
1107
- const response = projectResult(result);
1108
- Object.assign(current, {
1109
- error: "",
1110
- ...response,
1111
- status: result.status || "completed",
1112
- watcher: null
1113
- });
1114
- return codexAppServerEphemeralConversationSnapshot(current);
1115
- }).catch(async (error) => {
1116
- const current = codexAppServerConversation(sessionId, conversationId);
1117
- if (!current || current.runId !== runId || current.status === "interrupted") {
1118
- throw error;
1119
- }
1120
- if (executionProfile && !nativeCompleted && codexAppServerConversationTurnIsActive(current.status)) {
1121
- try {
1122
- const interrupted = await provider.interruptTurn(conversationId, runId);
1123
- const failure = codexAppServerInterruptFailure(interrupted, interruptFailedCode);
1124
- if (failure) throw new Error(failure.error);
1125
- } catch {
1126
- current.error = "The helper deadline ended, but Stop could not be confirmed. Retry Stop before reusing this helper.";
1127
- current.watcher = null;
1128
- throw new Error(current.error);
1129
- }
1130
- }
1131
- Object.assign(current, {
1132
- error: codexAppServerErrorMessage(error, "Temporary AI turn failed."),
1133
- status: "failed",
1134
- watcher: null
1135
- });
1136
- throw error;
1137
- });
1138
- void conversationState.completion.catch(() => null);
1139
- }
1140
- if (input.persistent && conversationState) {
1141
- Object.assign(conversationState, { runId, status, messageId });
1142
- holdWhileActive(conversationState);
1143
- }
1144
- return { conversationId, messageId, ok: true, runId, status };
1145
- }
1146
-
1147
- async function interruptCodexAppServerConversation(conversationState, operation) {
1148
- const result = await operation(async ({ provider, threadId, turnId }) => {
1149
- const result = await provider.interruptTurn(threadId, turnId);
1150
- const interruptFailure = codexAppServerInterruptFailure(result, interruptFailedCode);
1151
- if (interruptFailure) {
1152
- return {
1153
- ...interruptFailure,
1154
- result,
1155
- threadId,
1156
- turnId
1157
- };
1158
- }
1159
- return {
1160
- ok: true,
1161
- result,
1162
- status: "interrupted",
1163
- threadId,
1164
- turnId
1165
- };
1166
- });
1167
- if (result.ok !== false && conversationState) {
1168
- conversationState.status = "interrupted";
1169
- conversationState.watcher?.failNow(new Error("Temporary AI turn was stopped."));
1170
- conversationState.watcher = null;
1171
- holdWhileActive(conversationState);
1172
- }
1173
- return result;
1174
- }
1175
-
1176
- async function deleteCodexAppServerConversation(conversation, operation) {
1177
- const result = await operation(async ({ provider, threadId }) => {
1178
- try {
1179
- const result = await provider.deleteThread(threadId);
1180
- return {
1181
- ok: true,
1182
- result,
1183
- status: "deleted",
1184
- threadId
1185
- };
1186
- } catch (error) {
1187
- if (
1188
- codexAppServerRequestIsInvalid(error, "thread/delete") &&
1189
- !await codexAppServerThreadHasReadableHistory(provider, threadId)
1190
- ) {
1191
- return {
1192
- ok: true,
1193
- status: "notFound",
1194
- threadId
1195
- };
1196
- }
1197
- throw error;
1198
- }
1199
- });
1200
- if (result.ok !== false && conversation) {
1201
- const { conversationId, sessionKey, conversations } = conversation;
1202
- const state = conversations?.get(conversationId);
1203
- state?.watcher?.failNow(new Error("Temporary AI conversation was closed."));
1204
- if (state) forgetObservation(state);
1205
- conversations?.delete(conversationId);
1206
- if (conversations?.size === 0) {
1207
- codexAppServerConversations.delete(sessionKey);
1208
- }
1209
- }
1210
- return result;
1211
- }
1212
-
1213
- async function stopPersistentCodexConversation({
1214
- conversationId,
1215
- conversationState,
1216
- provider,
1217
- turnOutcome = ""
1218
- } = {}) {
1219
- // Set before stopping: the provider can report turn/completed while it stops.
1220
- if (conversationState && codexAppServerConversationTurnIsActive(conversationState.status)) {
1221
- conversationState.turnOutcome = turnOutcome;
1222
- }
1223
- try {
1224
- await provider.readThreadStatus(conversationId);
1225
- await provider.stopThreadForObservationLoss(conversationId, "");
1226
- } catch (error) {
1227
- if (!codexAppServerThreadIsMissing(error, conversationId) || error.message.toLowerCase().startsWith("thread not loaded:")) {
1228
- if (conversationState) conversationState.turnOutcome = "";
1229
- throw error;
1230
- }
1231
- }
1232
- if (conversationState) {
1233
- conversationState.status = "interrupted";
1234
- if (conversationState.goal?.status === "active") conversationState.goal = { ...conversationState.goal, status: "paused" };
1235
- conversationState.watcher?.failNow(new Error("Temporary AI turn was stopped."));
1236
- conversationState.watcher = null;
1237
- holdWhileActive(conversationState);
1238
- }
1239
- return { ok: true, conversationId, status: "interrupted" };
1240
- }
1241
-
1242
- async function waitForPreparedCodexAppServerConversationTurn(sessionId, input, {
1243
- provider,
1244
- onEvent = null
1245
- } = {}) {
1246
- const conversationId = normalizeCodexRunText(input.conversationId);
1247
- const runId = normalizeCodexRunText(input.runId);
1248
- const watcher = createCodexAppServerDetachedTurnWatcher(provider, conversationId, {
1249
- onEvent,
1250
- timeoutMs: Number(input.timeoutMs || 0) > 0
1251
- ? Number(input.timeoutMs)
1252
- : input.persistent === true ? 0 : CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS
1253
- });
1254
- const conversationState = codexAppServerConversation(sessionId, conversationId);
1255
- if (input.persistent && conversationState) conversationState.watcher = watcher;
1256
- const waitForResult = watcher.wait();
1257
- void waitForResult.catch(() => {});
1258
- watcher.setTurnId(runId);
1259
- try {
1260
- const current = await provider.readThread(conversationId);
1261
- const status = codexAppServerThreadStatus(current);
1262
- if (codexAppServerTurnStatusIsProviderFailure(status)) {
1263
- watcher.failNow(new Error(
1264
- codexAppServerThreadError(current) || `Codex app-server turn ${status}.`
1265
- ));
1266
- } else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
1267
- await watcher.completeNow(status);
1268
- }
1269
- const result = await waitForResult;
1270
- return {
1271
- conversationId,
1272
- ok: true,
1273
- runId,
1274
- status: result.status || status || "completed",
1275
- rawText: result.text
1276
- };
1277
- } catch (error) {
1278
- watcher.failNow(error);
1279
- throw error;
1280
- } finally {
1281
- if (conversationState?.watcher === watcher) conversationState.watcher = null;
1282
- }
1283
- }
1284
-
1285
- return {
555
+ return Object.freeze({
1286
556
  codexAppServerConversations,
1287
557
  codexAppServerConversationTurnStarts,
1288
558
  codexAppServerConversation,
1289
559
  hasActiveConversation,
1290
- withCodexAppServerConversationTurnStart,
1291
560
  observeCodexConversation,
1292
561
  readPersistentCodexConversation,
1293
- codexAppServerEphemeralConversationSnapshot,
1294
562
  prepareCodexAppServerConversationThread,
1295
563
  codexAppServerConversationContext,
1296
564
  codexAppServerEphemeralScopeContext,
@@ -1304,12 +572,12 @@ export function createCodexConversationCommands({
1304
572
  closeCodexAppServerConversations,
1305
573
  interruptDetachedCodexAppServerConversationTurn,
1306
574
  deleteDetachedCodexAppServerConversationThread,
1307
- steerCodexAppServerConversationTurn,
1308
575
  acquireCodexAppServerDetachedThread,
1309
576
  codexAppServerDetachedThreadId,
1310
577
  dispatchCodexAppServerDetachedTurn,
1311
578
  runDetachedCodexAppServerTurn,
1312
- interruptCodexAppServerConversation,
1313
579
  stopPersistentCodexConversation
1314
- };
580
+ });
1315
581
  }
582
+
583
+ /** @typedef {ReturnType<typeof createCodexConversationCommands>} CodexConversationCommands */