@jskit-ai/assistant-core 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +13 -10
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -0,0 +1,377 @@
1
+ import { codexAppServerErrorMessage, codexAppServerInterruptFailure, codexAppServerConversationTurnIsActive } from "./codexTurnState.js";
2
+ import { codexAppServerRequestIsInvalid, codexAppServerThreadHasReadableHistory, codexAppServerThreadIsMissing } from "./codexProviderThreads.js";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+
5
+ /**
6
+ * Stopping, interrupting, deleting and closing native conversations. The conversation registry, its observation
7
+ * and context acquisition belong to the conversation commands, which pass them in.
8
+ * @param {object} dependencies
9
+ * @param {import("./codexConversations.js").CodexConversationRegistry} dependencies.codexAppServerConversations
10
+ * @param {import("./codexConversations.js").CodexConversationLookup} dependencies.codexAppServerConversation
11
+ * @param {import("./codexConversations.js").CodexConversationContextReader} dependencies.codexAppServerConversationContext
12
+ * @param {import("./codexConversations.js").CodexConversationResult} dependencies.codexAppServerConversationResult
13
+ * @param {import("./codexConversations.js").CodexProviderBorrow} dependencies.withProviderBorrow
14
+ * @param {import("./codexConversations.js").CodexConversationStateUpdate} dependencies.forgetObservation
15
+ * @param {import("./codexConversations.js").CodexConversationStateUpdate} dependencies.holdWhileActive
16
+ * @param {object} dependencies.conversationPreparation The application's conversation preparation.
17
+ * @param {import("./codexHelperThreads.js").CodexHelperThreadLifecycle} dependencies.helperLifecycle
18
+ * @param {import("./codexTurn.js").CodexProviderSessions} dependencies.providerSessions
19
+ * @param {import("./codexTurn.js").CodexSessionNamespace} dependencies.namespace
20
+ * @param {string} dependencies.errorPrefix
21
+ * @param {string} dependencies.interruptFailedCode
22
+ * @param {import("./codexTurn.js").CodexTurnOutcomes} dependencies.turnOutcomes
23
+ */
24
+ export function createCodexConversationStops({
25
+ codexAppServerConversations,
26
+ codexAppServerConversation,
27
+ codexAppServerConversationContext,
28
+ codexAppServerConversationResult,
29
+ withProviderBorrow,
30
+ forgetObservation,
31
+ holdWhileActive,
32
+ conversationPreparation,
33
+ helperLifecycle,
34
+ providerSessions,
35
+ namespace,
36
+ errorPrefix,
37
+ interruptFailedCode,
38
+ turnOutcomes
39
+ }) {
40
+ // A conversation whose provider lost its observation stops through that failure.
41
+ async function stopThroughObservationFailure(state) {
42
+ await state.provider.failObservation(state.provider.observationFailure);
43
+ if (codexAppServerConversationTurnIsActive(state.status)) {
44
+ throw new Error("Codex's stop is not yet confirmed.");
45
+ }
46
+ }
47
+
48
+ async function stopCodexAppServerConversation(sessionId, input = {}, options = {}, {
49
+ turnOutcome = turnOutcomes.USER_CANCELLED
50
+ } = {}) {
51
+ const conversationId = normalizeText(input.conversationId);
52
+ const conversationState = codexAppServerConversation(sessionId, conversationId);
53
+ if (conversationState?.provider.observationFailure) {
54
+ await stopThroughObservationFailure(conversationState);
55
+ return { conversationId, ok: true, runId: conversationState.runId, status: "interrupted" };
56
+ }
57
+ if (input.ephemeral === true && !conversationState) {
58
+ return {
59
+ conversationExpired: true,
60
+ conversationId,
61
+ ok: true,
62
+ runId: normalizeText(input.runId),
63
+ status: "interrupted"
64
+ };
65
+ }
66
+ if (input.persistent) {
67
+ return withProviderBorrow(true, async (borrow) => {
68
+ const context = await codexAppServerConversationContext(sessionId, input, options, borrow);
69
+ if (context.ok === false) return context;
70
+ return stopPersistentCodexConversation({
71
+ conversationId,
72
+ conversationState,
73
+ provider: context.provider,
74
+ turnOutcome
75
+ });
76
+ });
77
+ }
78
+ const result = await interruptDetachedCodexAppServerConversationTurn(sessionId, {
79
+ agentSettings: input.agentSettings,
80
+ threadId: input.conversationId,
81
+ turnId: input.runId
82
+ }, options, conversationState);
83
+ return {
84
+ ...result,
85
+ conversationId,
86
+ runId: normalizeText(input.runId)
87
+ };
88
+ }
89
+
90
+ async function stopPersistentCodexConversation({
91
+ conversationId,
92
+ conversationState,
93
+ provider,
94
+ turnOutcome = ""
95
+ } = {}) {
96
+ // Set before stopping: the provider can report turn/completed while it stops.
97
+ if (conversationState && codexAppServerConversationTurnIsActive(conversationState.status)) {
98
+ conversationState.turnOutcome = turnOutcome;
99
+ }
100
+ try {
101
+ await provider.readThreadStatus(conversationId);
102
+ await provider.stopThreadForObservationLoss(conversationId, "");
103
+ } catch (error) {
104
+ if (!codexAppServerThreadIsMissing(error, conversationId) || error.message.toLowerCase().startsWith("thread not loaded:")) {
105
+ if (conversationState) conversationState.turnOutcome = "";
106
+ throw error;
107
+ }
108
+ }
109
+ if (conversationState) {
110
+ conversationState.status = "interrupted";
111
+ if (conversationState.goal?.status === "active") conversationState.goal = { ...conversationState.goal, status: "paused" };
112
+ conversationState.watcher?.failNow(new Error("Temporary AI turn was stopped."));
113
+ conversationState.watcher = null;
114
+ holdWhileActive(conversationState);
115
+ }
116
+ return { ok: true, conversationId, status: "interrupted" };
117
+ }
118
+
119
+ async function interruptDetachedCodexAppServerConversationTurn(sessionId, input = {}, options = {}, conversationState = null) {
120
+ return interruptCodexAppServerConversation(conversationState, dispatch =>
121
+ codexAppServerConversationControl(sessionId, input, options, "interrupt", dispatch));
122
+ }
123
+
124
+ async function interruptCodexAppServerConversation(conversationState, operation) {
125
+ const result = await operation(async ({ provider, threadId, turnId }) => {
126
+ const result = await provider.interruptTurn(threadId, turnId);
127
+ const interruptFailure = codexAppServerInterruptFailure(result, interruptFailedCode);
128
+ if (interruptFailure) {
129
+ return {
130
+ ...interruptFailure,
131
+ result,
132
+ threadId,
133
+ turnId
134
+ };
135
+ }
136
+ return {
137
+ ok: true,
138
+ result,
139
+ status: "interrupted",
140
+ threadId,
141
+ turnId
142
+ };
143
+ });
144
+ if (result.ok !== false && conversationState) {
145
+ conversationState.status = "interrupted";
146
+ conversationState.watcher?.failNow(new Error("Temporary AI turn was stopped."));
147
+ conversationState.watcher = null;
148
+ holdWhileActive(conversationState);
149
+ }
150
+ return result;
151
+ }
152
+
153
+ async function deleteScopedCodexAppServerConversation(sessionId, input = {}, options = {}) {
154
+ const conversationId = normalizeText(input.conversationId);
155
+ const sessionKey = namespace(sessionId);
156
+ const conversations = codexAppServerConversations.get(sessionKey);
157
+ // An ephemeral native thread lives only in the process that created it, so
158
+ // one this server no longer tracks has nothing left to delete.
159
+ const conversationExpired = input.ephemeral === true && !conversations?.has(conversationId);
160
+ let result;
161
+ if (conversationExpired) {
162
+ result = {
163
+ conversationExpired: true
164
+ };
165
+ } else {
166
+ result = await deleteDetachedCodexAppServerConversationThread(sessionId, {
167
+ agentSettings: input.agentSettings,
168
+ threadId: input.conversationId
169
+ }, options, { conversationId, sessionKey, conversations });
170
+ if (result.ok === false) {
171
+ return { ...result, conversationId };
172
+ }
173
+ }
174
+ const providerExit = options.assistantScope ? await stopCodexScopeRuntimes(sessionKey) : null;
175
+ return {
176
+ ...result,
177
+ conversationId,
178
+ ok: conversationExpired ? providerExit?.ok !== false : result?.ok !== false,
179
+ ...(providerExit ? { providerExit } : {})
180
+ };
181
+ }
182
+
183
+ async function deleteDetachedCodexAppServerConversationThread(sessionId, input = {}, options = {}, conversation = null) {
184
+ return deleteCodexAppServerConversation(conversation, dispatch =>
185
+ codexAppServerConversationControl(sessionId, input, options, "delete", dispatch));
186
+ }
187
+
188
+ async function deleteCodexAppServerConversation(conversation, operation) {
189
+ const result = await operation(async ({ provider, threadId }) => {
190
+ try {
191
+ const result = await provider.deleteThread(threadId);
192
+ return {
193
+ ok: true,
194
+ result,
195
+ status: "deleted",
196
+ threadId
197
+ };
198
+ } catch (error) {
199
+ if (
200
+ codexAppServerRequestIsInvalid(error, "thread/delete") &&
201
+ !await codexAppServerThreadHasReadableHistory(provider, threadId)
202
+ ) {
203
+ return {
204
+ ok: true,
205
+ status: "notFound",
206
+ threadId
207
+ };
208
+ }
209
+ throw error;
210
+ }
211
+ });
212
+ if (result.ok !== false && conversation) {
213
+ const { conversationId, sessionKey, conversations } = conversation;
214
+ const state = conversations?.get(conversationId);
215
+ state?.watcher?.failNow(new Error("Temporary AI conversation was closed."));
216
+ if (state) forgetObservation(state);
217
+ conversations?.delete(conversationId);
218
+ if (conversations?.size === 0) {
219
+ codexAppServerConversations.delete(sessionKey);
220
+ }
221
+ }
222
+ return result;
223
+ }
224
+
225
+ async function codexAppServerConversationControl(sessionId, input, {
226
+ assistantScope = null,
227
+ runtime = null,
228
+ session = null
229
+ } = {}, operation, dispatch) {
230
+ return codexAppServerConversationResult(async () => {
231
+ const prepared = conversationPreparation.control(sessionId, input, operation);
232
+ if (prepared.result) return prepared.result;
233
+ const { admission, threadId, turnId } = prepared;
234
+ // Stopping or deleting a thread is cleanup, not a use that keeps Codex running.
235
+ const borrow = { release() {} };
236
+ try {
237
+ let context = null;
238
+ try {
239
+ context = await codexAppServerConversationContext(sessionId, input, { assistantScope, runtime, session }, borrow);
240
+ } catch (error) {
241
+ const unavailable = prepared.unavailable();
242
+ if (unavailable) return unavailable;
243
+ throw error;
244
+ }
245
+ if (context.ok === false) return context;
246
+ const unavailable = prepared.unavailable();
247
+ if (unavailable) return unavailable;
248
+ const policyResult = helperLifecycle.inspectControl(operation, input, {
249
+ sessionId, threadId, turnId,
250
+ get retiredThreadIds() { return context.helperRestore?.retiredThreadIds; },
251
+ get projectRuntimeRoot() { return context.runtime?.stateRoot; },
252
+ get provider() { return context.provider; },
253
+ get workdir() { return context.workdir; }
254
+ });
255
+ // A Helper-retirement policy result releases admission before its
256
+ // promise settles; only the native dispatch below is awaited here.
257
+ if (policyResult !== undefined) return policyResult;
258
+ if (operation === "interrupt") {
259
+ const unavailable = prepared.unavailable();
260
+ if (unavailable) return unavailable;
261
+ // readable-check: allow method-probe - optional operation: thread deletion is the one control a provider may lack, answered by its catalogued unavailable result
262
+ } else if (typeof context.provider.deleteThread !== "function") {
263
+ return {
264
+ code: `${errorPrefix}codex_detached_thread_delete_unavailable`,
265
+ error: "Codex app-server thread deletion is not available.",
266
+ messageCode: "assistant.codex.threadDeleteUnavailable",
267
+ messageParams: {},
268
+ ok: false,
269
+ statusCode: 409,
270
+ threadId
271
+ };
272
+ }
273
+ return await dispatch({ provider: context.provider, threadId, turnId });
274
+ } finally {
275
+ borrow.release();
276
+ admission.release();
277
+ }
278
+ });
279
+ }
280
+
281
+ // A scope's delete stops only what this server runs for it: its live providers,
282
+ // and a runtime whose earlier stop could not prove its exit. It never acquires
283
+ // a provider, which would start a Codex app-server only to stop it, so it needs
284
+ // no account, tool home or workdir. A runtime other providers share stays theirs.
285
+ async function stopCodexScopeRuntimes(sessionKey) {
286
+ const owner = providerSessions.owner;
287
+ const attempted = new Set([...owner.providers]
288
+ .filter(([providerKey]) => owner.providerOwners.get(providerKey)?.sessionKey === sessionKey)
289
+ .map(([, provider]) => provider));
290
+ const stopped = await owner.stopCachedProvidersForSession(sessionKey, { requireStopped: true });
291
+ const failed = [...stopped.failed];
292
+ const results = [...stopped.results];
293
+ for (const record of [...owner.ownedRuntimes.values()]) {
294
+ if (record.sessionKey !== sessionKey || attempted.has(record.provider) ||
295
+ owner.runtimeIsShared(record.providerKey, record.providerOptions)) continue;
296
+ try {
297
+ // Its provider is no longer cached, so the runtime's own options name its lock.
298
+ results.push(await owner.withLifecycle(() => owner.stopOwnedRuntime(record),
299
+ record.providerKey, record.providerOptions));
300
+ } catch (error) {
301
+ failed.push({ code: normalizeText(error?.code), error: codexAppServerErrorMessage(error),
302
+ providerKey: record.providerKey, retryable: error?.retryable === true });
303
+ }
304
+ }
305
+ if (failed.length) {
306
+ const error = new Error(failed.map((failure) => failure.error).join(" "));
307
+ error.code = failed[0].code;
308
+ error.retryable = failed.every((failure) => failure.retryable);
309
+ error.details = { failed };
310
+ throw error;
311
+ }
312
+ return { ok: true, results, stopped: results.some((result) => result?.stopped === true) };
313
+ }
314
+
315
+ // Close stops each persistent conversation through the provider that observes
316
+ // it. A closing session refuses a rebuilt context, and a new connection could
317
+ // not see the running turn anyway. One conversation's failure does not skip
318
+ // the others; it stays registered so that a retried close stops it.
319
+ async function stopObservedCodexConversation(state) {
320
+ const { provider } = state;
321
+ if (provider.observationFailure) {
322
+ await stopThroughObservationFailure(state);
323
+ return;
324
+ }
325
+ // A provider its owner has since closed observes nothing left to stop here:
326
+ // the owner stopped or detached its process, and reconnecting it would
327
+ // start a process nothing owns.
328
+ if (![...providerSessions.owner.providers].some(([, owned]) => owned === provider)) return;
329
+ // Closing is not the person stopping the turn; its status alone decides the outcome.
330
+ await stopPersistentCodexConversation({
331
+ conversationId: state.conversationId, conversationState: state, provider, turnOutcome: ""
332
+ });
333
+ }
334
+
335
+ async function closeCodexAppServerConversations(sessionId) {
336
+ const key = namespace(sessionId);
337
+ const conversations = codexAppServerConversations.get(key);
338
+ const failed = [];
339
+ for (const state of [...(conversations?.values() || [])]) {
340
+ if (!state.persistent) {
341
+ await deleteScopedCodexAppServerConversation(sessionId, { conversationId: state.conversationId }).catch(() => null);
342
+ conversations.delete(state.conversationId);
343
+ continue;
344
+ }
345
+ try {
346
+ await stopObservedCodexConversation(state);
347
+ } catch (error) {
348
+ failed.push({ conversationId: state.conversationId, error: codexAppServerErrorMessage(error) });
349
+ continue;
350
+ }
351
+ forgetObservation(state);
352
+ conversations.delete(state.conversationId);
353
+ }
354
+ if (conversations && codexAppServerConversations.get(key) === conversations && !conversations.size) {
355
+ codexAppServerConversations.delete(key);
356
+ }
357
+ if (failed.length) {
358
+ const error = new Error(`Codex could not confirm that ${failed.length === 1 ? "a conversation" : `${failed.length} conversations`} stopped: ${
359
+ failed.map(({ conversationId, error: reason }) => `${conversationId} (${reason})`).join("; ")}. Retry closing the session.`);
360
+ error.code = `${errorPrefix}codex_conversation_close_failed`;
361
+ error.retryable = true;
362
+ error.details = { failed, sessionId: normalizeText(sessionId) };
363
+ throw error;
364
+ }
365
+ }
366
+
367
+ return Object.freeze({
368
+ stopCodexAppServerConversation,
369
+ stopPersistentCodexConversation,
370
+ interruptDetachedCodexAppServerConversationTurn,
371
+ deleteScopedCodexAppServerConversation,
372
+ deleteDetachedCodexAppServerConversationThread,
373
+ closeCodexAppServerConversations
374
+ });
375
+ }
376
+
377
+ /** @typedef {ReturnType<typeof createCodexConversationStops>} CodexConversationStops */
@@ -3,11 +3,20 @@ import { createReentrantConversationStorage } from "./storage.js";
3
3
  import { isCompletedEnvelopeTurn } from "./transcript.js";
4
4
  import { conversationAgentRunRecord, conversationAgentRunEvent } from "./agentRun.js";
5
5
 
6
- /** The original native run occupies its bound conversation segment. Reading a
7
- * binding without a run never constructs historical ownership or writes state.
6
+ /** @typedef {import("../types.js").ConversationTranscript} ConversationTranscript */
7
+ /** @typedef {import("../types.js").ConversationStreams} ConversationStreams */
8
+
9
+ /**
10
+ * The native run occupies its bound conversation segment. Reading a binding without a run never constructs
11
+ * historical ownership or writes state.
12
+ * @param {object} dependencies
13
+ * @param {object} dependencies.storage The conversation storage the transcript uses.
14
+ * @param {string} dependencies.scope
15
+ * @param {string} dependencies.segmentId
16
+ * @param {() => boolean} dependencies.isCurrent The provider ownership fence.
17
+ * @param {() => Date} [dependencies.clock]
8
18
  */
9
- export function createCodexConversationRunStore({ storage, scope, segmentId, isCurrent, clock = () => new Date() } = {}) {
10
- if (typeof isCurrent !== "function") throw new TypeError("A native run store requires its provider ownership fence.");
19
+ export function createCodexConversationRunStore({ storage, scope, segmentId, isCurrent, clock = () => new Date() }) {
11
20
  storage = createReentrantConversationStorage(storage);
12
21
  const runId = "codex_app_server";
13
22
 
@@ -41,32 +50,43 @@ export function createCodexConversationRunStore({ storage, scope, segmentId, isC
41
50
  });
42
51
  }
43
52
 
53
+ async function getSession(sessionId) {
54
+ const run = await readAgentRun(sessionId, runId);
55
+ return { sessionId, agentRuns: run ? [run] : [] };
56
+ }
57
+
58
+ async function writeAgentRunEvent(sessionId, requestedRunId, input = {}) {
59
+ assertScope(sessionId, requestedRunId);
60
+ return mutateSession(sessionId, async transaction => {
61
+ const previous = await readAgentRun(sessionId, runId) || { events: [], id: runId };
62
+ const record = conversationAgentRunEvent(previous, runId, input, { now: clock });
63
+ const metadata = await currentMetadata(transaction);
64
+ metadata.runtime.binding.codexAppServerRun = record;
65
+ await transaction.writeMetadata(metadata);
66
+ return record;
67
+ });
68
+ }
69
+
44
70
  return Object.freeze({
45
71
  readAgentRun,
46
72
  mutateSession,
47
- async getSession(sessionId) {
48
- const run = await readAgentRun(sessionId, runId);
49
- return { sessionId, agentRuns: run ? [run] : [] };
50
- },
51
- async writeAgentRunEvent(sessionId, requestedRunId, input = {}) {
52
- assertScope(sessionId, requestedRunId);
53
- return mutateSession(sessionId, async transaction => {
54
- const previous = await readAgentRun(sessionId, runId) || { events: [], id: runId };
55
- const record = conversationAgentRunEvent(previous, runId, input, { now: clock });
56
- const metadata = await currentMetadata(transaction);
57
- metadata.runtime.binding.codexAppServerRun = record;
58
- await transaction.writeMetadata(metadata);
59
- return record;
60
- });
61
- }
73
+ getSession,
74
+ writeAgentRunEvent
62
75
  });
63
76
  }
64
77
 
65
- /** The native owner uses the existing transcript and transient streams through
66
- * the same fenced transaction as its original run. Its private delivery read is
67
- * a projection of the one authored request, never another stored journal.
78
+ /**
79
+ * The native owner uses the transcript and transient streams through the same fenced transaction as its run. Its
80
+ * private delivery read is a projection of the one authored request, never another stored journal.
81
+ * @param {object} dependencies
82
+ * @param {object} dependencies.storage The conversation storage the transcript uses.
83
+ * @param {string} dependencies.scope
84
+ * @param {string} dependencies.segmentId
85
+ * @param {() => boolean} dependencies.isCurrent The provider ownership fence.
86
+ * @param {ConversationTranscript} dependencies.transcript
87
+ * @param {ConversationStreams} dependencies.streams
68
88
  */
69
- export function createCodexConversationStore({ storage, scope, segmentId, isCurrent, transcript, streams } = {}) {
89
+ export function createCodexConversationStore({ storage, scope, segmentId, isCurrent, transcript, streams }) {
70
90
  const runStore = createCodexConversationRunStore({ storage, scope, segmentId, isCurrent });
71
91
  const assertCurrent = sessionId => {
72
92
  if (sessionId !== scope || !isCurrent()) throw new Error("A retired provider cannot change this conversation.");
@@ -116,74 +136,88 @@ export function createCodexConversationStore({ storage, scope, segmentId, isCurr
116
136
  } });
117
137
  return transaction.readTurn(written.turnId);
118
138
  });
139
+
140
+ async function writeConversationUserMessage(sessionId, input = {}) {
141
+ return runStore.mutateSession(sessionId, async transaction => {
142
+ if (input.messageId && await transaction.hasMessage(input.messageId)) return null;
143
+ const metadata = await transaction.readMetadata();
144
+ const request = input.authoredRequest?.messageId === input.messageId ? input.authoredRequest
145
+ : metadata.runtime.request?.messageId === input.messageId ? metadata.runtime.request : null;
146
+ if (!request) {
147
+ const written = await transcript.writeConversationUserMessage(sessionId, input);
148
+ if (!written) return null;
149
+ await transaction.updateTurnMetadata(written.turnId, { runtime: {
150
+ status: "running", engine: "codex", segmentId, origin: "user",
151
+ ...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {})
152
+ } });
153
+ return transaction.readTurn(written.turnId);
154
+ }
155
+ const turnId = await transaction.nextTurnId();
156
+ await transaction.appendMessage(turnId, {
157
+ role: request.origin === "application" ? "system" : "user",
158
+ messageId: request.messageId, text: request.text, origin: request.origin || "user",
159
+ attachments: request.attachments || input.attachments || [], at: request.at || new Date().toISOString(),
160
+ ...(request.data !== undefined ? { data: request.data } : {}), ...(request.goal ? { goal: request.goal } : {}),
161
+ turnMetadata: { ...input.turnMetadata, runtime: { ...input.turnMetadata?.runtime,
162
+ status: "running", engine: "codex", segmentId, origin: request.origin || "user",
163
+ ...(metadata.runtime.request?.messageId === input.messageId && metadata.runtime.request.completedEnvelope === true
164
+ ? { completedEnvelope: true } : {}),
165
+ ...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {}),
166
+ ...(request.goal ? { goalMessageId: request.messageId } : {}) } }
167
+ });
168
+ return transaction.readTurn(turnId);
169
+ });
170
+ }
171
+
172
+ function readMetadataValue(sessionId, key) {
173
+ assertCurrent(sessionId);
174
+ if (key !== "assistant_delivery") throw new Error("The native conversation requested unknown delivery metadata.");
175
+ return storage.read(sessionId, async transaction => {
176
+ const { runtime } = await transaction.readMetadata();
177
+ if (!isCurrent() || runtime.segmentId !== segmentId || runtime.engine !== "codex") {
178
+ throw new Error("A retired native conversation cannot read current delivery metadata.");
179
+ }
180
+ const request = runtime.request;
181
+ return JSON.stringify({ engines: { codex: { pending: request && { ...request,
182
+ displayMessage: request.displayMessage ?? request.text,
183
+ displayAttachments: request.displayAttachments ?? request.attachments } } } });
184
+ });
185
+ }
186
+
187
+ function updateConversationStream(sessionId, input) {
188
+ return runStore.mutateSession(sessionId, async transaction => {
189
+ const authorship = await nativeAuthorship(transaction, input.nativeIdentity);
190
+ return streams.update(sessionId, { ...input, origin: authorship?.origin, authorship });
191
+ });
192
+ }
193
+
194
+ function completeConversationStreamMessage(sessionId, messageId, options) {
195
+ assertCurrent(sessionId);
196
+ if (options?.nativeIdentity) return runStore.mutateSession(sessionId, async transaction => {
197
+ const authorship = await nativeAuthorship(transaction, options.nativeIdentity);
198
+ return streams.complete(sessionId, messageId, { ...options, authorship,
199
+ ...(!authorship ? { text: undefined } : {}) });
200
+ });
201
+ return streams.complete(sessionId, messageId, options);
202
+ }
203
+
204
+ function clearConversationStream(sessionId) { assertCurrent(sessionId); return streams.clear(sessionId); }
205
+
119
206
  return Object.freeze({
120
- ...runStore,
207
+ readAgentRun: runStore.readAgentRun,
208
+ mutateSession: runStore.mutateSession,
209
+ getSession: runStore.getSession,
210
+ writeAgentRunEvent: runStore.writeAgentRunEvent,
121
211
  readConversationLog: reader("readConversationLog"),
122
212
  conversationMessageIdExists: reader("conversationMessageIdExists"),
123
213
  writeConversationAssistantMessage: writer("writeConversationAssistantMessage"),
124
214
  writeConversationCommentaryMessage: writer("writeConversationCommentaryMessage"),
125
215
  writeConversationThinkingMessage: writer("writeConversationThinkingMessage"),
126
216
  upsertConversationAssistantMessage: writer("upsertConversationAssistantMessage"),
127
- async writeConversationUserMessage(sessionId, input = {}) {
128
- return runStore.mutateSession(sessionId, async transaction => {
129
- if (input.messageId && await transaction.hasMessage(input.messageId)) return null;
130
- const metadata = await transaction.readMetadata();
131
- const request = input.authoredRequest?.messageId === input.messageId ? input.authoredRequest
132
- : metadata.runtime.request?.messageId === input.messageId ? metadata.runtime.request : null;
133
- if (!request) {
134
- const written = await transcript.writeConversationUserMessage(sessionId, input);
135
- if (!written) return null;
136
- await transaction.updateTurnMetadata(written.turnId, { runtime: {
137
- status: "running", engine: "codex", segmentId, origin: "user",
138
- ...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {})
139
- } });
140
- return transaction.readTurn(written.turnId);
141
- }
142
- const turnId = await transaction.nextTurnId();
143
- await transaction.appendMessage(turnId, {
144
- role: request.origin === "application" ? "system" : "user",
145
- messageId: request.messageId, text: request.text, origin: request.origin || "user",
146
- attachments: request.attachments || input.attachments || [], at: request.at || new Date().toISOString(),
147
- ...(request.data !== undefined ? { data: request.data } : {}), ...(request.goal ? { goal: request.goal } : {}),
148
- turnMetadata: { ...input.turnMetadata, runtime: { ...input.turnMetadata?.runtime,
149
- status: "running", engine: "codex", segmentId, origin: request.origin || "user",
150
- ...(metadata.runtime.request?.messageId === input.messageId && metadata.runtime.request.completedEnvelope === true
151
- ? { completedEnvelope: true } : {}),
152
- ...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {}),
153
- ...(request.goal ? { goalMessageId: request.messageId } : {}) } }
154
- });
155
- return transaction.readTurn(turnId);
156
- });
157
- },
158
- readMetadataValue(sessionId, key) {
159
- assertCurrent(sessionId);
160
- if (key !== "assistant_delivery") throw new Error("The native conversation requested unknown delivery metadata.");
161
- return storage.read(sessionId, async transaction => {
162
- const { runtime } = await transaction.readMetadata();
163
- if (!isCurrent() || runtime.segmentId !== segmentId || runtime.engine !== "codex") {
164
- throw new Error("A retired native conversation cannot read current delivery metadata.");
165
- }
166
- const request = runtime.request;
167
- return JSON.stringify({ engines: { codex: { pending: request && { ...request,
168
- displayMessage: request.displayMessage ?? request.text,
169
- displayAttachments: request.displayAttachments ?? request.attachments } } } });
170
- });
171
- },
172
- updateConversationStream(sessionId, input) {
173
- return runStore.mutateSession(sessionId, async transaction => {
174
- const authorship = await nativeAuthorship(transaction, input.nativeIdentity);
175
- return streams.update(sessionId, { ...input, origin: authorship?.origin, authorship });
176
- });
177
- },
178
- completeConversationStreamMessage(sessionId, messageId, options) {
179
- assertCurrent(sessionId);
180
- if (options?.nativeIdentity) return runStore.mutateSession(sessionId, async transaction => {
181
- const authorship = await nativeAuthorship(transaction, options.nativeIdentity);
182
- return streams.complete(sessionId, messageId, { ...options, authorship,
183
- ...(!authorship ? { text: undefined } : {}) });
184
- });
185
- return streams.complete(sessionId, messageId, options);
186
- },
187
- clearConversationStream(sessionId) { assertCurrent(sessionId); return streams.clear(sessionId); }
217
+ writeConversationUserMessage,
218
+ readMetadataValue,
219
+ updateConversationStream,
220
+ completeConversationStreamMessage,
221
+ clearConversationStream
188
222
  });
189
223
  }