@jskit-ai/assistant-core 0.1.200 → 0.1.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,288 @@
1
+ import { normalizeCodexRunText as normalizeText } from "./codexTurnState.js";
2
+
3
+ // The original shared runtime shutdown and wellbeing lifecycle. This facet
4
+ // uses the run/provider owners' existing collections; it does not own servers.
5
+ export function createCodexRuntimeLifecycle({
6
+ runOwner,
7
+ providerSessions,
8
+ helperLifecycle,
9
+ captureContext,
10
+ runInContext,
11
+ debugLog,
12
+ debugError,
13
+ closingError = null
14
+ }) {
15
+ const codexAppServerSessionClosures = new Map();
16
+ const codexAppServerWellbeingTimers = new Map();
17
+ const codexAppServerReconcileTasks = new Set();
18
+ let codexAppServerThreadReconcileGeneration = 0;
19
+ let codexAppServerServerClosing = false;
20
+ let codexAppServerShutdownPromise = null;
21
+
22
+ function assertOpen() {
23
+ if (!closingError) return providerSessions.assertOpen();
24
+ if (codexAppServerServerClosing) {
25
+ const error = new Error(closingError.message);
26
+ error.code = closingError.code;
27
+ error.retryable = closingError.retryable;
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ function closeCodexAppServerProvider(providerKey = "", options = {}) {
33
+ const normalizedProviderKey = normalizeText(providerKey);
34
+ if (!providerSessions.owner.providers.get(normalizedProviderKey)) {
35
+ stopCodexAppServerWellbeing(normalizedProviderKey);
36
+ providerSessions.managed.delete(normalizedProviderKey);
37
+ return;
38
+ }
39
+ providerSessions.owner.closeProvider(normalizedProviderKey, options);
40
+ }
41
+
42
+ async function retireAndCloseCodexAppServerProviderUnlocked(providerKey = "", options = {}) {
43
+ const normalizedProviderKey = normalizeText(providerKey);
44
+ if (!providerSessions.owner.providers.get(normalizedProviderKey)) {
45
+ closeCodexAppServerProvider(normalizedProviderKey, options);
46
+ return;
47
+ }
48
+ return providerSessions.owner.retireAndCloseProvider(normalizedProviderKey, options);
49
+ }
50
+
51
+ function retireAndCloseCodexAppServerProvider(providerKey = "", options = {}) {
52
+ return providerSessions.owner.withLifecycle(
53
+ () => retireAndCloseCodexAppServerProviderUnlocked(providerKey, options)
54
+ );
55
+ }
56
+
57
+ function codexAppServerProviderResources(providerKey) {
58
+ const normalizedProviderKey = normalizeText(providerKey);
59
+ return {
60
+ retire: runOwner.helperThreads.retireProvider,
61
+ reconcileRetirement: runOwner.helperThreads.reconcileProviderRetirement,
62
+ release(provider) {
63
+ runOwner.helperThreads.assertProviderReleased(provider);
64
+ stopCodexAppServerWellbeing(normalizedProviderKey);
65
+ providerSessions.managed.delete(normalizedProviderKey);
66
+ const subscriptionPrefix = `${normalizedProviderKey}:`;
67
+ for (const key of [...runOwner.eventSubscriptions.keys()]) {
68
+ if (key.startsWith(subscriptionPrefix)) {
69
+ runOwner.unsubscribeEventSubscription(key);
70
+ }
71
+ }
72
+ }
73
+ };
74
+ }
75
+
76
+ async function invalidateCodexAppServerRuntimes(input = {}) {
77
+ const result = await providerSessions.owner.invalidateRuntimes(input);
78
+ debugLog("appServerRuntime.invalidate.done", {
79
+ failedCount: result.failed.length,
80
+ providerCount: result.providerCount,
81
+ reason: normalizeText(input.reason),
82
+ stopped: result.stopped,
83
+ toolHomeSource: normalizeText(input.toolHomeSource)
84
+ });
85
+ return result;
86
+ }
87
+
88
+ function shutdownCodexAppServerRuntimes(input = {}) {
89
+ beginCodexAppServerShutdown();
90
+ if (!codexAppServerShutdownPromise) {
91
+ codexAppServerShutdownPromise = (async () => {
92
+ const invalidation = invalidateCodexAppServerRuntimes({
93
+ ...input,
94
+ includeOwned: true,
95
+ reason: "server-shutdown",
96
+ requireVerifiedExit: true,
97
+ stopOwnedRuntimes: true
98
+ });
99
+ const drain = drainCodexAppServerControllerTasks();
100
+ const [invalidated, drained] = await Promise.allSettled([
101
+ invalidation,
102
+ drain
103
+ ]);
104
+ if (drained.status === "rejected") {
105
+ throw drained.reason;
106
+ }
107
+ const verified = await invalidateCodexAppServerRuntimes({
108
+ ...input,
109
+ includeOwned: true,
110
+ reason: "server-shutdown",
111
+ requireVerifiedExit: true,
112
+ stopOwnedRuntimes: true
113
+ });
114
+ if (invalidated.status === "rejected") {
115
+ throw invalidated.reason;
116
+ }
117
+ const initial = invalidated.value;
118
+ const resultsByProvider = new Map([
119
+ ...initial.results,
120
+ ...verified.results
121
+ ].map((result) => [normalizeText(result?.providerKey), result]));
122
+ const results = [...resultsByProvider.values()];
123
+ return {
124
+ ...verified,
125
+ providerCount: Math.max(initial.providerCount, verified.providerCount),
126
+ results,
127
+ stopped: results.filter((result) => result?.stopped === true).length
128
+ };
129
+ })();
130
+ }
131
+ return codexAppServerShutdownPromise;
132
+ }
133
+
134
+ function stopCodexAppServerWellbeing(providerKey = "") {
135
+ const normalizedProviderKey = normalizeText(providerKey);
136
+ const timer = codexAppServerWellbeingTimers.get(normalizedProviderKey);
137
+ if (timer) {
138
+ clearTimeout(timer);
139
+ codexAppServerWellbeingTimers.delete(normalizedProviderKey);
140
+ }
141
+ }
142
+
143
+ function beginCodexAppServerShutdown() {
144
+ if (codexAppServerServerClosing) {
145
+ return;
146
+ }
147
+ codexAppServerServerClosing = true;
148
+ providerSessions.owner.beginShutdown();
149
+ codexAppServerThreadReconcileGeneration += 1;
150
+ for (const providerKey of [...codexAppServerWellbeingTimers.keys()]) {
151
+ stopCodexAppServerWellbeing(providerKey);
152
+ }
153
+ }
154
+
155
+ async function drainCodexAppServerControllerTasks() {
156
+ while (true) {
157
+ const pending = new Set([
158
+ ...runOwner.conversationTurnStarts.values(),
159
+ ...helperLifecycle.projectOperations.values(),
160
+ ...helperLifecycle.cleanups.values(),
161
+ ...helperLifecycle.mutations.values(),
162
+ ...helperLifecycle.restores.values(),
163
+ ...helperLifecycle.turnStarts.values(),
164
+ ...runOwner.messageDeliveries.values(),
165
+ ...runOwner.reasoningPersistQueues.values(),
166
+ ...runOwner.resultFinalizations.values(),
167
+ ...providerSessions.owner.lifecycleTasks,
168
+ ...providerSessions.owner.runtimeAcquisitions,
169
+ ...codexAppServerReconcileTasks,
170
+ ...codexAppServerSessionClosures.values(),
171
+ ...runOwner.threadReconciliations.values(),
172
+ ...runOwner.notificationQueue.tasks.values()
173
+ ]);
174
+ if (pending.size === 0) {
175
+ return;
176
+ }
177
+ await Promise.allSettled([...pending]);
178
+ }
179
+ }
180
+
181
+ function scheduleCodexAppServerWellbeing(providerKey = "") {
182
+ const normalizedProviderKey = normalizeText(providerKey);
183
+ const managed = providerSessions.managed.get(normalizedProviderKey);
184
+ if (!managed || codexAppServerServerClosing) {
185
+ stopCodexAppServerWellbeing(normalizedProviderKey);
186
+ return;
187
+ }
188
+ stopCodexAppServerWellbeing(normalizedProviderKey);
189
+ const timer = setTimeout(() => {
190
+ void (async () => {
191
+ const current = providerSessions.managed.get(normalizedProviderKey);
192
+ if (!current) {
193
+ return;
194
+ }
195
+ await runInContext(current.projectContext, async () => {
196
+ await runOwner.checkManagedConnection(normalizedProviderKey);
197
+ });
198
+ })()
199
+ .catch((error) => {
200
+ debugLog("appServerDaemon.wellbeing.error", {
201
+ error: debugError(error),
202
+ providerKey: normalizedProviderKey,
203
+ sessionId: managed.sessionId
204
+ });
205
+ })
206
+ .finally(() => {
207
+ if (
208
+ !codexAppServerServerClosing &&
209
+ providerSessions.managed.has(normalizedProviderKey)
210
+ ) {
211
+ scheduleCodexAppServerWellbeing(normalizedProviderKey);
212
+ }
213
+ });
214
+ }, providerSessions.wellbeingMs);
215
+ timer.unref?.();
216
+ codexAppServerWellbeingTimers.set(normalizedProviderKey, timer);
217
+ }
218
+
219
+ function rememberCodexAppServerManagedSession(providerKey = "", {
220
+ providerOptions = {},
221
+ sessionId = "",
222
+ executionRoot = "",
223
+ threadId = "",
224
+ workdir = ""
225
+ } = {}) {
226
+ assertOpen();
227
+ const normalizedProviderKey = normalizeText(providerKey);
228
+ if (!normalizedProviderKey) {
229
+ return;
230
+ }
231
+ providerSessions.managed.set(normalizedProviderKey, {
232
+ projectContext: captureContext(),
233
+ providerOptions,
234
+ sessionId: normalizeText(sessionId),
235
+ executionRoot: normalizeText(executionRoot),
236
+ threadId: normalizeText(threadId),
237
+ workdir: normalizeText(workdir)
238
+ });
239
+ scheduleCodexAppServerWellbeing(normalizedProviderKey);
240
+ }
241
+
242
+ function invalidateRuntimes(input = {}, preparation) {
243
+ const serverShutdown = normalizeText(input?.reason) === "server-shutdown";
244
+ if (serverShutdown) {
245
+ beginCodexAppServerShutdown();
246
+ }
247
+ return (async () => {
248
+ if (!preparation.enabled) {
249
+ return preparation.disabledResult;
250
+ }
251
+ if (serverShutdown) {
252
+ return shutdownCodexAppServerRuntimes(input);
253
+ }
254
+ if (input.includeOwned === true) {
255
+ return invalidateCodexAppServerRuntimes({
256
+ ...input,
257
+ includeOwned: true,
258
+ requireVerifiedExit: true,
259
+ stopOwnedRuntimes: true
260
+ });
261
+ }
262
+ const helpers = await preparation.helpers;
263
+ helperLifecycle.assertRestored(
264
+ await helperLifecycle.restoreThreads(helpers.projectRuntimeRoot, helpers.context)
265
+ );
266
+ return invalidateCodexAppServerRuntimes({
267
+ ...input,
268
+ includeOwned: false,
269
+ requireVerifiedExit: false
270
+ });
271
+ })();
272
+ }
273
+
274
+ return {
275
+ get closing() { return codexAppServerServerClosing; },
276
+ assertOpen,
277
+ get reconcileGeneration() { return codexAppServerThreadReconcileGeneration; },
278
+ nextReconcileGeneration() { return ++codexAppServerThreadReconcileGeneration; },
279
+ reconcileTasks: codexAppServerReconcileTasks,
280
+ sessionClosures: codexAppServerSessionClosures,
281
+ invalidateRuntimes,
282
+ resources: codexAppServerProviderResources,
283
+ retireProvider: retireAndCloseCodexAppServerProvider,
284
+ rememberManagedSession: rememberCodexAppServerManagedSession,
285
+ managedSession: providerKey => providerSessions.managed.get(providerKey),
286
+ managedThreadId: providerKey => providerSessions.managed.get(providerKey)?.threadId
287
+ };
288
+ }
@@ -0,0 +1,96 @@
1
+ import { normalizeCodexRunText } from "./codexTurnState.js";
2
+
3
+ // Cleanup of the existing run/provider/Helper owners. The application keeps
4
+ // its authority and storage projections; all native coordination stays here.
5
+ export function createCodexSessionCleanup({ namespace, runOwner, providerSessions, helperLifecycle, journal,
6
+ conversations, debugLog, debugError }) {
7
+ const providerOwner = providerSessions?.owner;
8
+
9
+ async function closeSession(sessionId, cleanup) {
10
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
11
+ const sessionKey = cleanup.sessionKey;
12
+ const pendingClosures = cleanup.pending;
13
+ const restrictedClosures = cleanup.restrictedPending;
14
+ let pending = pendingClosures.get(sessionKey);
15
+ while (pending) {
16
+ const pendingIsRestricted = restrictedClosures.has(pending);
17
+ if (cleanup.restricted && !pendingIsRestricted) {
18
+ await pending.catch(() => null);
19
+ pending = pendingClosures.get(sessionKey);
20
+ continue;
21
+ }
22
+ if (!cleanup.restricted && pendingIsRestricted) {
23
+ await cleanup.assertPublicAccess();
24
+ }
25
+ return pending;
26
+ }
27
+ const closing = (async () => {
28
+ await providerOwner.drainThreadEnvironmentTasks(sessionKey);
29
+ journal.clearSessionRecoveryTimers(normalizedSessionId);
30
+ let prepared = null;
31
+ let session = null;
32
+ try {
33
+ prepared = await cleanup.read();
34
+ const restoration = await prepared.helpers;
35
+ helperLifecycle.assertRestored(await helperLifecycle.restoreThreads(
36
+ restoration.projectRuntimeRoot, restoration.context
37
+ ));
38
+ helperLifecycle.assertRetired(await helperLifecycle.retireAll({
39
+ projectRuntimeRoot: prepared.projectRuntimeRoot,
40
+ sessionId: normalizedSessionId
41
+ }));
42
+ session = prepared.session;
43
+ } catch (error) {
44
+ debugLog("appServerRuntime.closeSession.prepare.error", {
45
+ error: debugError(error),
46
+ sessionId: normalizedSessionId
47
+ });
48
+ throw error;
49
+ }
50
+ await conversations.closeCodexAppServerConversations(normalizedSessionId);
51
+ const { exitFailure } = await providerOwner.releaseSession(sessionKey, {
52
+ sessionId: normalizedSessionId,
53
+ runOwner,
54
+ get changeover() { return cleanup.changeover; },
55
+ preserveProcessExitProof: cleanup.preserveProcessExitProof,
56
+ get requireStopped() { return cleanup.requireStopped; },
57
+ session
58
+ });
59
+ await prepared.complete(exitFailure);
60
+ })();
61
+ pendingClosures.set(sessionKey, closing);
62
+ if (cleanup.restricted) {
63
+ restrictedClosures.add(closing);
64
+ }
65
+ try {
66
+ return await closing;
67
+ } finally {
68
+ if (pendingClosures.get(sessionKey) === closing) {
69
+ pendingClosures.delete(sessionKey);
70
+ }
71
+ }
72
+ }
73
+
74
+ async function closeProject(input = {}, preparation) {
75
+ const restoration = await preparation.helpers;
76
+ helperLifecycle.assertRestored(await helperLifecycle.restoreThreads(
77
+ restoration.projectRuntimeRoot, restoration.context
78
+ ));
79
+ const sessionNamespacePrefix = namespace("");
80
+ for (const sessionKey of [...conversations.codexAppServerConversations.keys()]) {
81
+ if (!sessionKey.startsWith(sessionNamespacePrefix)) {
82
+ continue;
83
+ }
84
+ await conversations.closeCodexAppServerConversations(
85
+ sessionKey.slice(sessionNamespacePrefix.length)
86
+ );
87
+ }
88
+ return providerOwner.stopProvidersForProject(input, {
89
+ managed: providerSessions.managed,
90
+ keyFields: providerSessions.keyFields,
91
+ helperThreads: helperLifecycle
92
+ });
93
+ }
94
+
95
+ return { closeSession, closeProject };
96
+ }
@@ -0,0 +1,284 @@
1
+ import { normalizeCodexRunText as normalizeText, codexAppServerErrorMessage as errorMessage } from "./codexTurnState.js";
2
+ import { isPlainObject as isRecord } from "./normalize.js";
3
+
4
+ // Original batch restoration, prune and startup unsubscribe. The run owner's
5
+ // existing lifecycle and per-thread readiness retain all state and coordination.
6
+ export function createCodexSessionReconciliation({
7
+ runOwner, providerSessions, helperLifecycle, lifecycle, createRuntime,
8
+ namespace, turnState, sessionIdRequiredError, debugLog, debugError
9
+ }) {
10
+ async function pruneCodexAppServerManagedSessions({
11
+ keepProviderKeys = new Set(),
12
+ projectContextRoot = ""
13
+ } = {}) {
14
+ const normalizedProjectContextRoot = normalizeText(projectContextRoot);
15
+ if (!normalizedProjectContextRoot) {
16
+ return;
17
+ }
18
+ for (const [providerKey, managed] of [...providerSessions.managed.entries()]) {
19
+ if (keepProviderKeys.has(providerKey)) {
20
+ continue;
21
+ }
22
+ if (providerSessions.owner.providers.get(providerKey)?.observationFailure) continue;
23
+ if (normalizeText(managed.projectContext?.targetRoot) === normalizedProjectContextRoot) {
24
+ await lifecycle.retireProvider(providerKey);
25
+ }
26
+ }
27
+ }
28
+
29
+ async function retireAndCloseCodexAppServerProviderForSession(sessionId = "", options = null) {
30
+ const normalizedSessionId = normalizeText(sessionId);
31
+ if (
32
+ !normalizedSessionId ||
33
+ !options ||
34
+ typeof options !== "object" ||
35
+ Array.isArray(options)
36
+ ) {
37
+ return;
38
+ }
39
+ await lifecycle.retireProvider(
40
+ providerSessions.context(normalizedSessionId, options).providerKey
41
+ );
42
+ }
43
+
44
+ function codexAppServerReconcileSessionId(value = {}) {
45
+ if (typeof value === "string") {
46
+ return normalizeText(value);
47
+ }
48
+ return normalizeText(value?.sessionId || value?.id);
49
+ }
50
+
51
+ async function unsubscribeCodexAppServerThreadForSession(sessionId = "", options, preparation) {
52
+ const normalizedSessionId = normalizeText(sessionId);
53
+ if (!normalizedSessionId) {
54
+ return { ok: true, sessionId: normalizedSessionId, status: "notSubscribed" };
55
+ }
56
+ const unsubscribeParameters = await preparation.session(normalizedSessionId, options);
57
+ if (!unsubscribeParameters.threadId) {
58
+ return {
59
+ ok: true,
60
+ providerOptions: unsubscribeParameters.providerOptions,
61
+ sessionId: normalizedSessionId,
62
+ status: "notSubscribed"
63
+ };
64
+ }
65
+ return providerSessions.owner.unsubscribeSessionThread(
66
+ namespace(normalizedSessionId), normalizedSessionId, { unsubscribeParameters }
67
+ );
68
+ }
69
+
70
+ async function unsubscribeCodexAppServerThreadsForSessions(sessions = [], preparation) {
71
+ const results = [];
72
+ const failed = [];
73
+ const seenSessionIds = new Set();
74
+ for (const session of Array.isArray(sessions) ? sessions : []) {
75
+ const sessionId = normalizeText(session?.sessionId || session?.id || session);
76
+ if (!sessionId || seenSessionIds.has(sessionId)) {
77
+ continue;
78
+ }
79
+ seenSessionIds.add(sessionId);
80
+ let providerOptions = null;
81
+ try {
82
+ const result = await unsubscribeCodexAppServerThreadForSession(sessionId, {
83
+ session: isRecord(session) ? session : null
84
+ }, preparation);
85
+ providerOptions = result?.providerOptions || null;
86
+ results.push(result);
87
+ } catch (error) {
88
+ failed.push({
89
+ error: errorMessage(error, preparation.failureMessage),
90
+ sessionId
91
+ });
92
+ debugLog("appServerThread.unsubscribeKnown.error", {
93
+ error: debugError(error),
94
+ sessionId
95
+ });
96
+ } finally {
97
+ if (providerOptions) {
98
+ try {
99
+ await retireAndCloseCodexAppServerProviderForSession(
100
+ sessionId,
101
+ providerOptions
102
+ );
103
+ } catch (error) {
104
+ failed.push({
105
+ code: normalizeText(error?.code),
106
+ error: errorMessage(error, preparation.cleanupFailureMessage),
107
+ retryable: error?.retryable === true,
108
+ sessionId
109
+ });
110
+ }
111
+ }
112
+ }
113
+ }
114
+ return {
115
+ failed,
116
+ ok: failed.length === 0,
117
+ results,
118
+ sessionCount: seenSessionIds.size
119
+ };
120
+ }
121
+
122
+ async function reconcileCodexAppServerThreadForSession(sessionId = "", {
123
+ agentSettings = {}
124
+ } = {}, preparation) {
125
+ providerSessions.assertOpen();
126
+ const normalizedSessionId = normalizeText(sessionId);
127
+ if (!normalizedSessionId) {
128
+ return { ok: false, error: sessionIdRequiredError };
129
+ }
130
+ const prepared = await preparation.session(normalizedSessionId, { agentSettings });
131
+ if (prepared.result) return prepared.result;
132
+ const { context } = prepared;
133
+ if (turnState(context.session).status === "observation_lost") {
134
+ const recovered = await prepared.recover(runOwner.recoverSessionObservationLoss);
135
+ const turn = turnState(recovered);
136
+ return prepared.stopped({
137
+ ok: !turn.active,
138
+ sessionId: normalizedSessionId,
139
+ status: turn.active ? "stopUnverified" : "stopped",
140
+ threadId: turn.threadId
141
+ }, recovered);
142
+ }
143
+ return runOwner.reconcileThread(normalizedSessionId, context, prepared.thread);
144
+ }
145
+
146
+ async function reconcileCodexAppServerThreads(sessions = [], {
147
+ agentSettings = {}
148
+ } = {}, preparation) {
149
+ providerSessions.assertOpen();
150
+ const runtime = await createRuntime();
151
+ providerSessions.assertOpen();
152
+ const projectContextRoot = normalizeText(runtime.projectContextRoot);
153
+ const helpers = await preparation.helperRestoration(runtime);
154
+ const helperRestore = await helperLifecycle.restoreThreads(helpers.projectRuntimeRoot, helpers.context);
155
+ const reconcileGeneration = lifecycle.nextReconcileGeneration();
156
+ const sessionIds = [...new Set((Array.isArray(sessions) ? sessions : [])
157
+ .map((session) => codexAppServerReconcileSessionId(session))
158
+ .filter(Boolean))];
159
+ const results = await Promise.all(sessionIds.map(async (sessionId) => {
160
+ try {
161
+ const session = await runtime.getSession(sessionId, { inspectSource: false });
162
+ let helperFailure = null;
163
+ let helperInventory = null;
164
+ if (helperRestore.ok !== false) {
165
+ try {
166
+ helperInventory = await helperLifecycle.withProjectOperation(runtime?.stateRoot, () => (
167
+ helperLifecycle.reconcileRuntimeUnlocked(preparation.helperInventory({ runtime, session }))
168
+ ));
169
+ } catch (error) {
170
+ helperFailure = helperLifecycle.failure({
171
+ projectRuntimeRoot: runtime.stateRoot,
172
+ sessionId
173
+ }, error);
174
+ debugLog("appServerHelper.reconcile.error", {
175
+ error: debugError(error),
176
+ sessionId
177
+ });
178
+ }
179
+ }
180
+ const result = await reconcileCodexAppServerThreadForSession(sessionId, {
181
+ agentSettings
182
+ }, preparation);
183
+ return {
184
+ ...result,
185
+ helperFailure,
186
+ helperInventory
187
+ };
188
+ } catch (error) {
189
+ debugLog("appServerThread.reconcile.error", {
190
+ error: debugError(error),
191
+ sessionId
192
+ });
193
+ const failure = {
194
+ ok: false,
195
+ error: errorMessage(error, preparation.failureMessage),
196
+ sessionId
197
+ };
198
+ if (error?.code === preparation.runtimeBusyCode) {
199
+ failure.code = error.code;
200
+ failure.retryable = true;
201
+ failure.status = "reconnecting";
202
+ }
203
+ return failure;
204
+ }
205
+ }));
206
+ const failed = [
207
+ ...helperRestore.failed,
208
+ ...results.flatMap((result) => [
209
+ result?.ok === false ? result : null,
210
+ result?.helperFailure || null
211
+ ].filter(Boolean))
212
+ ];
213
+ const keepProviderKeys = new Set();
214
+ const reconnectingSessionIds = new Set();
215
+ for (const result of results) {
216
+ const providerKey = normalizeText(result?.providerKey);
217
+ if (providerKey) {
218
+ keepProviderKeys.add(providerKey);
219
+ }
220
+ if (result.code === preparation.runtimeBusyCode) {
221
+ reconnectingSessionIds.add(result.sessionId);
222
+ }
223
+ }
224
+ for (const [providerKey, managed] of providerSessions.managed) {
225
+ if (reconnectingSessionIds.has(managed.sessionId)) {
226
+ keepProviderKeys.add(providerKey);
227
+ }
228
+ }
229
+ if (reconcileGeneration === lifecycle.reconcileGeneration) {
230
+ await runOwner.waitForThreadReconciliations({
231
+ keepProviderKeys,
232
+ projectContextRoot
233
+ });
234
+ }
235
+ if (reconcileGeneration === lifecycle.reconcileGeneration) {
236
+ await pruneCodexAppServerManagedSessions({
237
+ keepProviderKeys,
238
+ projectContextRoot
239
+ });
240
+ } else {
241
+ debugLog("appServerThread.reconcile.pruneSkipped", {
242
+ reason: "stale_reconcile",
243
+ sessionCount: sessionIds.length,
244
+ projectContextRoot
245
+ });
246
+ }
247
+ debugLog("appServerThread.reconcile.done", {
248
+ failedCount: failed.length,
249
+ sessionCount: sessionIds.length
250
+ });
251
+ return {
252
+ helperRestore,
253
+ failed,
254
+ ok: failed.length === 0,
255
+ results,
256
+ sessionCount: sessionIds.length
257
+ };
258
+ }
259
+
260
+ async function reconcileSessions(sessions = [], options = {}, preparation) {
261
+ if (!preparation.enabled) return preparation.disabledResult;
262
+ providerSessions.assertOpen();
263
+ const reconciliation = Promise.resolve().then(() => (
264
+ reconcileCodexAppServerThreads(sessions, options, preparation)
265
+ ));
266
+ lifecycle.reconcileTasks.add(reconciliation);
267
+ try {
268
+ return await reconciliation;
269
+ } finally {
270
+ lifecycle.reconcileTasks.delete(reconciliation);
271
+ }
272
+ }
273
+
274
+ async function unsubscribeSessions(sessions = [], preparation) {
275
+ if (!preparation.enabled) return preparation.disabledResult;
276
+ return unsubscribeCodexAppServerThreadsForSessions(sessions, preparation);
277
+ }
278
+
279
+ return {
280
+ reconcileSessions,
281
+ unsubscribeSessions,
282
+ retireProviderForSession: retireAndCloseCodexAppServerProviderForSession
283
+ };
284
+ }