@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,465 @@
1
+ import {
2
+ codexAppServerOwnedRuntimeKey,
3
+ codexAppServerRuntimeStopWasVerified,
4
+ directoryExists,
5
+ withRuntimeLifecycle
6
+ } from "./codexRuntimeInventory.js";
7
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
8
+
9
+ /**
10
+ * Stops cached providers and releases what sessions and projects leave behind.
11
+ * @param {object} dependencies
12
+ * @param {Map<string, object>} dependencies.records
13
+ * @param {import("./codexProviderOwner.js").CodexAppServerProviderOwner["providers"]} dependencies.providers
14
+ * @param {import("./codexProviderOwner.js").CodexAppServerProviderOwner["providerOwners"]} dependencies.providerOwners
15
+ * @param {import("./codexProviderOwner.js").CodexAppServerProviderOwner["ownedRuntimes"]} dependencies.ownedRuntimes
16
+ * @param {<T>(operation: () => T, providerKey?: string, providerOptions?: object | null) => Promise<Awaited<T>>} dependencies.withLifecycle
17
+ * @param {(providerKey: string, providerOptions: object) => boolean} dependencies.runtimeIsShared
18
+ * @param {(provider: object) => void} dependencies.forgetOwnedRuntime
19
+ * @param {(providerKey: string, options?: object) => void} dependencies.closeProvider
20
+ * @param {string} dependencies.requiredStopCode
21
+ * @param {string} dependencies.exitUnverifiedCode
22
+ * @param {string} dependencies.runtimeCloseError
23
+ * @param {(event: string, details: object) => void} dependencies.debugLog
24
+ * @param {(error: unknown) => unknown} dependencies.debugError
25
+ */
26
+ export function createCodexProviderRelease({
27
+ records,
28
+ providers,
29
+ providerOwners,
30
+ ownedRuntimes,
31
+ withLifecycle,
32
+ runtimeIsShared,
33
+ forgetOwnedRuntime,
34
+ closeProvider,
35
+ requiredStopCode,
36
+ exitUnverifiedCode,
37
+ runtimeCloseError,
38
+ debugLog,
39
+ debugError
40
+ }) {
41
+ async function stopCachedProviderUnlocked(providerKey = "", {
42
+ preserveProcessExitProof = false,
43
+ requireStopped = false
44
+ } = {}) {
45
+ const normalizedProviderKey = normalizeText(providerKey);
46
+ const provider = providers.get(normalizedProviderKey);
47
+ if (!provider) {
48
+ closeProvider(normalizedProviderKey);
49
+ return {
50
+ providerKey: normalizedProviderKey,
51
+ stopped: false
52
+ };
53
+ }
54
+ // readable-check: allow optional-call - optional operation: a driver's resources have no retire step, and a borrowed provider has no resources
55
+ await records.get(normalizedProviderKey)?.resources?.retire?.(provider);
56
+ const sharedProcessRetained = runtimeIsShared(
57
+ normalizedProviderKey, providerOwners.get(normalizedProviderKey)?.providerOptions
58
+ );
59
+ if (sharedProcessRetained) {
60
+ closeProvider(normalizedProviderKey);
61
+ return {
62
+ providerKey: normalizedProviderKey,
63
+ sessionDetached: true,
64
+ sharedProcessRetained: true,
65
+ stopped: false
66
+ };
67
+ }
68
+ // readable-check: allow method-probe - safety invariant: a provider that cannot stop would leave its process running unverified
69
+ if (typeof provider.stopRuntime !== "function") {
70
+ closeProvider(normalizedProviderKey);
71
+ throw new Error("Codex app-server provider must implement stopRuntime().");
72
+ }
73
+ let providerStoppedRuntime = false;
74
+ try {
75
+ const stoppedRuntime = await provider.stopRuntime({
76
+ preserveProcessExitProof: preserveProcessExitProof || requireStopped
77
+ });
78
+ providerStoppedRuntime = codexAppServerRuntimeStopWasVerified(stoppedRuntime);
79
+ if (requireStopped && !providerStoppedRuntime) {
80
+ const error = new Error("Codex app-server process exit could not be verified.");
81
+ error.code = requiredStopCode;
82
+ error.retryable = true;
83
+ throw error;
84
+ }
85
+ if (providerStoppedRuntime) {
86
+ forgetOwnedRuntime(provider);
87
+ }
88
+ return {
89
+ ...(stoppedRuntime && typeof stoppedRuntime === "object" ? stoppedRuntime : {}),
90
+ providerKey: normalizedProviderKey,
91
+ stopped: providerStoppedRuntime
92
+ };
93
+ } finally {
94
+ closeProvider(normalizedProviderKey, {
95
+ closeProvider: !providerStoppedRuntime
96
+ });
97
+ }
98
+ }
99
+
100
+ function stopCachedProvider(providerKey = "", options = {}) {
101
+ return withLifecycle(
102
+ () => stopCachedProviderUnlocked(providerKey, options),
103
+ normalizeText(providerKey)
104
+ );
105
+ }
106
+
107
+ function runtimeExitUnverifiedError(providerKey = "") {
108
+ const error = new Error("Codex app-server process exit could not be verified.");
109
+ error.code = exitUnverifiedCode;
110
+ error.providerKey = normalizeText(providerKey);
111
+ error.retryable = true;
112
+ return error;
113
+ }
114
+
115
+ async function stopPreparedRuntime(context) {
116
+ const { runtime, parameters } = context.runtimeHost;
117
+ return runtime.stop(parameters);
118
+ }
119
+
120
+ async function stopProvider(providerKey, providerOptions, context = {}) {
121
+ const { preserveProcessExitProof = false, requireStopped = false } = context;
122
+ const provider = providers.get(providerKey);
123
+ if (!provider) {
124
+ if (runtimeIsShared(providerKey, providerOptions)) {
125
+ return {
126
+ providerKey,
127
+ sessionDetached: true,
128
+ sharedProcessRetained: true,
129
+ stopped: false
130
+ };
131
+ }
132
+ const stoppedRuntime = await stopPreparedRuntime(context);
133
+ const stopped = codexAppServerRuntimeStopWasVerified(stoppedRuntime);
134
+ if (requireStopped && !stopped) {
135
+ throw runtimeExitUnverifiedError(providerKey);
136
+ }
137
+ return {
138
+ ...(Boolean(stoppedRuntime && typeof stoppedRuntime === "object" && !Array.isArray(stoppedRuntime)) ? stoppedRuntime : {}),
139
+ providerKey,
140
+ stopped
141
+ };
142
+ }
143
+ return stopCachedProvider(providerKey, {
144
+ preserveProcessExitProof,
145
+ requireStopped
146
+ });
147
+ }
148
+
149
+ async function stopThreadBeforeRelease(providerKey, provider, threadId) {
150
+ try {
151
+ await provider.stopThreadForObservationLoss(threadId, "");
152
+ } catch (error) {
153
+ // If other sessions retain the shared process, closing this
154
+ // socket alone is not proof that its native goal/work stopped.
155
+ const shared = runtimeIsShared(
156
+ providerKey, providerOwners.get(providerKey)?.providerOptions
157
+ );
158
+ if (shared) throw error;
159
+ // The caller's own shutdown proves process exit instead.
160
+ }
161
+ }
162
+
163
+ async function stopProvidersForProject({
164
+ preserveProcessExitProof = false,
165
+ projectContextRoot = "",
166
+ reason = "",
167
+ } = {}, { managed, keyFields, helperThreads, sessionKeyPrefix = "" }) {
168
+ const normalizedProjectContextRoot = normalizeText(projectContextRoot);
169
+ if (!normalizedProjectContextRoot) {
170
+ return {
171
+ failed: [],
172
+ ok: true,
173
+ projectContextRoot: normalizedProjectContextRoot,
174
+ providerCount: 0,
175
+ reason: normalizeText(reason),
176
+ results: [],
177
+ stopped: 0
178
+ };
179
+ }
180
+ const helperProviders = new Set(
181
+ helperThreads.records({
182
+ projectContextRoot: normalizedProjectContextRoot
183
+ }).map((record) => record.provider)
184
+ );
185
+ // Main threads name their project; every other provider of the project's
186
+ // sessions (Temporary chats, reads) is found by its session key.
187
+ const providerKeys = [...providers.keys()]
188
+ .filter((providerKey) => {
189
+ const current = managed.get(providerKey);
190
+ return normalizeText(current?.projectContext?.targetRoot) === normalizedProjectContextRoot ||
191
+ helperProviders.has(providers.get(providerKey)) ||
192
+ Boolean(sessionKeyPrefix && normalizeText(providerOwners.get(providerKey)?.sessionKey).startsWith(sessionKeyPrefix));
193
+ });
194
+ const failed = [];
195
+ const results = [];
196
+ for (const providerKey of providerKeys) {
197
+ try {
198
+ const fields = keyFields(providerKey);
199
+ results.push(await stopCachedProvider(providerKey, {
200
+ preserveProcessExitProof: Boolean(
201
+ preserveProcessExitProof &&
202
+ fields.sessionId &&
203
+ fields.executionMode !== "helper"
204
+ )
205
+ }));
206
+ } catch (error) {
207
+ failed.push({
208
+ code: normalizeText(error?.code),
209
+ error: normalizeText(error?.error || error?.message || error) || runtimeCloseError,
210
+ providerKey,
211
+ retryable: error?.retryable === true
212
+ });
213
+ }
214
+ }
215
+ const stopped = results.filter((result) => result.stopped).length;
216
+ debugLog("appServerRuntime.closeProject.done", {
217
+ failedCount: failed.length,
218
+ providerCount: providerKeys.length,
219
+ projectContextRoot: normalizedProjectContextRoot,
220
+ reason: normalizeText(reason),
221
+ stopped
222
+ });
223
+ return {
224
+ failed,
225
+ ok: failed.length === 0,
226
+ projectContextRoot: normalizedProjectContextRoot,
227
+ providerCount: providerKeys.length,
228
+ reason: normalizeText(reason),
229
+ results,
230
+ stopped
231
+ };
232
+ }
233
+
234
+ async function stopCachedProvidersForSession(sessionKey = "", options = {}) {
235
+ if (!sessionKey) {
236
+ return {
237
+ failed: [],
238
+ ok: true,
239
+ providerCount: 0,
240
+ results: [],
241
+ stopped: 0
242
+ };
243
+ }
244
+ const providerKeys = [...providers.keys()]
245
+ .filter((providerKey) => (
246
+ providerOwners.get(providerKey)?.sessionKey === sessionKey
247
+ ));
248
+ const failed = [];
249
+ const results = [];
250
+ for (const providerKey of providerKeys) {
251
+ try {
252
+ results.push(await stopCachedProvider(providerKey, options));
253
+ } catch (error) {
254
+ failed.push({
255
+ code: normalizeText(error?.code),
256
+ error: normalizeText(error?.error || error?.message || error) || runtimeCloseError,
257
+ providerKey,
258
+ retryable: error?.retryable === true
259
+ });
260
+ }
261
+ }
262
+ return {
263
+ failed,
264
+ ok: failed.length === 0,
265
+ providerCount: providerKeys.length,
266
+ results,
267
+ stopped: results.filter((result) => result.stopped).length
268
+ };
269
+ }
270
+
271
+ function drainThreadEnvironmentTasks(sessionKey) {
272
+ return Promise.all([...providers.entries()]
273
+ .filter(([key]) => providerOwners.get(key)?.sessionKey === sessionKey)
274
+ .flatMap(([, provider]) => [...(provider.threadEnvironmentTasks?.values() || [])])
275
+ .map((task) => task.catch(() => null)));
276
+ }
277
+
278
+ async function stopPersistedRuntime({ runtime, parameters }, runtimeDir) {
279
+ const result = await runtime.stop(parameters);
280
+
281
+ return {
282
+ ...result,
283
+ runtimeDirExists: await directoryExists(runtimeDir),
284
+ verifiedStopped: result?.stopped === true ||
285
+ result?.processExitVerified === true ||
286
+ result?.runtimeDirRemoved === true
287
+ };
288
+ }
289
+
290
+ function sessionExitProof(sessionKey, context) {
291
+ const { cachedProviders, persistedRuntime } = context;
292
+ const noRuntimeAcquired = cachedProviders.providerCount === 0 &&
293
+ !context.hasRuntime &&
294
+ !context.threadId &&
295
+ ![...ownedRuntimes.values()].some((record) => record.sessionKey === sessionKey);
296
+ const cachedRuntimeExitVerified = cachedProviders.providerCount > 0 &&
297
+ cachedProviders.results.length === cachedProviders.providerCount &&
298
+ cachedProviders.results.every((result) => (
299
+ result?.stopped === true ||
300
+ (result?.sessionDetached === true && result?.sharedProcessRetained === true)
301
+ ));
302
+ const persistedRuntimeExitVerified = persistedRuntime?.verifiedStopped === true ||
303
+ (
304
+ persistedRuntime?.sessionDetached === true &&
305
+ persistedRuntime?.sharedProcessRetained === true
306
+ );
307
+ return { noRuntimeAcquired, cachedRuntimeExitVerified, persistedRuntimeExitVerified };
308
+ }
309
+
310
+ async function unsubscribeSessionThread(sessionKey, sessionId, session) {
311
+ if (!sessionId) {
312
+ return { ok: true, sessionId, status: "notSubscribed" };
313
+ }
314
+ const { providerOptions, workdir, threadId } = session.unsubscribeParameters;
315
+ if (!threadId) {
316
+ return { ok: true, providerOptions, sessionId, status: "notSubscribed" };
317
+ }
318
+ // The existing record carries the workdir captured in its provider key.
319
+ // Never prepare new controls while releasing a retained connection.
320
+ const provider = [...records.values()].find(record =>
321
+ record.owner.sessionKey === sessionKey && record.owner.workdir === normalizeText(workdir))?.provider;
322
+ if (!provider) {
323
+ return { ok: true, providerOptions, sessionId, status: "notSubscribed" };
324
+ }
325
+ const result = await provider.unsubscribeThread(threadId);
326
+ debugLog("appServerThread.unsubscribe.done", {
327
+ sessionId,
328
+ status: normalizeText(result?.status),
329
+ threadId
330
+ });
331
+ return {
332
+ ok: true,
333
+ providerOptions,
334
+ result,
335
+ sessionId,
336
+ status: normalizeText(result?.status) || "unsubscribed",
337
+ threadId
338
+ };
339
+ }
340
+
341
+ async function stopPersistedSessionRuntime(session) {
342
+ const host = session.persistedRuntimeHost;
343
+ if (!host) return { stopped: false };
344
+ return stopPersistedRuntime(host, host.runtimeDir);
345
+ }
346
+
347
+ async function releasePersistedRuntimeProof(runtimeOptions, session) {
348
+ // Session closure already detached this participant. Its archived runtime
349
+ // path may now belong to the successor or another live conversation.
350
+ // Releasing the old proof must not stop or delete that shared process.
351
+ if (runtimeIsShared("", runtimeOptions)) {
352
+ return { sharedProcessRetained: true };
353
+ }
354
+ const existed = await directoryExists(runtimeOptions.runtimeDir);
355
+ if (!existed) return { existed };
356
+ const result = await stopPersistedSessionRuntime(session);
357
+ return { existed, result };
358
+ }
359
+
360
+ function releasePreparedRuntimeProof(preparation) {
361
+ return withLifecycle(async () => {
362
+ const prepared = preparation.read();
363
+ if (Object.hasOwn(prepared, "value")) return prepared.value;
364
+ return withRuntimeLifecycle(codexAppServerOwnedRuntimeKey("", prepared.runtimeOptions), async () => {
365
+ const evidence = await releasePersistedRuntimeProof(prepared.runtimeOptions, prepared);
366
+ return prepared.complete(evidence);
367
+ });
368
+ });
369
+ }
370
+
371
+ async function releaseSession(sessionKey, context) {
372
+ const { sessionId, runOwner, session } = context;
373
+ if (context.changeover) {
374
+ const threadId = session.threadId;
375
+ for (const [key, provider] of providers) {
376
+ if (!threadId || providerOwners.get(key)?.sessionKey !== sessionKey) continue;
377
+ await stopThreadBeforeRelease(key, provider, threadId);
378
+ }
379
+ }
380
+ let exitFailure = null;
381
+ try {
382
+ const unsubscribeResult = await unsubscribeSessionThread(sessionKey, sessionId, session);
383
+ session.providerOptions = unsubscribeResult?.providerOptions || session.providerOptions;
384
+ } catch (error) {
385
+ debugLog("appServerThread.unsubscribe.error", {
386
+ error: debugError(error),
387
+ sessionId
388
+ });
389
+ } finally {
390
+ await runOwner.notificationQueue.drain(sessionId);
391
+ const cachedProviders = await stopCachedProvidersForSession(sessionId ? sessionKey : "", {
392
+ preserveProcessExitProof: context.preserveProcessExitProof,
393
+ requireStopped: context.requireStopped
394
+ });
395
+ if (cachedProviders.ok === false) {
396
+ debugLog("appServerRuntime.closeSession.cached.error", {
397
+ failed: cachedProviders.failed,
398
+ sessionId
399
+ });
400
+ }
401
+ let persistedRuntime = null;
402
+ if (session.exists) {
403
+ persistedRuntime = cachedProviders.stopped > 0
404
+ ? {
405
+ stopped: true,
406
+ verifiedStopped: true
407
+ }
408
+ : runtimeIsShared("", session.providerOptions || {})
409
+ ? {
410
+ sessionDetached: true,
411
+ sharedProcessRetained: true,
412
+ stopped: false,
413
+ verifiedStopped: false
414
+ }
415
+ : session.hasRuntime
416
+ ? await stopPersistedSessionRuntime(session)
417
+ : {
418
+ stopped: false,
419
+ verifiedStopped: false
420
+ };
421
+ debugLog("appServerRuntime.closeSession.persisted.done", {
422
+ removed: persistedRuntime?.removed === true,
423
+ runtimeDirRemoved: persistedRuntime?.runtimeDirRemoved === true,
424
+ sessionId,
425
+ stopped: persistedRuntime?.removed === true || persistedRuntime?.runtimeDirRemoved === true
426
+ });
427
+ }
428
+ if (session.requiresExitProof) {
429
+ // A fresh session with no acquired native owner needs no process proof.
430
+ // Losing a cache or recorded runtime does not make it a fresh session.
431
+ const { noRuntimeAcquired, cachedRuntimeExitVerified, persistedRuntimeExitVerified } =
432
+ sessionExitProof(sessionKey, {
433
+ cachedProviders,
434
+ persistedRuntime,
435
+ get hasRuntime() { return session.hasRuntime; },
436
+ get threadId() { return session.threadId; }
437
+ });
438
+ if (cachedProviders.failed.length > 0 ||
439
+ (!noRuntimeAcquired && !cachedRuntimeExitVerified && !persistedRuntimeExitVerified)) {
440
+ exitFailure = {
441
+ cachedFailures: cachedProviders.failed,
442
+ cachedRuntimeExitVerified,
443
+ persistedRuntime: persistedRuntime || null
444
+ };
445
+ }
446
+ }
447
+ }
448
+ return { exitFailure };
449
+ }
450
+
451
+ return Object.freeze({
452
+ stopCachedProvider,
453
+ stopProvider,
454
+ stopThreadBeforeRelease,
455
+ stopProvidersForProject,
456
+ stopCachedProvidersForSession,
457
+ drainThreadEnvironmentTasks,
458
+ stopPersistedRuntime,
459
+ sessionExitProof,
460
+ unsubscribeSessionThread,
461
+ releasePersistedRuntimeProof,
462
+ releasePreparedRuntimeProof,
463
+ releaseSession
464
+ });
465
+ }
@@ -1,10 +1,21 @@
1
- import { normalizeCodexRunText } from "./codexTurnState.js";
2
1
  import { codexAppServerProjectHookTrustConfig } from "./codexConfiguration.js";
3
2
  import { createCodexAppServerModelCatalogCache } from "./codexModelCatalog.js";
4
3
  import { readCodexAppServerAccountIdentity } from "./codexHelperExecution.js";
5
-
6
- // Provider acquisition and per-operation provider contexts for a run owner's
7
- // managed threads, with their model catalogue and account identity reads.
4
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
5
+
6
+ /** @typedef {ReturnType<typeof createCodexProviderSelection>} CodexProviderSelection */
7
+
8
+ /**
9
+ * Provider acquisition and per-operation provider contexts for a run owner's
10
+ * managed threads, with their model catalogue and account identity reads.
11
+ * @param {object} dependencies
12
+ * @param {import("./codexProviderOwner.js").CodexProviderSessions} dependencies.providerSessions
13
+ * @param {(session: object) => object} dependencies.turnState
14
+ * @param {import("./codexRuntimeLifecycle.js").CodexHelperThreadLifecycle | null} dependencies.helperLifecycle
15
+ * @param {ReturnType<typeof import("./codexHelperExecution.js").createCodexHelperOwnershipError>} dependencies.helperOwnershipError
16
+ * @param {number} [dependencies.modelCatalogCacheMs]
17
+ * @param {string} [dependencies.errorPrefix]
18
+ */
8
19
  export function createCodexProviderSelection({
9
20
  providerSessions,
10
21
  turnState,
@@ -18,7 +29,7 @@ export function createCodexProviderSelection({
18
29
  errorPrefix
19
30
  });
20
31
  function sessionConnection(sessionId, providerOptions) {
21
- return providerSessions.context(normalizeCodexRunText(sessionId), providerOptions);
32
+ return providerSessions.context(normalizeText(sessionId), providerOptions);
22
33
  }
23
34
 
24
35
  function ensureSessionProvider(sessionId, providerOptions, threadId) {
@@ -34,7 +45,7 @@ export function createCodexProviderSelection({
34
45
  const connection = sessionConnection(sessionId, providerOptions);
35
46
  connection.runOwner.runtimeLifecycle.rememberManagedSession(connection.providerKey, {
36
47
  providerOptions,
37
- sessionId: normalizeCodexRunText(sessionId),
48
+ sessionId: normalizeText(sessionId),
38
49
  executionRoot,
39
50
  threadId,
40
51
  workdir
@@ -51,7 +62,7 @@ export function createCodexProviderSelection({
51
62
  managed.sessionId !== sessionId ||
52
63
  managed.workdir !== workdir ||
53
64
  managed.threadId !== threadId ||
54
- provider?.isAvailable?.() !== true ||
65
+ provider?.isAvailable() !== true ||
55
66
  provider.stale && !turnActive
56
67
  ) {
57
68
  continue;
@@ -62,21 +73,21 @@ export function createCodexProviderSelection({
62
73
  }
63
74
 
64
75
  async function ensureCodexAppServerProviderForManagedThread(session = {}, options = {}) {
65
- const normalizedSessionId = normalizeCodexRunText(session.sessionId || session.id);
76
+ const normalizedSessionId = normalizeText(session.sessionId || session.id);
66
77
  const turn = turnState(session);
67
78
  if (!normalizedSessionId || !turn.threadId) {
68
79
  return null;
69
80
  }
70
- const normalizedExecutionRoot = normalizeCodexRunText(options.executionRoot);
71
- const normalizedWorkdir = normalizeCodexRunText(options.workdir);
81
+ const normalizedExecutionRoot = normalizeText(options.executionRoot);
82
+ const normalizedWorkdir = normalizeText(options.workdir);
72
83
  for (const [providerKey, managed] of providerSessions.managed.entries()) {
73
84
  const fields = providerSessions.keyFields(providerKey);
74
85
  if (
75
86
  fields.sessionId !== normalizedSessionId ||
76
87
  (normalizedExecutionRoot && fields.executionRoot !== normalizedExecutionRoot) ||
77
88
  (normalizedWorkdir && fields.workdir !== normalizedWorkdir) ||
78
- normalizeCodexRunText(managed?.sessionId) !== normalizedSessionId ||
79
- normalizeCodexRunText(managed?.threadId) !== turn.threadId
89
+ normalizeText(managed?.sessionId) !== normalizedSessionId ||
90
+ normalizeText(managed?.threadId) !== turn.threadId
80
91
  ) {
81
92
  continue;
82
93
  }
@@ -107,7 +118,7 @@ export function createCodexProviderSelection({
107
118
  }
108
119
 
109
120
  async function codexAppServerRuntimeForVisibleTerminal(rawSessionId, session, options, managedIdentity, prepareProviderOptions) {
110
- const sessionId = normalizeCodexRunText(rawSessionId);
121
+ const sessionId = normalizeText(rawSessionId);
111
122
  const activeProvider = await ensureCodexAppServerProviderForActiveTurn(session, managedIdentity);
112
123
  const providerOptions = activeProvider?.providerOptions || await prepareProviderOptions();
113
124
  const provider = activeProvider?.provider || await ensureSessionProvider(sessionId, providerOptions);
@@ -210,7 +221,7 @@ export function createCodexProviderSelection({
210
221
  return readCodexAppServerAccountIdentity(provider, helperOwnershipError);
211
222
  }
212
223
 
213
- return {
224
+ return Object.freeze({
214
225
  readExecutionProfileModelCatalog,
215
226
  readProviderAccountIdentity,
216
227
  managedProviderKeyForThread,
@@ -225,5 +236,5 @@ export function createCodexProviderSelection({
225
236
  ensureSessionProvider,
226
237
  borrowSessionProvider,
227
238
  rememberManagedSession
228
- };
239
+ });
229
240
  }