@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,638 @@
1
+ import crypto from "node:crypto";
2
+ import { createCodexHelperOwnershipError, errorMessage, prepareDetachedCodexHelperThread } from "./codexHelperExecution.js";
3
+ import { deleteCodexAppServerHelperThread } from "./codexProvider.js";
4
+ import { createCodexHelperThreadRecovery, directoryExists, normalizeText } from "./codexHelperThreadRecovery.js";
5
+ import {
6
+ CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
7
+ CODEX_HELPER_THREAD_LIFECYCLES
8
+ } from "./codexHelperThreadLedger.js";
9
+
10
+ const CODEX_APP_SERVER_PROVIDER_ID = "codex_app_server";
11
+
12
+ function isRecord(value) {
13
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
14
+ }
15
+
16
+ // Internal facet of the original run owner. Public supplies policy and authorized
17
+ // scope inputs; native ownership, pending work and retirement have one owner.
18
+ export function createCodexHelperThreadLifecycle({
19
+ providerOwner,
20
+ ledgerOwner,
21
+ applicationName = "Application",
22
+ errorPrefix = ""
23
+ } = {}) {
24
+ const codexAppServerProviders = providerOwner.providers;
25
+ const {
26
+ createCodexHelperThreadLedger: codexHelperThreadLedgerFactory,
27
+ defineCodexHelperThreadRecord
28
+ } = ledgerOwner;
29
+ const codexAppServerHelperThreads = new Map();
30
+ const codexAppServerHelperProjectOperations = new Map();
31
+ const codexAppServerHelperThreadCleanups = new Map();
32
+ const codexAppServerHelperThreadLedgers = new Map();
33
+ const codexAppServerHelperThreadMutations = new Map();
34
+ const codexAppServerHelperTurnStarts = new Map();
35
+ const codexAppServerHelperThreadRestores = new Map();
36
+
37
+ function codexAppServerHelperThreadKey({
38
+ projectRuntimeRoot = "",
39
+ sessionId = "",
40
+ threadId = ""
41
+ } = {}) {
42
+ return [
43
+ normalizeText(projectRuntimeRoot),
44
+ normalizeText(sessionId),
45
+ normalizeText(threadId)
46
+ ].join("\u001f");
47
+ }
48
+
49
+ async function withCodexAppServerHelperProjectOperation(
50
+ projectRuntimeRoot = "",
51
+ operation
52
+ ) {
53
+ const key = normalizeText(projectRuntimeRoot);
54
+ if (!key || typeof operation !== "function") {
55
+ throw codexAppServerHelperOwnershipError(
56
+ `${applicationName} project runtime state is unavailable for low-cost assistant ownership.`
57
+ );
58
+ }
59
+ const previous = codexAppServerHelperProjectOperations.get(key) || Promise.resolve();
60
+ const current = previous.catch(() => null).then(operation);
61
+ codexAppServerHelperProjectOperations.set(key, current);
62
+ try {
63
+ return await current;
64
+ } finally {
65
+ if (codexAppServerHelperProjectOperations.get(key) === current) {
66
+ codexAppServerHelperProjectOperations.delete(key);
67
+ }
68
+ }
69
+ }
70
+
71
+ function codexAppServerHelperThreadUnavailableError(threadId = "") {
72
+ const error = new Error(
73
+ "This low-cost assistant thread is no longer available. Start the background task again instead of reusing another assistant conversation."
74
+ );
75
+ error.code = `${errorPrefix}codex_helper_thread_unavailable`;
76
+ error.statusCode = 409;
77
+ error.threadId = normalizeText(threadId);
78
+ return error;
79
+ }
80
+
81
+ function codexAppServerHelperThreadForOperation({
82
+ executionProfile = null,
83
+ lifecycles = [CODEX_HELPER_THREAD_LIFECYCLES.READY],
84
+ projectRuntimeRoot = "",
85
+ provider = null,
86
+ sessionId = "",
87
+ threadId = "",
88
+ turnId = "",
89
+ workdir = ""
90
+ } = {}) {
91
+ const record = codexAppServerHelperThreads.get(
92
+ codexAppServerHelperThreadKey({ projectRuntimeRoot, sessionId, threadId })
93
+ );
94
+ if (
95
+ !record ||
96
+ !lifecycles.includes(record.lifecycle) ||
97
+ record.provider !== provider ||
98
+ record.workdir !== normalizeText(workdir) ||
99
+ (normalizeText(turnId) && record.turnId !== normalizeText(turnId)) ||
100
+ !ledgerOwner.matchesExecutionProfile(
101
+ record.executionProfile,
102
+ executionProfile || {}
103
+ )
104
+ ) {
105
+ throw codexAppServerHelperThreadUnavailableError(threadId);
106
+ }
107
+ return record;
108
+ }
109
+
110
+ function inspectCodexAppServerHelperControl(operation, input, scope) {
111
+ const { sessionId, threadId, turnId } = scope;
112
+ if (operation === "delete") {
113
+ if (scope.retiredThreadIds?.includes(threadId)) {
114
+ return { deleted: true, ok: true, status: "deleted", threadId };
115
+ }
116
+ const helperThread = codexAppServerHelperThreads.get(
117
+ codexAppServerHelperThreadKey({ projectRuntimeRoot: scope.projectRuntimeRoot, sessionId, threadId })
118
+ );
119
+ if (helperThread) {
120
+ if (!isRecord(input.executionProfile)) throw codexAppServerHelperThreadUnavailableError(threadId);
121
+ return retireCodexAppServerHelperThread(codexAppServerHelperThreadForOperation({
122
+ executionProfile: input.executionProfile,
123
+ lifecycles: Object.values(CODEX_HELPER_THREAD_LIFECYCLES),
124
+ projectRuntimeRoot: scope.projectRuntimeRoot,
125
+ provider: scope.provider,
126
+ sessionId,
127
+ threadId,
128
+ workdir: scope.workdir
129
+ }));
130
+ }
131
+ if (isRecord(input.executionProfile)) throw codexAppServerHelperThreadUnavailableError(threadId);
132
+ } else if (isRecord(input.executionProfile)) {
133
+ codexAppServerHelperThreadForOperation({
134
+ executionProfile: input.executionProfile,
135
+ lifecycles: [CODEX_HELPER_THREAD_LIFECYCLES.ACTIVE],
136
+ projectRuntimeRoot: scope.projectRuntimeRoot,
137
+ provider: scope.provider,
138
+ sessionId,
139
+ threadId,
140
+ turnId,
141
+ workdir: scope.workdir
142
+ });
143
+ }
144
+ }
145
+
146
+ function codexAppServerHelperThreadLedger(projectRuntimeRoot = "") {
147
+ const normalizedProjectRuntimeRoot = normalizeText(projectRuntimeRoot);
148
+ let ledger = codexAppServerHelperThreadLedgers.get(normalizedProjectRuntimeRoot);
149
+ if (!ledger) {
150
+ ledger = codexHelperThreadLedgerFactory({
151
+ projectRuntimeRoot: normalizedProjectRuntimeRoot
152
+ });
153
+ codexAppServerHelperThreadLedgers.set(normalizedProjectRuntimeRoot, ledger);
154
+ }
155
+ return ledger;
156
+ }
157
+
158
+ function codexAppServerProviderKeyForProvider(provider = null) {
159
+ for (const [providerKey, candidate] of codexAppServerProviders.entries()) {
160
+ if (candidate === provider) {
161
+ return providerKey;
162
+ }
163
+ }
164
+ return "";
165
+ }
166
+
167
+ function codexAppServerProviderKeyFingerprint(providerKey = "") {
168
+ return `sha256:${crypto.createHash("sha256")
169
+ .update(normalizeText(providerKey))
170
+ .digest("hex")}`;
171
+ }
172
+
173
+ async function codexAppServerHelperThreadIdentity({
174
+ executionProfile = null,
175
+ provider = null
176
+ } = {}) {
177
+ if (
178
+ typeof provider?.currentRuntimeInfo !== "function" ||
179
+ typeof provider?.currentServerInfo !== "function"
180
+ ) {
181
+ throw new Error("Codex provider cannot prove durable helper thread ownership.");
182
+ }
183
+ const runtime = await provider.currentRuntimeInfo();
184
+ const server = provider.currentServerInfo();
185
+ const providerKey = codexAppServerProviderKeyForProvider(provider);
186
+ if (!providerKey) {
187
+ throw new Error("Codex helper provider is not owned by this controller.");
188
+ }
189
+ return Object.freeze({
190
+ providerId: normalizeText(executionProfile?.providerId),
191
+ providerKeyFingerprint: codexAppServerProviderKeyFingerprint(
192
+ providerKey
193
+ ),
194
+ runtime,
195
+ server,
196
+ transportId: CODEX_APP_SERVER_PROVIDER_ID
197
+ });
198
+ }
199
+
200
+ function attachCodexAppServerHelperThread({
201
+ durable = null,
202
+ ledger = null,
203
+ onRetired = null,
204
+ provider = null
205
+ } = {}) {
206
+ const key = codexAppServerHelperThreadKey(durable);
207
+ const record = Object.freeze({
208
+ ...durable,
209
+ durable,
210
+ ledger,
211
+ onRetired,
212
+ provider
213
+ });
214
+ codexAppServerHelperThreads.set(key, record);
215
+ return record;
216
+ }
217
+
218
+ async function rememberCodexAppServerHelperThread({
219
+ executionProfile = null,
220
+ lifecycle = CODEX_HELPER_THREAD_LIFECYCLES.READY,
221
+ onRetired = null,
222
+ projectContextRoot = "",
223
+ projectRuntimeRoot = "",
224
+ provider = null,
225
+ sessionId = "",
226
+ threadId = "",
227
+ turnId = "",
228
+ workdir = ""
229
+ } = {}) {
230
+ if (
231
+ !normalizeText(projectRuntimeRoot) ||
232
+ !normalizeText(sessionId) ||
233
+ !normalizeText(threadId) ||
234
+ !provider
235
+ ) {
236
+ throw codexAppServerHelperThreadUnavailableError(threadId);
237
+ }
238
+ const now = new Date().toISOString();
239
+ const durable = defineCodexHelperThreadRecord({
240
+ createdAt: now,
241
+ executionProfile: ledgerOwner.snapshotExecutionProfile(executionProfile),
242
+ identity: await codexAppServerHelperThreadIdentity({ executionProfile, provider }),
243
+ lifecycle,
244
+ ownershipId: crypto.randomUUID(),
245
+ projectContextRoot: normalizeText(projectContextRoot),
246
+ projectRuntimeRoot: normalizeText(projectRuntimeRoot),
247
+ revision: 1,
248
+ schemaVersion: CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
249
+ sessionId: normalizeText(sessionId),
250
+ threadId: normalizeText(threadId),
251
+ turnId: normalizeText(turnId),
252
+ updatedAt: now,
253
+ workdir: normalizeText(workdir)
254
+ });
255
+ const ledger = codexAppServerHelperThreadLedger(projectRuntimeRoot);
256
+ await ledger.write(durable);
257
+ return attachCodexAppServerHelperThread({ durable, ledger, onRetired, provider });
258
+ }
259
+
260
+ async function withCodexAppServerHelperThreadMutation(record = null, operation) {
261
+ const key = codexAppServerHelperThreadKey(record);
262
+ if (!record || typeof operation !== "function") {
263
+ throw codexAppServerHelperThreadUnavailableError(record?.threadId);
264
+ }
265
+ const previous = codexAppServerHelperThreadMutations.get(key) || Promise.resolve();
266
+ const mutation = previous.catch(() => null).then(operation);
267
+ codexAppServerHelperThreadMutations.set(key, mutation);
268
+ try {
269
+ return await mutation;
270
+ } finally {
271
+ if (codexAppServerHelperThreadMutations.get(key) === mutation) {
272
+ codexAppServerHelperThreadMutations.delete(key);
273
+ }
274
+ }
275
+ }
276
+
277
+ async function updateCodexAppServerHelperThreadUnlocked(record = null, {
278
+ lifecycle = record?.lifecycle,
279
+ onRetired = record?.onRetired,
280
+ turnId = record?.turnId
281
+ } = {}) {
282
+ const key = codexAppServerHelperThreadKey(record);
283
+ if (!record || codexAppServerHelperThreads.get(key) !== record) {
284
+ throw codexAppServerHelperThreadUnavailableError(record?.threadId);
285
+ }
286
+ const durable = defineCodexHelperThreadRecord({
287
+ ...record.durable,
288
+ lifecycle,
289
+ revision: record.revision + 1,
290
+ updatedAt: new Date().toISOString(),
291
+ turnId: normalizeText(turnId)
292
+ });
293
+ await record.ledger.write(durable, { expected: record.durable });
294
+ return attachCodexAppServerHelperThread({
295
+ durable,
296
+ ledger: record.ledger,
297
+ onRetired,
298
+ provider: record.provider
299
+ });
300
+ }
301
+
302
+ async function updateCodexAppServerHelperThread(record = null, changes = {}) {
303
+ return withCodexAppServerHelperThreadMutation(record, () => (
304
+ updateCodexAppServerHelperThreadUnlocked(record, changes)
305
+ ));
306
+ }
307
+
308
+ function forgetCodexAppServerHelperThreadInMemory(record = null) {
309
+ const key = codexAppServerHelperThreadKey(record);
310
+ if (!record || codexAppServerHelperThreads.get(key) !== record) {
311
+ return false;
312
+ }
313
+ codexAppServerHelperThreadCleanups.delete(key);
314
+ codexAppServerHelperThreads.delete(key);
315
+ record.onRetired?.({ threadId: record.threadId });
316
+ return true;
317
+ }
318
+
319
+ async function removeCodexAppServerHelperThreadUnlocked(record = null) {
320
+ const key = codexAppServerHelperThreadKey(record);
321
+ if (!record || codexAppServerHelperThreads.get(key) !== record) {
322
+ throw codexAppServerHelperThreadUnavailableError(record?.threadId);
323
+ }
324
+ await record.ledger.remove(record.durable);
325
+ forgetCodexAppServerHelperThreadInMemory(record);
326
+ return true;
327
+ }
328
+
329
+ async function removeCodexAppServerHelperThread(record = null) {
330
+ return withCodexAppServerHelperThreadMutation(record, () => (
331
+ removeCodexAppServerHelperThreadUnlocked(record)
332
+ ));
333
+ }
334
+
335
+ function codexAppServerHelperThreadRecords({
336
+ projectContextRoot = "",
337
+ projectRuntimeRoot = "",
338
+ provider = null,
339
+ sessionId = ""
340
+ } = {}) {
341
+ const normalizedProjectContextRoot = normalizeText(projectContextRoot);
342
+ const normalizedProjectRuntimeRoot = normalizeText(projectRuntimeRoot);
343
+ const normalizedSessionId = normalizeText(sessionId);
344
+ return [...codexAppServerHelperThreads.values()].filter((record) => {
345
+ return (!provider || record.provider === provider) &&
346
+ (!normalizedSessionId || record.sessionId === normalizedSessionId) &&
347
+ (!normalizedProjectContextRoot || record.projectContextRoot === normalizedProjectContextRoot) &&
348
+ (!normalizedProjectRuntimeRoot || record.projectRuntimeRoot === normalizedProjectRuntimeRoot);
349
+ });
350
+ }
351
+
352
+ function codexAppServerHelperThreadCleanupError(record = {}, error = null, interruptError = null) {
353
+ const failure = new Error(
354
+ `${applicationName} could not retire a low-cost assistant thread. Retry cleanup before shutting down its Codex provider.`
355
+ );
356
+ failure.code = `${errorPrefix}codex_helper_thread_cleanup_failed`;
357
+ failure.statusCode = 503;
358
+ failure.retryable = true;
359
+ failure.details = {
360
+ cleanupError: errorMessage(error, "Codex helper thread deletion failed."),
361
+ ...(interruptError
362
+ ? { interruptError: errorMessage(interruptError, "Codex helper turn interruption failed.") }
363
+ : {}),
364
+ retryable: true,
365
+ sessionId: normalizeText(record.sessionId),
366
+ threadId: normalizeText(record.threadId),
367
+ turnId: normalizeText(record.turnId)
368
+ };
369
+ return failure;
370
+ }
371
+
372
+ async function retireCodexAppServerHelperThread(record = null) {
373
+ const key = codexAppServerHelperThreadKey(record);
374
+ const pendingTurnStart = codexAppServerHelperTurnStarts.get(key);
375
+ if (pendingTurnStart) {
376
+ await pendingTurnStart.catch(() => null);
377
+ }
378
+ const current = codexAppServerHelperThreads.get(key);
379
+ if (!record || !current) {
380
+ return {
381
+ deleted: false,
382
+ ok: true,
383
+ status: "notOwned",
384
+ threadId: normalizeText(record?.threadId)
385
+ };
386
+ }
387
+ const pending = codexAppServerHelperThreadCleanups.get(key);
388
+ if (pending) {
389
+ return pending;
390
+ }
391
+ const cleanup = withCodexAppServerHelperThreadMutation(record, async () => {
392
+ let cleanupRecord = codexAppServerHelperThreads.get(key);
393
+ if (!cleanupRecord || cleanupRecord.ownershipId !== record.ownershipId) {
394
+ return {
395
+ deleted: false,
396
+ ok: true,
397
+ status: "notOwned",
398
+ threadId: normalizeText(record.threadId)
399
+ };
400
+ }
401
+ if (cleanupRecord.lifecycle !== CODEX_HELPER_THREAD_LIFECYCLES.CLEANUP_REQUIRED) {
402
+ try {
403
+ cleanupRecord = await updateCodexAppServerHelperThreadUnlocked(cleanupRecord, {
404
+ lifecycle: CODEX_HELPER_THREAD_LIFECYCLES.CLEANUP_REQUIRED
405
+ });
406
+ } catch (error) {
407
+ throw codexAppServerHelperThreadCleanupError(cleanupRecord, error);
408
+ }
409
+ }
410
+ if (!await directoryExists(cleanupRecord.identity.runtime.runtimeDir)) {
411
+ await removeCodexAppServerHelperThreadUnlocked(cleanupRecord);
412
+ return {
413
+ deleted: false,
414
+ ok: true,
415
+ status: "runtimeAbsent",
416
+ threadId: cleanupRecord.threadId,
417
+ turnId: cleanupRecord.turnId
418
+ };
419
+ }
420
+ const { error, interruptError, ...result } = await deleteCodexAppServerHelperThread({
421
+ provider: cleanupRecord.provider,
422
+ threadId: cleanupRecord.threadId,
423
+ turnId: cleanupRecord.turnId
424
+ });
425
+ if (result.ok === false) {
426
+ throw codexAppServerHelperThreadCleanupError(cleanupRecord, error, interruptError);
427
+ }
428
+ try {
429
+ await removeCodexAppServerHelperThreadUnlocked(cleanupRecord);
430
+ } catch (ledgerError) {
431
+ throw codexAppServerHelperThreadCleanupError(cleanupRecord, ledgerError, interruptError);
432
+ }
433
+ return result;
434
+ });
435
+ codexAppServerHelperThreadCleanups.set(key, cleanup);
436
+ try {
437
+ return await cleanup;
438
+ } finally {
439
+ if (codexAppServerHelperThreadCleanups.get(key) === cleanup) {
440
+ codexAppServerHelperThreadCleanups.delete(key);
441
+ }
442
+ }
443
+ }
444
+
445
+ async function retireCodexAppServerHelperThreads(filters = {}) {
446
+ const records = codexAppServerHelperThreadRecords(filters);
447
+ const results = [];
448
+ const failed = [];
449
+ for (const record of records) {
450
+ try {
451
+ results.push(await retireCodexAppServerHelperThread(record));
452
+ } catch (error) {
453
+ failed.push({
454
+ code: normalizeText(error?.code),
455
+ error: errorMessage(error),
456
+ retryable: error?.retryable === true,
457
+ sessionId: record.sessionId,
458
+ threadId: record.threadId,
459
+ turnId: record.turnId
460
+ });
461
+ }
462
+ }
463
+ return {
464
+ failed,
465
+ ok: failed.length === 0,
466
+ owned: records.length,
467
+ results
468
+ };
469
+ }
470
+
471
+ function assertCodexAppServerHelperThreadsRetired(result = {}) {
472
+ if (result.ok !== false) {
473
+ return result;
474
+ }
475
+ const failure = new Error(
476
+ `${applicationName} could not retire every low-cost assistant thread. Retry cleanup before shutting down Codex.`
477
+ );
478
+ failure.code = `${errorPrefix}codex_helper_thread_cleanup_failed`;
479
+ failure.statusCode = 503;
480
+ failure.retryable = true;
481
+ failure.details = {
482
+ failed: result.failed,
483
+ retryable: true
484
+ };
485
+ throw failure;
486
+ }
487
+
488
+ const codexAppServerHelperOwnershipError = createCodexHelperOwnershipError({ applicationName, errorPrefix });
489
+
490
+ function codexAppServerRuntimeIdentityMatches(expected = {}, actual = {}, {
491
+ requireAccount = true,
492
+ requireEndpoint = true
493
+ } = {}) {
494
+ return (!requireAccount || expected.accountIdentitySignature === normalizeText(actual.accountIdentitySignature)) &&
495
+ (!requireEndpoint || expected.endpoint === normalizeText(actual.endpoint)) &&
496
+ expected.executionMode === normalizeText(actual.executionMode) &&
497
+ expected.executionContextHash === normalizeText(actual.executionContextHash) &&
498
+ expected.provider === normalizeText(actual.provider) &&
499
+ expected.runtimeDir === normalizeText(actual.runtimeDir) &&
500
+ expected.runtimesHash === normalizeText(actual.runtimesHash) &&
501
+ expected.terminalEnvHash === normalizeText(actual.terminalEnvHash) &&
502
+ expected.toolHomeSource === normalizeText(actual.toolHomeSource) &&
503
+ expected.transport === normalizeText(actual.transport);
504
+ }
505
+
506
+ function codexAppServerHelperThreadOwnershipMatches(record = {}, {
507
+ providerKey = "",
508
+ runtime = {},
509
+ server = null
510
+ } = {}, options = {}) {
511
+ return record.identity?.providerId === normalizeText(record.executionProfile?.providerId) &&
512
+ record.identity?.providerKeyFingerprint === codexAppServerProviderKeyFingerprint(providerKey) &&
513
+ record.identity?.transportId === CODEX_APP_SERVER_PROVIDER_ID &&
514
+ codexAppServerRuntimeIdentityMatches(record.identity?.runtime, runtime, options) &&
515
+ (
516
+ options.requireServer !== true ||
517
+ record.identity?.server?.userAgent === normalizeText(server?.userAgent)
518
+ );
519
+ }
520
+
521
+ function codexAppServerHelperFailure(record = null, error = null) {
522
+ return {
523
+ code: normalizeText(error?.code) || `${errorPrefix}codex_helper_ownership_blocked`,
524
+ error: errorMessage(error),
525
+ projectRuntimeRoot: normalizeText(record?.projectRuntimeRoot),
526
+ retryable: error?.retryable !== false,
527
+ sessionId: normalizeText(record?.sessionId),
528
+ threadId: normalizeText(record?.threadId)
529
+ };
530
+ }
531
+
532
+ async function retireProvider(provider) {
533
+ assertCodexAppServerHelperThreadsRetired(await retireCodexAppServerHelperThreads({ provider }));
534
+ }
535
+ async function reconcileProviderRetirement({ provider, retired, stopped }) {
536
+ let pendingThreadCleanup = [];
537
+ if (
538
+ retired.status === "rejected" &&
539
+ stopped.status === "fulfilled" &&
540
+ stopped.value?.runtimeDirRemoved === true
541
+ ) {
542
+ // Stopping the isolated runtime can win the race with thread/delete.
543
+ // Reconcile its now-absent storage without reconnecting to the old account.
544
+ [retired] = await Promise.allSettled([
545
+ retireCodexAppServerHelperThreads({ provider }).then(assertCodexAppServerHelperThreadsRetired)
546
+ ]);
547
+ }
548
+ if (
549
+ retired.status === "rejected" &&
550
+ stopped.status === "fulfilled" &&
551
+ stopped.value?.processExitVerified === true &&
552
+ stopped.value?.runtimeDirPreserved === true
553
+ ) {
554
+ const pending = codexAppServerHelperThreadRecords({ provider });
555
+ if (pending.every((thread) => thread.lifecycle === CODEX_HELPER_THREAD_LIFECYCLES.CLEANUP_REQUIRED)) {
556
+ // Shared history survives process shutdown. Keep its durable cleanup
557
+ // records for the next connection, without retaining the stopped client
558
+ // as an account-transition blocker or claiming its history was deleted.
559
+ pendingThreadCleanup = retired.reason.details?.failed || [];
560
+ for (const thread of pending) {
561
+ codexAppServerHelperThreads.delete(codexAppServerHelperThreadKey(thread));
562
+ }
563
+ retired = { status: "fulfilled" };
564
+ }
565
+ }
566
+ return { retired, pendingThreadCleanup };
567
+ }
568
+ function assertProviderReleased(provider) {
569
+ if (codexAppServerHelperThreadRecords({ provider }).length > 0) {
570
+ throw new Error(
571
+ "Codex provider cannot close while it still owns low-cost assistant threads."
572
+ );
573
+ }
574
+ }
575
+
576
+ function assertCodexAppServerHelperThreadsRestored(result = {}) {
577
+ if (result.ok !== false) {
578
+ return result;
579
+ }
580
+ throw codexAppServerHelperOwnershipError(
581
+ `${applicationName} could not reconcile persisted low-cost assistant thread ownership.`,
582
+ {
583
+ failed: result.failed,
584
+ retryable: result.failed?.some((failure) => failure.retryable !== false) !== false,
585
+ projectRuntimeRoot: normalizeText(result.projectRuntimeRoot)
586
+ }
587
+ );
588
+ }
589
+
590
+ async function assertCodexAppServerOrdinaryThreadAvailable(projectRuntimeRoot, threadId) {
591
+ const ledger = codexAppServerHelperThreadLedger(projectRuntimeRoot);
592
+ const { records } = await ledger.readAll();
593
+ if (records.some((record) => record.threadId === threadId)) {
594
+ throw codexAppServerHelperThreadUnavailableError(threadId);
595
+ }
596
+ }
597
+
598
+ const lifecycle = {
599
+ threads: codexAppServerHelperThreads,
600
+ projectOperations: codexAppServerHelperProjectOperations,
601
+ cleanups: codexAppServerHelperThreadCleanups,
602
+ mutations: codexAppServerHelperThreadMutations,
603
+ turnStarts: codexAppServerHelperTurnStarts,
604
+ restores: codexAppServerHelperThreadRestores,
605
+ key: codexAppServerHelperThreadKey,
606
+ withProjectOperation: withCodexAppServerHelperProjectOperation,
607
+ unavailableError: codexAppServerHelperThreadUnavailableError,
608
+ threadForOperation: codexAppServerHelperThreadForOperation,
609
+ inspectControl: inspectCodexAppServerHelperControl,
610
+ ledger: codexAppServerHelperThreadLedger,
611
+ providerFingerprint: codexAppServerProviderKeyFingerprint,
612
+ attach: attachCodexAppServerHelperThread,
613
+ remember: rememberCodexAppServerHelperThread,
614
+ update: updateCodexAppServerHelperThread,
615
+ remove: removeCodexAppServerHelperThread,
616
+ records: codexAppServerHelperThreadRecords,
617
+ cleanupError: codexAppServerHelperThreadCleanupError,
618
+ retire: retireCodexAppServerHelperThread,
619
+ retireAll: retireCodexAppServerHelperThreads,
620
+ assertRetired: assertCodexAppServerHelperThreadsRetired,
621
+ assertRestored: assertCodexAppServerHelperThreadsRestored,
622
+ assertOrdinaryThreadAvailable: assertCodexAppServerOrdinaryThreadAvailable,
623
+ ownershipError: codexAppServerHelperOwnershipError,
624
+ ownershipMatches: codexAppServerHelperThreadOwnershipMatches,
625
+ failure: codexAppServerHelperFailure
626
+ };
627
+ Object.assign(lifecycle, createCodexHelperThreadRecovery(lifecycle, {
628
+ providerOwner, applicationName, errorPrefix
629
+ }), {
630
+ prepareDetached: (scope, preparation) => prepareDetachedCodexHelperThread(
631
+ lifecycle, scope, preparation, { applicationName }
632
+ ),
633
+ retireProvider,
634
+ reconcileProviderRetirement,
635
+ assertProviderReleased
636
+ });
637
+ return Object.freeze(lifecycle);
638
+ }