@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,395 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { deleteCodexAppServerThread, codexAppServerRuntimeStopWasVerified } from "./codexProvider.js";
3
+ import { CODEX_HELPER_THREAD_LIFECYCLES } from "./codexHelperThreadLedger.js";
4
+
5
+ export function normalizeText(value) {
6
+ return String(value || "").trim();
7
+ }
8
+
9
+ export async function directoryExists(filePath = "") {
10
+ try {
11
+ return (await stat(filePath)).isDirectory();
12
+ } catch (error) {
13
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
14
+ return false;
15
+ }
16
+ throw error;
17
+ }
18
+ }
19
+
20
+ // Recovery operations of the same Helper lifecycle. All maps and record mutations
21
+ // belong to the supplied lifecycle; this module creates no owner or registry.
22
+ export function createCodexHelperThreadRecovery(lifecycle, {
23
+ providerOwner,
24
+ applicationName,
25
+ errorPrefix
26
+ }) {
27
+ const codexAppServerProviders = providerOwner.providers;
28
+ const {
29
+ threads: codexAppServerHelperThreads,
30
+ mutations: codexAppServerHelperThreadMutations,
31
+ restores: codexAppServerHelperThreadRestores,
32
+ key: codexAppServerHelperThreadKey,
33
+ ledger: codexAppServerHelperThreadLedger,
34
+ providerFingerprint: codexAppServerProviderKeyFingerprint,
35
+ attach: attachCodexAppServerHelperThread,
36
+ remove: removeCodexAppServerHelperThread,
37
+ retire: retireCodexAppServerHelperThread,
38
+ ownershipError: codexAppServerHelperOwnershipError,
39
+ ownershipMatches: codexAppServerHelperThreadOwnershipMatches,
40
+ failure: codexAppServerHelperFailure
41
+ } = lifecycle;
42
+
43
+ async function restoreCodexAppServerHelperThread(record = {}, context = {}) {
44
+ const { ledger = null } = context;
45
+ const key = codexAppServerHelperThreadKey(record);
46
+ const pendingMutation = codexAppServerHelperThreadMutations.get(key);
47
+ if (pendingMutation) {
48
+ await pendingMutation.catch(() => null);
49
+ }
50
+ const existing = codexAppServerHelperThreads.get(key);
51
+ if (existing && (
52
+ existing.ownershipId !== record.ownershipId ||
53
+ existing.revision < record.revision
54
+ )) {
55
+ throw codexAppServerHelperOwnershipError(
56
+ "Persisted Codex helper ownership changed while the controller was running.",
57
+ { sessionId: record.sessionId, threadId: record.threadId }
58
+ );
59
+ }
60
+ const cleanupRequired = record.lifecycle === CODEX_HELPER_THREAD_LIFECYCLES.CLEANUP_REQUIRED;
61
+ if (existing && !cleanupRequired) {
62
+ if (!context.retiring) {
63
+ return { record: existing, retiredThreadId: "" };
64
+ }
65
+ await retireCodexAppServerHelperThread(existing);
66
+ return { record: null, retiredThreadId: existing.threadId };
67
+ }
68
+ if (!await directoryExists(record.identity.runtime.runtimeDir)) {
69
+ if (existing) {
70
+ await retireCodexAppServerHelperThread(existing);
71
+ } else {
72
+ await ledger.remove(record);
73
+ }
74
+ return { record: null, retiredThreadId: record.threadId };
75
+ }
76
+ const prepared = await context.prepareProvider();
77
+ const { providerOptions, providerKey } = prepared;
78
+ const contextChanged = !existing &&
79
+ codexAppServerProviderKeyFingerprint(providerKey) !== record.identity.providerKeyFingerprint;
80
+ if (contextChanged) {
81
+ const staleProvider = providerOwner.createNativeProvider({
82
+ ...providerOptions,
83
+ runtimeDir: record.identity.runtime.runtimeDir
84
+ });
85
+ try {
86
+ const currentRuntime = await staleProvider.currentRuntimeInfo();
87
+ const accountChanged = record.identity.runtime.accountIdentitySignature !==
88
+ normalizeText(currentRuntime.accountIdentitySignature);
89
+ if (accountChanged && !cleanupRequired) {
90
+ throw codexAppServerHelperOwnershipError(
91
+ "Reconnect the original Codex account to resume or delete this helper thread.",
92
+ { retryable: false, sessionId: record.sessionId, threadId: record.threadId }
93
+ );
94
+ }
95
+ if (accountChanged || contextChanged) {
96
+ // Context drift cannot reuse the previous provider configuration.
97
+ const stopped = await staleProvider.stopRuntime(accountChanged ? {
98
+ expectedAccountIdentitySignature: record.identity.runtime.accountIdentitySignature
99
+ } : {});
100
+ const retired = accountChanged
101
+ ? stopped?.ownershipSuperseded === true || stopped?.processExitVerified === true
102
+ : codexAppServerRuntimeStopWasVerified(stopped);
103
+ if (!retired) {
104
+ throw codexAppServerHelperOwnershipError(
105
+ "The earlier Codex runtime could not be verified as retired; its cleanup record has been preserved.",
106
+ { sessionId: record.sessionId, threadId: record.threadId }
107
+ );
108
+ }
109
+ await ledger.remove(record);
110
+ codexAppServerHelperThreads.delete(key);
111
+ return { record: null, retiredThreadId: record.threadId };
112
+ }
113
+ } finally {
114
+ staleProvider.close?.();
115
+ }
116
+ }
117
+ if (existing) {
118
+ await retireCodexAppServerHelperThread(existing);
119
+ return { record: null, retiredThreadId: existing.threadId };
120
+ }
121
+ let provider = codexAppServerProviders.get(providerKey);
122
+ if (!provider) {
123
+ const policy = prepared.restoreProvider;
124
+ provider = providerOwner.createProvider({
125
+ providerKey, providerOptions,
126
+ ...policy,
127
+ create: (onObservationLost) => providerOwner.createNativeProvider({ ...providerOptions, onObservationLost })
128
+ });
129
+ }
130
+ if (typeof provider.currentRuntimeInfo !== "function") {
131
+ throw codexAppServerHelperOwnershipError(
132
+ "The Codex provider cannot prove persisted runtime ownership.",
133
+ {
134
+ sessionId: record.sessionId,
135
+ threadId: record.threadId
136
+ }
137
+ );
138
+ }
139
+ const expectedRuntime = await provider.currentRuntimeInfo();
140
+ if (!codexAppServerHelperThreadOwnershipMatches(record, {
141
+ providerKey,
142
+ runtime: expectedRuntime
143
+ }, {
144
+ // Cleanup deletes locally owned history; it never resumes account-bound work.
145
+ requireAccount: !cleanupRequired,
146
+ requireEndpoint: false
147
+ })) {
148
+ throw codexAppServerHelperOwnershipError(
149
+ "The current Codex runtime/auth identity does not match persisted helper ownership.",
150
+ {
151
+ retryable: record.identity.runtime.accountIdentitySignature === normalizeText(expectedRuntime.accountIdentitySignature),
152
+ sessionId: record.sessionId,
153
+ threadId: record.threadId
154
+ }
155
+ );
156
+ }
157
+ await providerOwner.acquireRuntime({
158
+ operation: () => provider.ensureAvailable?.(),
159
+ provider,
160
+ providerKey,
161
+ providerOptions
162
+ });
163
+ const currentRuntime = await provider.currentRuntimeInfo();
164
+ const currentServer = provider.currentServerInfo?.();
165
+ if (!codexAppServerHelperThreadOwnershipMatches(record, {
166
+ providerKey,
167
+ runtime: currentRuntime,
168
+ server: currentServer
169
+ }, {
170
+ requireAccount: !cleanupRequired,
171
+ requireEndpoint: true,
172
+ requireServer: !cleanupRequired
173
+ })) {
174
+ throw codexAppServerHelperOwnershipError(
175
+ "The connected Codex server identity does not match persisted helper ownership.",
176
+ {
177
+ sessionId: record.sessionId,
178
+ threadId: record.threadId
179
+ }
180
+ );
181
+ }
182
+ const currentLedger = await ledger.readAll();
183
+ if (currentLedger.failures.length > 0) {
184
+ throw codexAppServerHelperOwnershipError(
185
+ "Persisted Codex helper ownership could not be revalidated before restore.",
186
+ {
187
+ failed: currentLedger.failures,
188
+ sessionId: record.sessionId,
189
+ threadId: record.threadId
190
+ }
191
+ );
192
+ }
193
+ const currentDurable = currentLedger.records.find((candidate) => (
194
+ codexAppServerHelperThreadKey(candidate) === key
195
+ ));
196
+ if (!currentDurable) {
197
+ return { record: null, retiredThreadId: record.threadId };
198
+ }
199
+ if (
200
+ currentDurable.ownershipId !== record.ownershipId ||
201
+ currentDurable.revision !== record.revision
202
+ ) {
203
+ throw codexAppServerHelperOwnershipError(
204
+ "Persisted Codex helper ownership changed before it could be restored.",
205
+ {
206
+ sessionId: record.sessionId,
207
+ threadId: record.threadId
208
+ }
209
+ );
210
+ }
211
+ const attached = attachCodexAppServerHelperThread({
212
+ durable: currentDurable,
213
+ ledger,
214
+ provider
215
+ });
216
+ if (
217
+ attached.lifecycle !== CODEX_HELPER_THREAD_LIFECYCLES.READY ||
218
+ context.retiring
219
+ ) {
220
+ await retireCodexAppServerHelperThread(attached);
221
+ return { record: null, retiredThreadId: attached.threadId };
222
+ }
223
+ return { record: attached, retiredThreadId: "" };
224
+ }
225
+
226
+ async function restoreCodexAppServerHelperThreads(projectRuntimeRoot = "", context = {}) {
227
+ const previous = codexAppServerHelperThreadRestores.get(projectRuntimeRoot) || Promise.resolve();
228
+ const restore = previous.catch(() => null).then(async () => {
229
+ const ledger = codexAppServerHelperThreadLedger(projectRuntimeRoot);
230
+ const listed = await ledger.readAll();
231
+ const failed = listed.failures.map((failure) => ({
232
+ ...failure,
233
+ projectRuntimeRoot
234
+ }));
235
+ const normalizedSessionId = normalizeText(context.sessionId);
236
+ const records = listed.records.filter((record) => {
237
+ return !normalizedSessionId || record.sessionId === normalizedSessionId;
238
+ });
239
+ const retiredThreadIds = [];
240
+ const retiredBySession = new Map();
241
+ for (const record of records) {
242
+ try {
243
+ const prepared = await context.prepareRecord(record, ledger);
244
+ const restored = await restoreCodexAppServerHelperThread(record, prepared);
245
+ if (normalizeText(restored?.retiredThreadId)) {
246
+ retiredThreadIds.push(normalizeText(restored.retiredThreadId));
247
+ const retired = retiredBySession.get(record.sessionId) || [];
248
+ retired.push(normalizeText(restored.retiredThreadId));
249
+ retiredBySession.set(record.sessionId, retired);
250
+ }
251
+ } catch (error) {
252
+ failed.push(codexAppServerHelperFailure(record, error));
253
+ }
254
+ }
255
+ await context.publish({ records, failed, retiredBySession, normalizedSessionId });
256
+ return {
257
+ failed,
258
+ ok: failed.length === 0,
259
+ projectRuntimeRoot,
260
+ recordCount: records.length,
261
+ retiredThreadIds: [...new Set(retiredThreadIds)]
262
+ };
263
+ });
264
+ codexAppServerHelperThreadRestores.set(projectRuntimeRoot, restore);
265
+ try {
266
+ return await restore;
267
+ } finally {
268
+ if (codexAppServerHelperThreadRestores.get(projectRuntimeRoot) === restore) {
269
+ codexAppServerHelperThreadRestores.delete(projectRuntimeRoot);
270
+ }
271
+ }
272
+ }
273
+
274
+ async function reconcileCodexAppServerHelperRuntimeUnlocked({
275
+ sessionId = "",
276
+ projectRuntimeRoot = "",
277
+ prepareProvider
278
+ } = {}) {
279
+ const ledger = codexAppServerHelperThreadLedger(projectRuntimeRoot);
280
+ const listed = await ledger.readAll();
281
+ if (listed.failures.length > 0) {
282
+ throw codexAppServerHelperOwnershipError(
283
+ `${applicationName} cannot inventory helper threads while durable ownership is malformed.`,
284
+ { failed: listed.failures, sessionId }
285
+ );
286
+ }
287
+ const ownedRecords = listed.records.filter((record) => record.sessionId === sessionId);
288
+ const remainingOwnedThreadIds = new Set(
289
+ ownedRecords.map((record) => record.threadId)
290
+ );
291
+ const retireOwnedReadyThreadsMissingFrom = async (inventoryThreadIds) => {
292
+ const retiredThreadIds = [];
293
+ for (const durable of ownedRecords) {
294
+ if (
295
+ durable.lifecycle !== CODEX_HELPER_THREAD_LIFECYCLES.READY ||
296
+ inventoryThreadIds.has(durable.threadId)
297
+ ) {
298
+ continue;
299
+ }
300
+ const record = codexAppServerHelperThreads.get(
301
+ codexAppServerHelperThreadKey(durable)
302
+ );
303
+ if (!record || record.ownershipId !== durable.ownershipId) {
304
+ throw codexAppServerHelperOwnershipError(
305
+ `${applicationName} cannot retire missing helper ownership without its verified controller record.`,
306
+ { sessionId, threadId: durable.threadId }
307
+ );
308
+ }
309
+ if (
310
+ record.lifecycle !== CODEX_HELPER_THREAD_LIFECYCLES.READY ||
311
+ record.revision !== durable.revision
312
+ ) {
313
+ continue;
314
+ }
315
+ try {
316
+ await removeCodexAppServerHelperThread(record);
317
+ } catch (error) {
318
+ if (error?.code === `${errorPrefix}codex_helper_thread_unavailable`) {
319
+ continue;
320
+ }
321
+ throw error;
322
+ }
323
+ remainingOwnedThreadIds.delete(durable.threadId);
324
+ retiredThreadIds.push(durable.threadId);
325
+ }
326
+ return retiredThreadIds;
327
+ };
328
+ const prepared = await prepareProvider();
329
+ const { providerOptions, providerKey } = prepared;
330
+ let provider = codexAppServerProviders.get(providerKey);
331
+ if (
332
+ ownedRecords.length === 0 &&
333
+ !provider &&
334
+ !await directoryExists(providerOptions.runtimeDir)
335
+ ) {
336
+ const retiredMissingThreadIds = await retireOwnedReadyThreadsMissingFrom(new Set());
337
+ return {
338
+ deletedThreadIds: [],
339
+ ok: true,
340
+ ownedThreadIds: [...remainingOwnedThreadIds],
341
+ providerKey,
342
+ retiredMissingThreadIds,
343
+ status: "runtimeAbsent"
344
+ };
345
+ }
346
+ provider ||= await providerOwner.ensureSession(prepared.sessionContext);
347
+ if (typeof provider.listHelperThreads !== "function") {
348
+ throw codexAppServerHelperOwnershipError(
349
+ "The Codex helper provider cannot authoritatively inventory its threads.",
350
+ { sessionId }
351
+ );
352
+ }
353
+ const inventory = await provider.listHelperThreads();
354
+ if (!Array.isArray(inventory?.threadIds)) {
355
+ throw codexAppServerHelperOwnershipError(
356
+ "The Codex helper provider returned an invalid thread inventory.",
357
+ { sessionId }
358
+ );
359
+ }
360
+ const inventoryThreadIds = new Set(inventory.threadIds.map(normalizeText).filter(Boolean));
361
+ const unknownThreadIds = inventory.threadIds.filter((threadId) => {
362
+ return normalizeText(threadId) && !remainingOwnedThreadIds.has(normalizeText(threadId));
363
+ });
364
+ const deletedThreadIds = [];
365
+ for (const threadId of unknownThreadIds) {
366
+ try {
367
+ await deleteCodexAppServerThread({ provider, threadId });
368
+ } catch (error) {
369
+ if (error?.code !== "codex_helper_thread_delete_unconfirmed") throw error;
370
+ throw codexAppServerHelperOwnershipError(
371
+ "Codex did not confirm deletion of an unowned helper thread.",
372
+ { sessionId, threadId }
373
+ );
374
+ }
375
+ deletedThreadIds.push(threadId);
376
+ }
377
+ const retiredMissingThreadIds = await retireOwnedReadyThreadsMissingFrom(
378
+ inventoryThreadIds
379
+ );
380
+ return {
381
+ deletedThreadIds,
382
+ ok: true,
383
+ ownedThreadIds: [...remainingOwnedThreadIds],
384
+ providerKey,
385
+ retiredMissingThreadIds,
386
+ status: "reconciled"
387
+ };
388
+ }
389
+
390
+ return {
391
+ restoreThread: restoreCodexAppServerHelperThread,
392
+ restoreThreads: restoreCodexAppServerHelperThreads,
393
+ reconcileRuntimeUnlocked: reconcileCodexAppServerHelperRuntimeUnlocked
394
+ };
395
+ }