@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,1058 @@
1
+ import path from "node:path";
2
+ import { stat } from "node:fs/promises";
3
+ import { CodexAppServerAgentProvider } from "./codexProvider.js";
4
+ import { codexAppServerProjectHookTrustConfig } from "./codexConfiguration.js";
5
+
6
+ // Moved from Vibe64's codexTerminal provider owner. Caller-local provider keys
7
+ // are grouped without duplicating the inventory. Recovery and sharing always
8
+ // inspect the complete inventory by native runtime identity.
9
+ const codexAppServerProviders = new Map();
10
+ const codexAppServerOwnedRuntimes = new Map();
11
+ let codexAppServerProviderLifecycle = Promise.resolve();
12
+ const normalizeText = value => String(value || "").trim();
13
+
14
+ export function codexAppServerOwnedRuntimeKey(providerKey = "", providerOptions = {}) {
15
+ const runtimeDir = normalizeText(providerOptions?.runtimeDir);
16
+ return runtimeDir
17
+ ? `runtime:${path.resolve(runtimeDir)}`
18
+ : `provider:${normalizeText(providerKey)}`;
19
+ }
20
+
21
+ export function codexAppServerRuntimeStopWasVerified(result = {}) {
22
+ return result?.stopped === true ||
23
+ result?.processExitVerified === true ||
24
+ result?.runtimeDirRemoved === true;
25
+ }
26
+
27
+ async function directoryExists(filePath = "") {
28
+ try {
29
+ return (await stat(filePath)).isDirectory();
30
+ } catch (error) {
31
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
32
+ return false;
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+
38
+ function* providerRecords() {
39
+ for (const records of codexAppServerProviders.values()) yield* records.values();
40
+ }
41
+
42
+ /** Internal consumer view of the one native provider owner, not a server per view. */
43
+ export function createCodexAppServerProviderOwner({
44
+ runtimeRoot = "",
45
+ assertOpen: assertApplicationOpen = () => {},
46
+ onRecoveryEvent = () => {},
47
+ debugLog = () => {},
48
+ debugError = error => error,
49
+ providerFactory = null,
50
+ prepareNativeHost = () => ({}),
51
+ runtimeBusyCode = "codex_app_server_runtime_busy",
52
+ exitUnverifiedCode = "codex_runtime_exit_unverified",
53
+ requiredStopCode = "codex_runtime_exit_unverified",
54
+ runtimeCloseError = "Codex app-server runtime close failed."
55
+ } = {}) {
56
+ const scopeRoot = normalizeText(runtimeRoot) ? path.resolve(runtimeRoot) : "";
57
+ const records = new Map();
58
+ const lifecycleTasks = new Set();
59
+ const runtimeAcquisitions = new Set();
60
+ let closing = false;
61
+ let runtimeLifecycle = null;
62
+ let codexAppServerChatModelCatalog = null;
63
+
64
+ const providers = {
65
+ get: key => records.get(key)?.provider,
66
+ keys: () => records.keys(),
67
+ *entries() {
68
+ for (const [key, record] of records) yield [key, record.provider];
69
+ },
70
+ [Symbol.iterator]() { return this.entries(); }
71
+ };
72
+ const providerOwners = { get: key => records.get(key)?.owner };
73
+ const ownedRuntimes = {
74
+ get: key => codexAppServerOwnedRuntimes.get(key),
75
+ *values() {
76
+ for (const record of codexAppServerOwnedRuntimes.values()) {
77
+ if (inScope(record)) yield record;
78
+ }
79
+ }
80
+ };
81
+
82
+ function attachRuntimeLifecycle(lifecycle) {
83
+ if (runtimeLifecycle && runtimeLifecycle !== lifecycle) {
84
+ throw new TypeError("Codex provider owner already has a runtime lifecycle.");
85
+ }
86
+ runtimeLifecycle = lifecycle;
87
+ }
88
+
89
+ function assertOpen() {
90
+ if (runtimeLifecycle) return runtimeLifecycle.assertOpen();
91
+ return assertApplicationOpen();
92
+ }
93
+
94
+ function inScope(record) {
95
+ if (record.scope === api) return true;
96
+ const runtimeDir = normalizeText(record.providerOptions?.runtimeDir);
97
+ if (!scopeRoot || !runtimeDir) return false;
98
+ const relative = path.relative(scopeRoot, path.resolve(runtimeDir));
99
+ return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
100
+ }
101
+
102
+ function withLifecycle(operation) {
103
+ const run = codexAppServerProviderLifecycle.catch(() => null).then(operation);
104
+ const tracked = run.catch(() => null).finally(() => {
105
+ lifecycleTasks.delete(tracked);
106
+ });
107
+ codexAppServerProviderLifecycle = tracked;
108
+ lifecycleTasks.add(tracked);
109
+ return run;
110
+ }
111
+
112
+ function runtimeIsShared(providerKey = "", providerOptions = {}) {
113
+ const runtimeKey = codexAppServerOwnedRuntimeKey(providerKey, providerOptions);
114
+ return [...providerRecords()].some(record =>
115
+ !(record.scope === api && record.providerKey === providerKey) &&
116
+ codexAppServerOwnedRuntimeKey(record.providerKey, record.providerOptions) === runtimeKey);
117
+ }
118
+
119
+ function rememberOwnedRuntime({ provider = null, providerKey = "", providerOptions = {} } = {}) {
120
+ if (!path.isAbsolute(providerOptions.runtimeDir || "")) throw new TypeError("Codex ownership requires an absolute native runtime directory.");
121
+ const runtimeKey = codexAppServerOwnedRuntimeKey(providerKey, providerOptions);
122
+ const consumer = records.get(providerKey);
123
+ const record = {
124
+ provider,
125
+ providerKey: normalizeText(providerKey),
126
+ providerOptions,
127
+ sessionKey: consumer?.owner.sessionKey || "",
128
+ runtimeDir: normalizeText(providerOptions?.runtimeDir),
129
+ runtimeKey,
130
+ scope: api,
131
+ resources: consumer?.resources,
132
+ preserveProcessExitProof: consumer?.preserveProcessExitProof === true
133
+ };
134
+ codexAppServerOwnedRuntimes.set(runtimeKey, record);
135
+ return record;
136
+ }
137
+
138
+ function forgetOwnedRuntime(provider = null) {
139
+ for (const [runtimeKey, record] of codexAppServerOwnedRuntimes.entries()) {
140
+ if (record.provider === provider) codexAppServerOwnedRuntimes.delete(runtimeKey);
141
+ }
142
+ }
143
+
144
+ async function acquireRuntime({ operation, provider = null, providerKey = "", providerOptions = {} } = {}) {
145
+ assertOpen();
146
+ rememberOwnedRuntime({ provider, providerKey, providerOptions });
147
+ const acquisition = Promise.resolve().then(operation);
148
+ runtimeAcquisitions.add(acquisition);
149
+ try {
150
+ const result = await acquisition;
151
+ assertOpen();
152
+ return result;
153
+ } finally {
154
+ runtimeAcquisitions.delete(acquisition);
155
+ }
156
+ }
157
+
158
+ function createProvider({ providerKey, providerOptions = {}, owner = {}, create, observation, resources, preserveProcessExitProof = false } = {}) {
159
+ assertOpen();
160
+ if (!path.isAbsolute(providerOptions.runtimeDir || "")) throw new TypeError("Codex ownership requires an absolute native runtime directory.");
161
+ const existing = records.get(providerKey);
162
+ if (existing) return existing.provider;
163
+ const provider = create(error => recover(providerKey, error));
164
+ records.set(providerKey, {
165
+ provider, providerKey, providerOptions,
166
+ owner: { ...owner, providerOptions },
167
+ observation, resources, preserveProcessExitProof, scope: api
168
+ });
169
+ codexAppServerProviders.set(api, records);
170
+ return provider;
171
+ }
172
+
173
+ async function retireAndCloseProvider(providerKey = "", options = {}) {
174
+ const record = records.get(normalizeText(providerKey));
175
+ if (!record) return;
176
+ await record.resources?.retire?.(record.provider);
177
+ closeProvider(normalizeText(providerKey), options);
178
+ }
179
+
180
+ function createNativeProvider(parameters) {
181
+ return providerFactory
182
+ ? providerFactory(parameters)
183
+ : new CodexAppServerAgentProvider(parameters, prepareNativeHost(parameters));
184
+ }
185
+
186
+ async function providerForSession(context) {
187
+ assertOpen();
188
+ context.assertAdmission();
189
+ const providerKey = context.providerKey;
190
+ const existing = providers.get(providerKey);
191
+ if (existing) return existing;
192
+ const identity = context.keyFields(providerKey);
193
+ for (const currentKey of [...providers.keys()]) {
194
+ const current = context.keyFields(currentKey);
195
+ if (current.sessionId === identity.sessionId &&
196
+ current.executionMode === identity.executionMode &&
197
+ current.executionRoot === identity.executionRoot &&
198
+ current.runtimeInstanceId === identity.runtimeInstanceId &&
199
+ current.workdir === identity.workdir) {
200
+ await retireAndCloseProvider(currentKey);
201
+ }
202
+ }
203
+ context.assertAdmission();
204
+ assertOpen();
205
+ const prepared = await context.prepare(providerKey, identity);
206
+ let unsubscribeControls = null;
207
+ const provider = createProvider({
208
+ providerKey,
209
+ providerOptions: context.providerOptions,
210
+ preserveProcessExitProof: prepared.preserveProcessExitProof,
211
+ owner: prepared.owner,
212
+ observation: prepared.observation,
213
+ resources: prepared.resources,
214
+ create: onObservationLost => {
215
+ const parameters = {
216
+ ...prepared.parameters,
217
+ onClose() { unsubscribeControls?.(); },
218
+ beforeResumeThread(threadId) {
219
+ return prepared.runInContext(async () => {
220
+ const guard = await prepared.prepareResumeGuard();
221
+ if (!guard) return;
222
+ context.runOwner.assertThreadCanResume(guard.run, threadId);
223
+ });
224
+ },
225
+ prepareThreadResumeParams(threadId, params) {
226
+ return prepared.runInContext(async () => {
227
+ const resume = await prepared.prepareResume(threadId, params);
228
+ if (!resume) return params;
229
+ const config = await codexAppServerProjectHookTrustConfig(provider, resume.workdir);
230
+ const settings = resume.settings;
231
+ return {
232
+ ...settings,
233
+ ...params,
234
+ config: { ...settings.config, ...config, ...params.config }
235
+ };
236
+ });
237
+ },
238
+ onObservationLost
239
+ };
240
+ return createNativeProvider(parameters);
241
+ }
242
+ });
243
+ unsubscribeControls = prepared.controls.subscribe(event => {
244
+ if (!prepared.controls.available) return;
245
+ for (const [threadId, binding] of provider.threadEnvironments || []) {
246
+ const boundEnv = binding.params.config?.shell_environment_policy?.set || {};
247
+ if (!Object.entries(boundEnv).some(([key, value]) => key.endsWith("_COMMAND_SOCKET") && value === event.socketPath)) continue;
248
+ // Keep listener replacement and rejected command recovery on the
249
+ // provider's original per-thread operation.
250
+ if (provider.threadEnvironmentTasks?.has(threadId)) continue;
251
+ void prepared.runInContext(() => provider.ensureThreadControls(threadId))
252
+ .catch(error => provider.failObservation(error))
253
+ .catch(() => null);
254
+ }
255
+ });
256
+ return provider;
257
+ }
258
+
259
+ async function ensureSession(context, mainThreadId = "") {
260
+ const sessionId = context.sessionId;
261
+ const providerOptions = context.providerOptions;
262
+ let provider = await withLifecycle(() => providerForSession(context));
263
+ if (provider.observationFailure) {
264
+ await provider.failObservation(provider.observationFailure);
265
+ provider = await withLifecycle(() => providerForSession(context));
266
+ }
267
+ const providerKey = context.providerKey;
268
+ if (mainThreadId) {
269
+ const fields = context.keyFields(providerKey);
270
+ context.runOwner.runtimeLifecycle.rememberManagedSession(providerKey, {
271
+ providerOptions,
272
+ sessionId,
273
+ threadId: mainThreadId,
274
+ executionRoot: fields.executionRoot,
275
+ workdir: fields.workdir
276
+ });
277
+ }
278
+ try {
279
+ context.assertAdmission();
280
+ await acquireRuntime({
281
+ operation: () => {
282
+ if (typeof provider.ensureAvailable === "function") {
283
+ return provider.ensureAvailable({ modelProviderId: providerOptions.routingModelProviderId });
284
+ }
285
+ if (typeof provider.listLoadedThreads === "function") {
286
+ return provider.listLoadedThreads({ limit: 1 });
287
+ }
288
+ return provider.ensureRuntime?.();
289
+ },
290
+ provider,
291
+ providerKey,
292
+ providerOptions
293
+ });
294
+ return provider;
295
+ } catch (error) {
296
+ if (error?.code === runtimeBusyCode) {
297
+ const managedThreadId = mainThreadId || context.runOwner.runtimeLifecycle.managedThreadId(providerKey);
298
+ if (!context.closing && managedThreadId) context.runOwner.scheduleActiveRecovery(sessionId);
299
+ throw error;
300
+ }
301
+ if (!context.closing && mainThreadId) await provider.failObservation(error);
302
+ if (!context.closing && !provider.observationFailure) {
303
+ await stopProvider(context.providerKey, providerOptions, {
304
+ get runtimeHost() { return context.runtimeHost; }
305
+ });
306
+ }
307
+ throw error;
308
+ }
309
+ }
310
+
311
+ function closeProvider(providerKey = "", { closeProvider = true } = {}) {
312
+ const record = records.get(providerKey);
313
+ if (!record) return;
314
+ record.resources?.release?.(record.provider);
315
+ if (closeProvider) record.provider.close?.();
316
+ records.delete(providerKey);
317
+ if (!records.size) codexAppServerProviders.delete(api);
318
+ }
319
+
320
+ async function stopOwnedRuntime(record = {}, {
321
+ cached = false,
322
+ preserveProcessExitProof = false,
323
+ requireVerifiedExit = true
324
+ } = {}) {
325
+ const provider = record.provider;
326
+ const providerKey = normalizeText(record.providerKey);
327
+ const resources = record.resources || records.get(providerKey)?.resources;
328
+ const retirement = Promise.resolve().then(() => resources?.retire?.(provider));
329
+ const runtimeStop = Promise.resolve().then(async () => {
330
+ if (typeof provider?.stopRuntime !== "function") {
331
+ throw new Error("Codex app-server provider must implement stopRuntime().");
332
+ }
333
+ const result = await provider.stopRuntime({ preserveProcessExitProof });
334
+ if (requireVerifiedExit && !codexAppServerRuntimeStopWasVerified(result)) {
335
+ const error = new Error("Codex app-server process exit could not be verified.");
336
+ error.code = exitUnverifiedCode;
337
+ error.providerKey = providerKey;
338
+ error.retryable = true;
339
+ throw error;
340
+ }
341
+ return result;
342
+ });
343
+ let [retired, stopped] = await Promise.allSettled([retirement, runtimeStop]);
344
+ let pendingThreadCleanup = [];
345
+ if (resources?.reconcileRetirement) {
346
+ ({ retired, pendingThreadCleanup } = await resources.reconcileRetirement({ provider, retired, stopped }));
347
+ }
348
+ if (stopped.status === "fulfilled" && codexAppServerRuntimeStopWasVerified(stopped.value)) {
349
+ forgetOwnedRuntime(provider);
350
+ } else {
351
+ provider?.close?.();
352
+ }
353
+ if (retired.status === "fulfilled" && cached) {
354
+ (record.scope || api).closeProvider(providerKey, { closeProvider: stopped.status !== "fulfilled" });
355
+ }
356
+ const failures = [retired, stopped]
357
+ .filter(result => result.status === "rejected")
358
+ .map(result => result.reason);
359
+ if (failures.length === 1) throw failures[0];
360
+ if (failures.length > 0) {
361
+ throw new AggregateError(failures, `Codex app-server shutdown failed for provider "${providerKey}".`);
362
+ }
363
+ return {
364
+ ...stopped.value, pendingThreadCleanup, providerKey,
365
+ stopped: codexAppServerRuntimeStopWasVerified(stopped.value)
366
+ };
367
+ }
368
+
369
+ async function stopCachedProviderUnlocked(providerKey = "", {
370
+ preserveProcessExitProof = false,
371
+ requireStopped = false
372
+ } = {}) {
373
+ const normalizedProviderKey = normalizeText(providerKey);
374
+ const provider = providers.get(normalizedProviderKey);
375
+ if (!provider) {
376
+ closeProvider(normalizedProviderKey);
377
+ return {
378
+ providerKey: normalizedProviderKey,
379
+ stopped: false
380
+ };
381
+ }
382
+ await records.get(normalizedProviderKey)?.resources?.retire?.(provider);
383
+ const sharedProcessRetained = runtimeIsShared(
384
+ normalizedProviderKey, providerOwners.get(normalizedProviderKey)?.providerOptions
385
+ );
386
+ if (sharedProcessRetained) {
387
+ closeProvider(normalizedProviderKey);
388
+ return {
389
+ providerKey: normalizedProviderKey,
390
+ sessionDetached: true,
391
+ sharedProcessRetained: true,
392
+ stopped: false
393
+ };
394
+ }
395
+ if (typeof provider.stopRuntime !== "function") {
396
+ closeProvider(normalizedProviderKey);
397
+ throw new Error("Codex app-server provider must implement stopRuntime().");
398
+ }
399
+ let providerStoppedRuntime = false;
400
+ try {
401
+ const stoppedRuntime = await provider.stopRuntime({
402
+ preserveProcessExitProof: preserveProcessExitProof || requireStopped
403
+ });
404
+ providerStoppedRuntime = codexAppServerRuntimeStopWasVerified(stoppedRuntime);
405
+ if (requireStopped && !providerStoppedRuntime) {
406
+ const error = new Error("Codex app-server process exit could not be verified.");
407
+ error.code = requiredStopCode;
408
+ error.retryable = true;
409
+ throw error;
410
+ }
411
+ if (providerStoppedRuntime) {
412
+ forgetOwnedRuntime(provider);
413
+ }
414
+ return {
415
+ ...(stoppedRuntime && typeof stoppedRuntime === "object" ? stoppedRuntime : {}),
416
+ providerKey: normalizedProviderKey,
417
+ stopped: providerStoppedRuntime
418
+ };
419
+ } finally {
420
+ closeProvider(normalizedProviderKey, {
421
+ closeProvider: !providerStoppedRuntime
422
+ });
423
+ }
424
+ }
425
+
426
+ function stopCachedProvider(providerKey = "", options = {}) {
427
+ return withLifecycle(
428
+ () => stopCachedProviderUnlocked(providerKey, options)
429
+ );
430
+ }
431
+
432
+ function runtimeExitUnverifiedError(providerKey = "") {
433
+ const error = new Error("Codex app-server process exit could not be verified.");
434
+ error.code = exitUnverifiedCode;
435
+ error.providerKey = normalizeText(providerKey);
436
+ error.retryable = true;
437
+ return error;
438
+ }
439
+
440
+ async function stopPreparedRuntime(context) {
441
+ const { runtime, parameters } = context.runtimeHost;
442
+ return runtime.stop(parameters);
443
+ }
444
+
445
+ async function stopProvider(providerKey, providerOptions, context = {}) {
446
+ const { preserveProcessExitProof = false, requireStopped = false } = context;
447
+ const provider = providers.get(providerKey);
448
+ if (!provider) {
449
+ if (runtimeIsShared(providerKey, providerOptions)) {
450
+ return {
451
+ providerKey,
452
+ sessionDetached: true,
453
+ sharedProcessRetained: true,
454
+ stopped: false
455
+ };
456
+ }
457
+ const stoppedRuntime = await stopPreparedRuntime(context);
458
+ const stopped = codexAppServerRuntimeStopWasVerified(stoppedRuntime);
459
+ if (requireStopped && !stopped) {
460
+ throw runtimeExitUnverifiedError(providerKey);
461
+ }
462
+ return {
463
+ ...(Boolean(stoppedRuntime && typeof stoppedRuntime === "object" && !Array.isArray(stoppedRuntime)) ? stoppedRuntime : {}),
464
+ providerKey,
465
+ stopped
466
+ };
467
+ }
468
+ return stopCachedProvider(providerKey, {
469
+ preserveProcessExitProof,
470
+ requireStopped
471
+ });
472
+ }
473
+
474
+ async function stopThreadBeforeRelease(providerKey, provider, threadId) {
475
+ try {
476
+ await provider.stopThreadForObservationLoss(threadId, "");
477
+ } catch (error) {
478
+ // If other sessions retain the shared process, closing this
479
+ // socket alone is not proof that its native goal/work stopped.
480
+ const shared = runtimeIsShared(
481
+ providerKey, providerOwners.get(providerKey)?.providerOptions
482
+ );
483
+ if (shared) throw error;
484
+ // The caller's existing shutdown must instead prove process exit.
485
+ }
486
+ }
487
+
488
+ function readModelCatalog({
489
+ prepareProviderOptions,
490
+ providerFactory,
491
+ resources = null,
492
+ signal = null,
493
+ cacheMs = 30_000
494
+ } = {}) {
495
+ return withLifecycle(async () => {
496
+ assertOpen();
497
+ signal?.throwIfAborted();
498
+ const providerOptions = await prepareProviderOptions();
499
+ const existing = codexAppServerOwnedRuntimes.get(
500
+ codexAppServerOwnedRuntimeKey("", providerOptions)
501
+ );
502
+ const provider = existing?.provider || providerFactory(providerOptions);
503
+ let runtime = null;
504
+ let catalog = null;
505
+ let identity = "";
506
+ try {
507
+ identity = JSON.stringify(await provider.currentRuntimeInfo());
508
+ signal?.throwIfAborted();
509
+ if (codexAppServerChatModelCatalog?.identity === identity &&
510
+ codexAppServerChatModelCatalog.expiresAt > Date.now()) {
511
+ return codexAppServerChatModelCatalog.value;
512
+ }
513
+ if (!existing?.provider) {
514
+ runtime = await acquireRuntime({
515
+ operation: () => provider.ensureRuntime(),
516
+ provider,
517
+ providerOptions
518
+ });
519
+ }
520
+ catalog = await provider.listModels({ includeHidden: false, limit: 100 }, { signal });
521
+ } finally {
522
+ if (!existing?.provider) {
523
+ try {
524
+ if ((runtime || provider.runtime)?.reused === false) {
525
+ await stopOwnedRuntime({ provider, resources });
526
+ } else {
527
+ forgetOwnedRuntime(provider);
528
+ }
529
+ } finally {
530
+ provider.close();
531
+ }
532
+ }
533
+ }
534
+ codexAppServerChatModelCatalog = {
535
+ expiresAt: Date.now() + cacheMs,
536
+ identity,
537
+ value: catalog
538
+ };
539
+ return catalog;
540
+ });
541
+ }
542
+
543
+ async function invalidateRuntimes({
544
+ includeOwned = false,
545
+ requireVerifiedExit = false,
546
+ stopOwnedRuntimes = false,
547
+ toolHomeSource = ""
548
+ } = {}) {
549
+ codexAppServerChatModelCatalog = null;
550
+ const normalizedToolHomeSource = normalizeText(toolHomeSource);
551
+ const ownedRecordsByProvider = new Map(
552
+ [...codexAppServerOwnedRuntimes.values()].filter(inScope).map((record) => [record.provider, record])
553
+ );
554
+ const targets = [...providerRecords()]
555
+ .filter(record => inScope(record) && (!normalizedToolHomeSource ||
556
+ normalizeText(record.providerOptions.toolHomeSource) === normalizedToolHomeSource))
557
+ .map(record => ({
558
+ ...record,
559
+ kind: "cached",
560
+ record: ownedRecordsByProvider.get(record.provider) || null
561
+ }));
562
+ const targetedProviders = new Set(targets.map((target) => target.provider));
563
+ if (includeOwned) {
564
+ for (const record of codexAppServerOwnedRuntimes.values()) {
565
+ if (!inScope(record) || targetedProviders.has(record.provider)) {
566
+ continue;
567
+ }
568
+ const recordToolHomeSource = normalizeText(
569
+ record.providerOptions?.toolHomeSource
570
+ );
571
+ if (
572
+ normalizedToolHomeSource &&
573
+ recordToolHomeSource !== normalizedToolHomeSource
574
+ ) {
575
+ continue;
576
+ }
577
+ targets.push({
578
+ ...record,
579
+ kind: "owned",
580
+ provider: record.provider,
581
+ providerKey: record.providerKey,
582
+ record
583
+ });
584
+ targetedProviders.add(record.provider);
585
+ }
586
+ }
587
+ targets.sort((left, right) => Number(Boolean(left.record)) - Number(Boolean(right.record)));
588
+ const failed = [];
589
+ const results = [];
590
+ for (const target of targets) {
591
+ if (!target.resources?.invalidation) continue;
592
+ const error = Object.assign(new Error("Codex's native runtime was invalidated."), {
593
+ code: "codex_runtime_invalidated"
594
+ });
595
+ target.invalidation = { error };
596
+ try {
597
+ if (await target.resources.invalidation.prepare({ provider: target.provider, error }) === false) {
598
+ delete target.invalidation;
599
+ }
600
+ } catch (preparationError) {
601
+ target.invalidation.preparationError = preparationError;
602
+ }
603
+ }
604
+ for (const target of targets) {
605
+ const preserveProcessExitProof = target.preserveProcessExitProof === true;
606
+ try {
607
+ const result = stopOwnedRuntimes && target.record
608
+ ? await target.scope.stopOwnedRuntime({
609
+ ...(target.record || {}),
610
+ provider: target.provider,
611
+ providerKey: target.providerKey
612
+ }, {
613
+ cached: target.kind === "cached",
614
+ preserveProcessExitProof,
615
+ requireVerifiedExit
616
+ })
617
+ : await target.scope.stopCachedProvider(target.providerKey, {
618
+ preserveProcessExitProof,
619
+ requireStopped: stopOwnedRuntimes && target.record != null
620
+ });
621
+ if (
622
+ requireVerifiedExit &&
623
+ target.record &&
624
+ !codexAppServerRuntimeStopWasVerified(result)
625
+ ) {
626
+ throw Object.assign(new Error("Codex app-server process exit could not be verified."), {
627
+ code: exitUnverifiedCode, providerKey: target.providerKey, retryable: true
628
+ });
629
+ }
630
+ target.stopResult = result;
631
+ results.push(result);
632
+ } catch (error) {
633
+ target.stopError = error;
634
+ failed.push({
635
+ code: normalizeText(error?.code),
636
+ error: String(error?.message || "Codex app-server runtime invalidation failed."),
637
+ providerKey: target.providerKey,
638
+ retryable: error?.retryable === true
639
+ });
640
+ }
641
+ }
642
+ for (const target of targets) {
643
+ if (!target.invalidation) continue;
644
+ const runtimeKey = codexAppServerOwnedRuntimeKey(target.providerKey, target.providerOptions);
645
+ const peers = targets.filter(peer =>
646
+ codexAppServerOwnedRuntimeKey(peer.providerKey, peer.providerOptions) === runtimeKey);
647
+ const proof = peers.find(peer => codexAppServerRuntimeStopWasVerified(peer.stopResult));
648
+ const stopped = proof
649
+ ? { status: "fulfilled", value: proof.stopResult }
650
+ : { status: "rejected", reason: peers.find(peer => peer.stopError)?.stopError ||
651
+ Object.assign(new Error("Codex app-server process exit could not be verified."), {
652
+ code: exitUnverifiedCode, providerKey: target.providerKey, retryable: true
653
+ }) };
654
+ try {
655
+ await target.resources.invalidation.complete({
656
+ provider: target.provider, ...target.invalidation, stopped
657
+ });
658
+ if (target.invalidation.preparationError) throw target.invalidation.preparationError;
659
+ if (stopped.status === "rejected") throw stopped.reason;
660
+ } catch (error) {
661
+ failed.push({
662
+ code: normalizeText(error?.code),
663
+ error: String(error?.message || "Codex app-server invalidation could not be recorded."),
664
+ providerKey: target.providerKey,
665
+ retryable: true
666
+ });
667
+ }
668
+ }
669
+ const stopped = results.filter((result) => result.stopped).length;
670
+ return {
671
+ failed,
672
+ ok: failed.length === 0,
673
+ providerCount: targets.length,
674
+ results,
675
+ stopped
676
+ };
677
+ }
678
+
679
+ async function suspendUnobservedProvider(record, error) {
680
+ const targets = [record];
681
+ let sharedStop = false;
682
+ try {
683
+ const threads = await record.observation.prepare({ provider: record.provider, error });
684
+ if (!threads.size) throw new Error("The unobserved Codex thread could not be identified.");
685
+ for (const [threadId, turnId] of threads) {
686
+ await record.provider.stopThreadForObservationLoss(threadId, turnId);
687
+ }
688
+ } catch {
689
+ sharedStop = true;
690
+ const runtimeKey = codexAppServerOwnedRuntimeKey(record.providerKey, record.providerOptions);
691
+ for (const peer of providerRecords()) {
692
+ if (peer !== record && codexAppServerOwnedRuntimeKey(peer.providerKey, peer.providerOptions) === runtimeKey) {
693
+ peer.provider.observationFailure = error;
694
+ targets.push(peer);
695
+ }
696
+ }
697
+ // Preserve the original ordering and failure policy: attempt every durable
698
+ // barrier before native stop; a storage failure cannot leave work running.
699
+ for (const target of targets) {
700
+ try {
701
+ await target.observation.barrier({ provider: target.provider, error });
702
+ } catch (failure) {
703
+ target.scope.reportRecoveryEvent(target, "persistenceFailed", failure);
704
+ }
705
+ }
706
+ const owned = codexAppServerOwnedRuntimes.get(runtimeKey) || record;
707
+ await owned.scope.stopOwnedRuntime(owned, { preserveProcessExitProof: true, requireVerifiedExit: true });
708
+ }
709
+ for (const target of targets) {
710
+ // Each adapter retains its original persistence/publication order. It can
711
+ // release this provider only after its stopped state is durable.
712
+ await target.observation.complete({ provider: target.provider, error, sharedStop });
713
+ }
714
+ }
715
+
716
+ function reportRecoveryEvent(record, event, error) {
717
+ onRecoveryEvent(record.owner, event, error);
718
+ }
719
+
720
+ function recover(providerKey, error) {
721
+ const record = records.get(providerKey);
722
+ if (!record || closing) return Promise.resolve();
723
+ assertOpen();
724
+ if (!record.observationStop || record.observationStop.error !== error) {
725
+ reportRecoveryEvent(record, "lost", error);
726
+ const stop = { error, promise: suspendUnobservedProvider(record, error) };
727
+ record.observationStop = stop;
728
+ const tracked = stop.promise.catch(failure => {
729
+ reportRecoveryEvent(record, "stopFailed", failure);
730
+ }).finally(() => {
731
+ if (record.observationStop === stop) record.observationStop = null;
732
+ lifecycleTasks.delete(tracked);
733
+ });
734
+ lifecycleTasks.add(tracked);
735
+ }
736
+ return record.observationStop.promise;
737
+ }
738
+
739
+ function pendingRecovery(providerKey, provider) {
740
+ const record = records.get(providerKey);
741
+ return record && record.provider === provider ? record.observationStop?.promise || null : null;
742
+ }
743
+
744
+ async function stopProvidersForProject({
745
+ preserveProcessExitProof = false,
746
+ projectContextRoot = "",
747
+ reason = "",
748
+ } = {}, { managed, keyFields, helperThreads }) {
749
+ const normalizedProjectContextRoot = normalizeText(projectContextRoot);
750
+ if (!normalizedProjectContextRoot) {
751
+ return {
752
+ failed: [],
753
+ ok: true,
754
+ projectContextRoot: normalizedProjectContextRoot,
755
+ providerCount: 0,
756
+ reason: normalizeText(reason),
757
+ results: [],
758
+ stopped: 0
759
+ };
760
+ }
761
+ const helperProviders = new Set(
762
+ helperThreads.records({
763
+ projectContextRoot: normalizedProjectContextRoot
764
+ }).map((record) => record.provider)
765
+ );
766
+ const providerKeys = [...providers.keys()]
767
+ .filter((providerKey) => {
768
+ const current = managed.get(providerKey);
769
+ return normalizeText(current?.projectContext?.targetRoot) === normalizedProjectContextRoot ||
770
+ helperProviders.has(providers.get(providerKey));
771
+ });
772
+ const failed = [];
773
+ const results = [];
774
+ for (const providerKey of providerKeys) {
775
+ try {
776
+ const fields = keyFields(providerKey);
777
+ results.push(await stopCachedProvider(providerKey, {
778
+ preserveProcessExitProof: Boolean(
779
+ preserveProcessExitProof &&
780
+ fields.sessionId &&
781
+ fields.executionMode !== "helper"
782
+ )
783
+ }));
784
+ } catch (error) {
785
+ failed.push({
786
+ code: normalizeText(error?.code),
787
+ error: normalizeText(error?.error || error?.message || error) || runtimeCloseError,
788
+ providerKey,
789
+ retryable: error?.retryable === true
790
+ });
791
+ }
792
+ }
793
+ const stopped = results.filter((result) => result.stopped).length;
794
+ debugLog("appServerRuntime.closeProject.done", {
795
+ failedCount: failed.length,
796
+ providerCount: providerKeys.length,
797
+ projectContextRoot: normalizedProjectContextRoot,
798
+ reason: normalizeText(reason),
799
+ stopped
800
+ });
801
+ return {
802
+ failed,
803
+ ok: failed.length === 0,
804
+ projectContextRoot: normalizedProjectContextRoot,
805
+ providerCount: providerKeys.length,
806
+ reason: normalizeText(reason),
807
+ results,
808
+ stopped
809
+ };
810
+ }
811
+
812
+ async function stopCachedProvidersForSession(sessionKey = "", options = {}) {
813
+ if (!sessionKey) {
814
+ return {
815
+ failed: [],
816
+ ok: true,
817
+ providerCount: 0,
818
+ results: [],
819
+ stopped: 0
820
+ };
821
+ }
822
+ const providerKeys = [...providers.keys()]
823
+ .filter((providerKey) => (
824
+ providerOwners.get(providerKey)?.sessionKey === sessionKey
825
+ ));
826
+ const failed = [];
827
+ const results = [];
828
+ for (const providerKey of providerKeys) {
829
+ try {
830
+ results.push(await stopCachedProvider(providerKey, options));
831
+ } catch (error) {
832
+ failed.push({
833
+ code: normalizeText(error?.code),
834
+ error: normalizeText(error?.error || error?.message || error) || runtimeCloseError,
835
+ providerKey,
836
+ retryable: error?.retryable === true
837
+ });
838
+ }
839
+ }
840
+ return {
841
+ failed,
842
+ ok: failed.length === 0,
843
+ providerCount: providerKeys.length,
844
+ results,
845
+ stopped: results.filter((result) => result.stopped).length
846
+ };
847
+ }
848
+
849
+ function drainThreadEnvironmentTasks(sessionKey) {
850
+ return Promise.all([...providers.entries()]
851
+ .filter(([key]) => providerOwners.get(key)?.sessionKey === sessionKey)
852
+ .flatMap(([, provider]) => [...(provider.threadEnvironmentTasks?.values() || [])])
853
+ .map((task) => task.catch(() => null)));
854
+ }
855
+
856
+ async function stopPersistedRuntime({ runtime, parameters }, runtimeDir) {
857
+ const result = await runtime.stop(parameters);
858
+
859
+ return {
860
+ ...result,
861
+ runtimeDirExists: await directoryExists(runtimeDir),
862
+ verifiedStopped: result?.stopped === true ||
863
+ result?.processExitVerified === true ||
864
+ result?.runtimeDirRemoved === true
865
+ };
866
+ }
867
+
868
+ function sessionExitProof(sessionKey, context) {
869
+ const { cachedProviders, persistedRuntime } = context;
870
+ const noRuntimeAcquired = cachedProviders.providerCount === 0 &&
871
+ !context.hasRuntime &&
872
+ !context.threadId &&
873
+ ![...ownedRuntimes.values()].some((record) => record.sessionKey === sessionKey);
874
+ const cachedRuntimeExitVerified = cachedProviders.providerCount > 0 &&
875
+ cachedProviders.results.length === cachedProviders.providerCount &&
876
+ cachedProviders.results.every((result) => (
877
+ result?.stopped === true ||
878
+ (result?.sessionDetached === true && result?.sharedProcessRetained === true)
879
+ ));
880
+ const persistedRuntimeExitVerified = persistedRuntime?.verifiedStopped === true ||
881
+ (
882
+ persistedRuntime?.sessionDetached === true &&
883
+ persistedRuntime?.sharedProcessRetained === true
884
+ );
885
+ return { noRuntimeAcquired, cachedRuntimeExitVerified, persistedRuntimeExitVerified };
886
+ }
887
+
888
+ async function unsubscribeSessionThread(sessionKey, sessionId, session) {
889
+ if (!sessionId) {
890
+ return { ok: true, sessionId, status: "notSubscribed" };
891
+ }
892
+ const { providerOptions, workdir, threadId } = session.unsubscribeParameters;
893
+ if (!threadId) {
894
+ return { ok: true, providerOptions, sessionId, status: "notSubscribed" };
895
+ }
896
+ // The existing record carries the workdir captured in its provider key.
897
+ // Never prepare new controls while releasing a retained connection.
898
+ const provider = [...records.values()].find(record =>
899
+ record.owner.sessionKey === sessionKey && record.owner.workdir === normalizeText(workdir))?.provider;
900
+ if (!provider || typeof provider.unsubscribeThread !== "function") {
901
+ return { ok: true, providerOptions, sessionId, status: "notSubscribed" };
902
+ }
903
+ const result = await provider.unsubscribeThread(threadId);
904
+ debugLog("appServerThread.unsubscribe.done", {
905
+ sessionId,
906
+ status: normalizeText(result?.status),
907
+ threadId
908
+ });
909
+ return {
910
+ ok: true,
911
+ providerOptions,
912
+ result,
913
+ sessionId,
914
+ status: normalizeText(result?.status) || "unsubscribed",
915
+ threadId
916
+ };
917
+ }
918
+
919
+ async function stopPersistedSessionRuntime(session) {
920
+ const host = session.persistedRuntimeHost;
921
+ if (!host) return { stopped: false };
922
+ return stopPersistedRuntime(host, host.runtimeDir);
923
+ }
924
+
925
+ async function releasePersistedRuntimeProof(runtimeOptions, session) {
926
+ // Session closure already detached this participant. Its archived runtime
927
+ // path may now belong to the successor or another live conversation.
928
+ // Releasing the old proof must not stop or delete that shared process.
929
+ if (runtimeIsShared("", runtimeOptions)) {
930
+ return { sharedProcessRetained: true };
931
+ }
932
+ const existed = await directoryExists(runtimeOptions.runtimeDir);
933
+ if (!existed) return { existed };
934
+ const result = await stopPersistedSessionRuntime(session);
935
+ return { existed, result };
936
+ }
937
+
938
+ function releasePreparedRuntimeProof(preparation) {
939
+ return withLifecycle(async () => {
940
+ const prepared = preparation.read();
941
+ if (Object.hasOwn(prepared, "value")) return prepared.value;
942
+ const evidence = await releasePersistedRuntimeProof(prepared.runtimeOptions, prepared);
943
+ return prepared.complete(evidence);
944
+ });
945
+ }
946
+
947
+ async function releaseSession(sessionKey, context) {
948
+ const { sessionId, runOwner, session } = context;
949
+ if (context.changeover) {
950
+ const threadId = session.threadId;
951
+ for (const [key, provider] of providers) {
952
+ if (!threadId || providerOwners.get(key)?.sessionKey !== sessionKey) continue;
953
+ await stopThreadBeforeRelease(key, provider, threadId);
954
+ }
955
+ }
956
+ let exitFailure = null;
957
+ try {
958
+ const unsubscribeResult = await unsubscribeSessionThread(sessionKey, sessionId, session);
959
+ session.providerOptions = unsubscribeResult?.providerOptions || session.providerOptions;
960
+ } catch (error) {
961
+ debugLog("appServerThread.unsubscribe.error", {
962
+ error: debugError(error),
963
+ sessionId
964
+ });
965
+ } finally {
966
+ await runOwner.notificationQueue.drain(sessionId);
967
+ const cachedProviders = await stopCachedProvidersForSession(sessionId ? sessionKey : "", {
968
+ preserveProcessExitProof: context.preserveProcessExitProof,
969
+ requireStopped: context.requireStopped
970
+ });
971
+ if (cachedProviders.ok === false) {
972
+ debugLog("appServerRuntime.closeSession.cached.error", {
973
+ failed: cachedProviders.failed,
974
+ sessionId
975
+ });
976
+ }
977
+ let persistedRuntime = null;
978
+ if (session.exists) {
979
+ persistedRuntime = cachedProviders.stopped > 0
980
+ ? {
981
+ stopped: true,
982
+ verifiedStopped: true
983
+ }
984
+ : runtimeIsShared("", session.providerOptions || {})
985
+ ? {
986
+ sessionDetached: true,
987
+ sharedProcessRetained: true,
988
+ stopped: false,
989
+ verifiedStopped: false
990
+ }
991
+ : session.hasRuntime
992
+ ? await stopPersistedSessionRuntime(session)
993
+ : {
994
+ stopped: false,
995
+ verifiedStopped: false
996
+ };
997
+ debugLog("appServerRuntime.closeSession.persisted.done", {
998
+ removed: persistedRuntime?.removed === true,
999
+ runtimeDirRemoved: persistedRuntime?.runtimeDirRemoved === true,
1000
+ sessionId,
1001
+ stopped: persistedRuntime?.removed === true || persistedRuntime?.runtimeDirRemoved === true
1002
+ });
1003
+ }
1004
+ if (session.requiresExitProof) {
1005
+ // A fresh session with no acquired native owner needs no process proof.
1006
+ // Losing a cache or recorded runtime does not make it a fresh session.
1007
+ const { noRuntimeAcquired, cachedRuntimeExitVerified, persistedRuntimeExitVerified } =
1008
+ sessionExitProof(sessionKey, {
1009
+ cachedProviders,
1010
+ persistedRuntime,
1011
+ get hasRuntime() { return session.hasRuntime; },
1012
+ get threadId() { return session.threadId; }
1013
+ });
1014
+ if (cachedProviders.failed.length > 0 ||
1015
+ (!noRuntimeAcquired && !cachedRuntimeExitVerified && !persistedRuntimeExitVerified)) {
1016
+ exitFailure = {
1017
+ cachedFailures: cachedProviders.failed,
1018
+ cachedRuntimeExitVerified,
1019
+ persistedRuntime: persistedRuntime || null
1020
+ };
1021
+ }
1022
+ }
1023
+ }
1024
+ return { exitFailure };
1025
+ }
1026
+
1027
+ async function readPlanUsage(sessionKey) {
1028
+ for (const [key, provider] of providers) {
1029
+ if (providerOwners.get(key)?.sessionKey === sessionKey &&
1030
+ !provider.isHelperProvider() && provider.isAvailable()) {
1031
+ return provider.readPlanUsage();
1032
+ }
1033
+ }
1034
+ return { status: "unavailable", windows: [] };
1035
+ }
1036
+
1037
+ async function readSessionGoal(sessionKey, threadId) {
1038
+ if (!threadId) return { status: "available", threadId: "", goal: null };
1039
+ for (const [key, provider] of providers) {
1040
+ if (providerOwners.get(key)?.sessionKey === sessionKey &&
1041
+ !provider.isHelperProvider() && provider.isAvailable()) {
1042
+ const result = await provider.readGoal(threadId);
1043
+ return { status: "available", threadId, goal: result.goal || null };
1044
+ }
1045
+ }
1046
+ return { status: "unavailable", goal: null };
1047
+ }
1048
+
1049
+ const api = Object.freeze({
1050
+ providers, providerOwners, ownedRuntimes, lifecycleTasks, runtimeAcquisitions,
1051
+ withLifecycle, runtimeIsShared, rememberOwnedRuntime, forgetOwnedRuntime, attachRuntimeLifecycle,
1052
+ acquireRuntime, createProvider, createNativeProvider, providerForSession, ensureSession, retireAndCloseProvider, closeProvider, stopOwnedRuntime, stopCachedProvider, stopProvider, stopThreadBeforeRelease, readModelCatalog, invalidateRuntimes, recover,
1053
+ reportRecoveryEvent, pendingRecovery, readSessionGoal, readPlanUsage, stopCachedProvidersForSession, stopProvidersForProject, drainThreadEnvironmentTasks, stopPersistedRuntime, sessionExitProof,
1054
+ unsubscribeSessionThread, releasePersistedRuntimeProof, releasePreparedRuntimeProof, releaseSession,
1055
+ beginShutdown() { closing = true; }
1056
+ });
1057
+ return api;
1058
+ }