@jskit-ai/assistant-core 0.1.199 → 0.1.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,397 @@
1
+ import {
2
+ codexAppServerTurnStatusIsActive,
3
+ codexAppServerThreadStatus,
4
+ codexAppServerReadThreadStatus,
5
+ normalizeCodexRunText
6
+ } from "./codexTurnState.js";
7
+ import { codexAppServerThreadIsMissing, ensureCodexAppServerThread } from "./codexProvider.js";
8
+ import { codexAppServerThreadRawValue, codexAppServerThreadTurnId } from "./codexEvents.js";
9
+
10
+ // Original native readiness, loaded-thread restoration and reconciliation drain.
11
+ // One Map retains the original run-owner lifetime and is shared with shutdown.
12
+ export function createCodexThreadReadiness({
13
+ providerSessions,
14
+ selection,
15
+ observer,
16
+ threadStatus,
17
+ recovery,
18
+ turnState,
19
+ debugLog,
20
+ debugError
21
+ }) {
22
+ const codexAppServerThreadReconciliations = new Map();
23
+ const {
24
+ ensureCodexAppServerProviderForActiveTurn,
25
+ codexAppServerSessionObserverOptions,
26
+ rememberCodexAppServerPreparedThread
27
+ } = selection;
28
+ const {
29
+ eventSubscriptionKey: codexAppServerEventSubscriptionKey,
30
+ eventSubscriptionIsCurrent: codexAppServerEventSubscriptionIsCurrent,
31
+ subscribeEvents: subscribeCodexAppServerEvents
32
+ } = observer;
33
+ const {
34
+ reconcileThreadStatus: reconcileCodexAppServerThreadStatus,
35
+ reconcileLoadedThreadStatus: reconcileCodexAppServerLoadedThreadStatus
36
+ } = threadStatus;
37
+ const {
38
+ recoverActiveTurn: recoverCodexAppServerActiveTurn
39
+ } = recovery;
40
+
41
+ async function ensureCodexAppServerThreadReady(sessionId, context, preparation) {
42
+ const { runtime, session, workdir } = context;
43
+ // Runtime recovery may await a disconnect callback that writes session
44
+ // state. This acquisition stays outside the original startup gate.
45
+ const activeProvider = await ensureCodexAppServerProviderForActiveTurn(session, preparation.managedIdentity);
46
+ let providerOptions = activeProvider?.providerOptions;
47
+ if (!providerOptions) {
48
+ providerOptions = await preparation.providerOptions(session);
49
+ }
50
+ const provider = activeProvider?.provider || await providerSessions.owner.ensureSession(
51
+ providerSessions.context(normalizeCodexRunText(sessionId), providerOptions),
52
+ preparation.threadId(session)
53
+ );
54
+ const { currentSession: preparedSession, thread } = await preparation.gate(async currentSession => {
55
+ const thread = await ensureCodexAppServerThread({
56
+ ...preparation.preparation(currentSession),
57
+ observeThread: threadId => subscribeCodexAppServerEvents(sessionId, provider, threadId,
58
+ codexAppServerSessionObserverOptions(sessionId, providerOptions)),
59
+ provider,
60
+ workdir
61
+ });
62
+ return { currentSession, thread };
63
+ });
64
+ subscribeCodexAppServerEvents(sessionId, provider, thread.threadId,
65
+ codexAppServerSessionObserverOptions(sessionId, providerOptions));
66
+ rememberCodexAppServerPreparedThread(sessionId, providerOptions, context, thread);
67
+ await reconcileCodexAppServerThreadStatus(sessionId, provider, thread.threadId, {
68
+ failUnconfirmedTrackedTurn: true,
69
+ observeLatestTurn: true,
70
+ requireTrackedTurn: true,
71
+ source: "thread_ready"
72
+ });
73
+ const ready = await preparation.ready(preparedSession);
74
+ let currentSession = await runtime.getSession(sessionId);
75
+ if (turnState(currentSession).state === "starting") {
76
+ currentSession = await recoverCodexAppServerActiveTurn(sessionId, {
77
+ provider,
78
+ retryOnError: false,
79
+ runtime
80
+ });
81
+ }
82
+ return { ...ready, currentSession, thread };
83
+ }
84
+
85
+ async function reconcileCodexAppServerThreadForSession(normalizedSessionId, context, preparation) {
86
+ const { runtime, session, executionRoot, workdir } = context;
87
+ const threadId = preparation.threadId(session);
88
+ if (!threadId) {
89
+ const providerOptions = await preparation.providerOptions(session);
90
+ const providerKey = providerSessions.context(normalizedSessionId, providerOptions).providerKey;
91
+ await providerSessions.owner.ensureSession(providerSessions.context(normalizedSessionId, providerOptions));
92
+ providerSessions.context(normalizedSessionId, providerOptions).runOwner.runtimeLifecycle.rememberManagedSession(providerKey, {
93
+ providerOptions,
94
+ sessionId: normalizedSessionId,
95
+ executionRoot,
96
+ workdir
97
+ });
98
+ return {
99
+ ok: true,
100
+ providerKey,
101
+ sessionId: normalizedSessionId,
102
+ status: "notStarted",
103
+ threadId: ""
104
+ };
105
+ }
106
+ const activeProvider = await ensureCodexAppServerProviderForActiveTurn(session, preparation.managedIdentity);
107
+ const providerOptions = activeProvider?.providerOptions || await preparation.providerOptions(session);
108
+ const providerKey = activeProvider?.providerKey || providerSessions.context(normalizedSessionId, providerOptions).providerKey;
109
+ const existing = codexAppServerThreadReconciliations.get(providerKey);
110
+ if (existing) {
111
+ return existing;
112
+ }
113
+ const reconciliation = (async () => {
114
+ const provider = activeProvider?.provider || await providerSessions.owner.ensureSession(
115
+ providerSessions.context(normalizedSessionId, providerOptions), threadId
116
+ );
117
+ try {
118
+ const restored = await restoreCodexAppServerLoadedThread(normalizedSessionId, {
119
+ runtime, provider, providerKey, threadId,
120
+ subscriptionOptions: {
121
+ get providerKey() { return providerSessions.context(normalizeCodexRunText(normalizedSessionId), providerOptions).providerKey; }
122
+ },
123
+ get resumeOptions() {
124
+ return preparation.resumeOptions;
125
+ },
126
+ remember() {
127
+ providerSessions.context(normalizedSessionId, providerOptions).runOwner.runtimeLifecycle.rememberManagedSession(providerKey, {
128
+ providerOptions,
129
+ sessionId: normalizedSessionId,
130
+ executionRoot,
131
+ threadId,
132
+ workdir
133
+ });
134
+ },
135
+ ready() { return preparation.ready(); }
136
+ });
137
+ if (restored) return restored;
138
+ } catch (error) {
139
+ debugLog("appServerThread.loadedList.error", {
140
+ error: debugError(error),
141
+ sessionId: normalizedSessionId,
142
+ threadId
143
+ });
144
+ }
145
+ const prepared = await preparation.readiness(ensureCodexAppServerThreadReady);
146
+ return {
147
+ ...prepared,
148
+ providerKey
149
+ };
150
+ })().finally(() => {
151
+ codexAppServerThreadReconciliations.delete(providerKey);
152
+ });
153
+ codexAppServerThreadReconciliations.set(providerKey, reconciliation);
154
+ return reconciliation;
155
+ }
156
+
157
+ async function waitForOtherCodexAppServerThreadReconciliations({
158
+ keepProviderKeys = new Set(),
159
+ projectContextRoot = ""
160
+ } = {}) {
161
+ const pending = [...codexAppServerThreadReconciliations.entries()]
162
+ .filter(([providerKey]) => !keepProviderKeys.has(providerKey));
163
+ if (pending.length === 0) {
164
+ return;
165
+ }
166
+ debugLog("appServerThread.reconcile.pruneWait.start", {
167
+ pendingCount: pending.length,
168
+ projectContextRoot: normalizeCodexRunText(projectContextRoot)
169
+ });
170
+ await Promise.allSettled(pending.map(([, reconciliation]) => reconciliation));
171
+ debugLog("appServerThread.reconcile.pruneWait.done", {
172
+ pendingCount: pending.length,
173
+ projectContextRoot: normalizeCodexRunText(projectContextRoot)
174
+ });
175
+ }
176
+
177
+ async function checkManagedConnection(providerKey = "", {
178
+ reconnect = true
179
+ } = {}) {
180
+ providerSessions.assertOpen();
181
+ const normalizedProviderKey = normalizeCodexRunText(providerKey);
182
+ const managed = providerSessions.managed.get(normalizedProviderKey);
183
+ const provider = providerSessions.owner.providers.get(normalizedProviderKey);
184
+ const sessionId = normalizeCodexRunText(managed?.sessionId);
185
+ const threadId = normalizeCodexRunText(managed?.threadId);
186
+ if (!managed || !provider || !sessionId) {
187
+ throw new Error("Codex app-server managed connection is incomplete.");
188
+ }
189
+ const current = providerSessions.connectionPolicy(sessionId, {
190
+ threadId,
191
+ get workdir() { return managed.workdir; },
192
+ get current() {
193
+ return providerSessions.managed.get(normalizedProviderKey) === managed &&
194
+ providerSessions.owner.providers.get(normalizedProviderKey) === provider;
195
+ }
196
+ });
197
+ current.assertCurrent();
198
+
199
+ if (reconnect) {
200
+ await providerSessions.owner.acquireRuntime({
201
+ operation: () => provider.ensureAvailable?.(),
202
+ provider,
203
+ providerKey: normalizedProviderKey,
204
+ providerOptions: managed.providerOptions || {}
205
+ });
206
+ }
207
+ return maintainCodexAppServerManagedConnection(sessionId, {
208
+ provider, providerKey: normalizedProviderKey, threadId, reconnect,
209
+ get workdir() { return managed.workdir; },
210
+ subscriptionOptions: {
211
+ get providerKey() { return providerSessions.context(normalizeCodexRunText(sessionId), managed.providerOptions || {}).providerKey; }
212
+ }
213
+ }, current);
214
+ }
215
+
216
+ async function maintainCodexAppServerManagedConnection(sessionId, connection, current) {
217
+ const { provider, providerKey, threadId, subscriptionOptions, reconnect = true } = connection;
218
+ if (!threadId) {
219
+ return {
220
+ ok: true,
221
+ status: "available"
222
+ };
223
+ }
224
+ let providerThread;
225
+ try {
226
+ providerThread = await codexAppServerReadThreadStatus(provider, threadId);
227
+ } catch (error) {
228
+ if (reconnect || !codexAppServerThreadIsMissing(error, threadId)) {
229
+ throw error;
230
+ }
231
+ providerThread = { status: "notLoaded" };
232
+ }
233
+ // The host rejects a late result after closure, renewal or lost admission.
234
+ const session = await current.readSession();
235
+ current.assertCurrent(session);
236
+ try {
237
+ const controls = await provider.ensureThreadControls?.(threadId);
238
+ if (controls?.recovered) providerThread = await codexAppServerReadThreadStatus(provider, threadId);
239
+ } catch (error) {
240
+ current.assertCurrent(session);
241
+ // Retain the existing stop owner and its visible recovery state. Do not
242
+ // spend automatic goal turns or repeatedly probe a known failed binding.
243
+ void Promise.resolve(provider.failObservation?.(error)).catch(() => null);
244
+ throw error;
245
+ }
246
+ current.assertCurrent(session);
247
+ const nativeStatus = codexAppServerThreadRawValue(providerThread).status;
248
+ const subscriptionKey = codexAppServerEventSubscriptionKey(
249
+ providerKey,
250
+ threadId
251
+ );
252
+ const subscriptionIsCurrent = codexAppServerEventSubscriptionIsCurrent(subscriptionKey, provider);
253
+ if (!reconnect && (
254
+ nativeStatus === "notLoaded" || nativeStatus?.type === "notLoaded" || !subscriptionIsCurrent
255
+ )) {
256
+ return { ok: true, requiresPreparation: true };
257
+ }
258
+ if (!subscriptionIsCurrent) {
259
+ subscribeCodexAppServerEvents(
260
+ sessionId,
261
+ provider,
262
+ threadId,
263
+ subscriptionOptions
264
+ );
265
+ await provider.resumeThread?.(threadId, {
266
+ cwd: normalizeCodexRunText(connection.workdir)
267
+ });
268
+ return reconcileCodexAppServerLoadedThreadStatus(
269
+ sessionId,
270
+ provider,
271
+ threadId,
272
+ {
273
+ observeLatestTurn: true
274
+ }
275
+ );
276
+ }
277
+
278
+ const trackedTurn = turnState(session);
279
+ const providerStatus = codexAppServerThreadStatus(providerThread);
280
+ const providerTurnId = codexAppServerThreadTurnId(providerThread);
281
+ const providerIsActive = codexAppServerTurnStatusIsActive(providerStatus);
282
+ const trackedIdentityMatches = (
283
+ trackedTurn.threadId === threadId &&
284
+ (!providerTurnId || trackedTurn.turnId === providerTurnId)
285
+ );
286
+ if (
287
+ (providerIsActive && (
288
+ !trackedTurn.active ||
289
+ trackedTurn.state !== "active" ||
290
+ !trackedIdentityMatches
291
+ )) ||
292
+ (!providerIsActive && trackedTurn.active)
293
+ ) {
294
+ return reconcileCodexAppServerThreadStatus(
295
+ sessionId,
296
+ provider,
297
+ threadId,
298
+ {
299
+ observeLatestTurn: true,
300
+ source: "wellbeing"
301
+ }
302
+ );
303
+ }
304
+ return {
305
+ ok: true,
306
+ status: "healthy",
307
+ threadId
308
+ };
309
+ }
310
+
311
+ async function codexAppServerLoadedThreadIds(provider = null) {
312
+ if (typeof provider?.listLoadedThreads !== "function") {
313
+ return null;
314
+ }
315
+ const threadIds = new Set();
316
+ let cursor = null;
317
+ do {
318
+ const response = await provider.listLoadedThreads({
319
+ ...(cursor ? { cursor } : {}),
320
+ limit: 100
321
+ });
322
+ for (const threadId of Array.isArray(response?.data) ? response.data : []) {
323
+ const normalizedThreadId = normalizeCodexRunText(threadId);
324
+ if (normalizedThreadId) {
325
+ threadIds.add(normalizedThreadId);
326
+ }
327
+ }
328
+ cursor = normalizeCodexRunText(response?.nextCursor);
329
+ } while (cursor);
330
+ return threadIds;
331
+ }
332
+
333
+ async function restoreCodexAppServerLoadedThread(sessionId, context) {
334
+ const { runtime, provider, providerKey, threadId } = context;
335
+ const loadedThreadIds = await codexAppServerLoadedThreadIds(provider);
336
+ if (!loadedThreadIds?.has(threadId)) return null;
337
+ const subscription = subscribeCodexAppServerEvents(
338
+ sessionId,
339
+ provider,
340
+ threadId,
341
+ context.subscriptionOptions
342
+ );
343
+ const subscriptionStatus = normalizeCodexRunText(subscription?.status) || "subscribed";
344
+ if (subscriptionStatus !== "alreadySubscribed") {
345
+ await provider.resumeThread(threadId, context.resumeOptions);
346
+ }
347
+ context.remember();
348
+ await reconcileCodexAppServerLoadedThreadStatus(
349
+ sessionId,
350
+ provider,
351
+ threadId,
352
+ {
353
+ observeLatestTurn: subscriptionStatus !== "alreadySubscribed"
354
+ }
355
+ ).catch((error) => {
356
+ debugLog("appServerThread.statusReconcile.error", {
357
+ error: debugError(error),
358
+ sessionId: sessionId,
359
+ threadId
360
+ });
361
+ });
362
+ let loadedSession = await runtime.getSession(sessionId);
363
+ if (turnState(loadedSession).state === "starting") {
364
+ loadedSession = await recoverCodexAppServerActiveTurn(sessionId, {
365
+ provider,
366
+ retryOnError: false,
367
+ runtime
368
+ });
369
+ }
370
+ await context.ready();
371
+ if (subscriptionStatus === "alreadySubscribed") {
372
+ return {
373
+ ok: true,
374
+ providerKey,
375
+ sessionId: sessionId,
376
+ status: "alreadySubscribed",
377
+ threadId
378
+ };
379
+ }
380
+ return {
381
+ ok: true,
382
+ providerKey,
383
+ sessionId: sessionId,
384
+ status: subscriptionStatus === "resubscribed" ? "resubscribed" : "loaded",
385
+ threadId
386
+ };
387
+ }
388
+
389
+ return {
390
+ threadReconciliations: codexAppServerThreadReconciliations,
391
+ ensureThreadReady: ensureCodexAppServerThreadReady,
392
+ reconcileThread: reconcileCodexAppServerThreadForSession,
393
+ waitForThreadReconciliations: waitForOtherCodexAppServerThreadReconciliations,
394
+ checkManagedConnection,
395
+ restoreLoadedThread: restoreCodexAppServerLoadedThread
396
+ };
397
+ }