@jskit-ai/assistant-core 0.2.7 → 0.2.8

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 (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +13 -10
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -0,0 +1,371 @@
1
+ import {
2
+ codexAppServerOwnedRuntimeKey,
3
+ codexAppServerOwnedRuntimes,
4
+ codexAppServerRuntimeStopWasVerified,
5
+ codexAppServerStaleRuntimes,
6
+ nativeRequest,
7
+ providerRecords,
8
+ runningNativeThreads,
9
+ withRuntimeLifecycle
10
+ } from "./codexRuntimeInventory.js";
11
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
12
+
13
+ /**
14
+ * Retires the native runtimes an account change or a lost observation leaves untrusted.
15
+ * @param {object} dependencies
16
+ * @param {(record: object) => boolean} dependencies.inScope
17
+ * @param {<T>(operation: () => T, providerKey?: string, providerOptions?: object | null) => Promise<Awaited<T>>} dependencies.withLifecycle
18
+ * @param {() => void} dependencies.forgetModelCatalog
19
+ * @param {string} dependencies.exitUnverifiedCode
20
+ * @param {(event: string, details: object) => void} dependencies.debugLog
21
+ * @param {(error: unknown) => unknown} dependencies.debugError
22
+ */
23
+ export function createCodexProviderInvalidation({ inScope, withLifecycle, forgetModelCatalog, exitUnverifiedCode, debugLog, debugError }) {
24
+ async function invalidateProviderThreads(modelProviderId, includeOwned, toolHomeSource) {
25
+ const groups = new Map();
26
+ const inventory = [...providerRecords()];
27
+ if (includeOwned) for (const record of codexAppServerOwnedRuntimes.values()) {
28
+ if (!inventory.some(candidate => candidate.provider === record.provider)) inventory.push(record);
29
+ }
30
+ for (const record of inventory) {
31
+ if (!inScope(record)) continue;
32
+ const key = codexAppServerOwnedRuntimeKey(record.providerKey, record.providerOptions);
33
+ if (!groups.has(key)) groups.set(key, []);
34
+ groups.get(key).push(record);
35
+ }
36
+ const results = [], failed = [];
37
+ for (const [runtimeKey, peers] of groups) {
38
+ try {
39
+ await withRuntimeLifecycle(runtimeKey, async () => {
40
+ const available = peers.filter(record => record.provider.client?.isOpen());
41
+ if (!available.length) {
42
+ if (peers.some(record => record.provider.runtime || record.provider.runtimeStopOwner ||
43
+ codexAppServerOwnedRuntimes.get(runtimeKey)?.provider === record.provider)) {
44
+ throw new Error("Codex's existing control connection is unavailable. Restore it before changing this provider connection.");
45
+ }
46
+ return;
47
+ }
48
+ const request = (record, method, params) => {
49
+ const client = record.provider.client;
50
+ if (!client?.isOpen()) throw new Error("Codex's control connection closed during provider retirement. Restore it and retry.");
51
+ return client.request(method, params, { signal: AbortSignal.timeout(5_000) });
52
+ };
53
+ const selectedHome = normalizeText(toolHomeSource);
54
+ const dedicated = Boolean(selectedHome && peers.some(record =>
55
+ normalizeText(record.providerOptions.toolHomeSource) === selectedHome));
56
+ if (dedicated && peers.some(record => normalizeText(record.providerOptions.toolHomeSource) !== selectedHome)) {
57
+ throw new Error("Codex's provider credential home has other native consumers. Close its native terminal/helper and retry; unrelated runtimes remain running.");
58
+ }
59
+ const inspector = available[0], candidates = new Set(), seen = new Set();
60
+ let cursor;
61
+ do {
62
+ const page = await request(inspector, "thread/loaded/list", { limit: 100, ...(cursor ? { cursor } : {}) });
63
+ if (!Array.isArray(page?.data)) throw new Error("Codex returned an invalid loaded-thread inventory. Restore its connection before retrying.");
64
+ for (const id of page.data) {
65
+ if (typeof id !== "string" || !id) throw new Error("Codex returned an invalid loaded-thread identity.");
66
+ candidates.add(id);
67
+ }
68
+ cursor = page.nextCursor;
69
+ if (cursor && (typeof cursor !== "string" || seen.has(cursor) || seen.size >= 100)) throw new Error("Codex's loaded-thread inventory did not finish.");
70
+ if (cursor) seen.add(cursor);
71
+ } while (cursor);
72
+ for (const { provider } of peers) for (const [id, binding] of provider.threadEnvironments || []) {
73
+ if (dedicated || binding.params?.modelProvider === modelProviderId) candidates.add(id);
74
+ }
75
+ const threadIds = [];
76
+ for (const id of candidates) {
77
+ const { thread } = await request(inspector, "thread/read", { threadId: id, includeTurns: false });
78
+ if (!thread || thread.id !== id || typeof thread.modelProvider !== "string" || !thread.modelProvider ||
79
+ !(typeof thread.status === "string" ? thread.status : thread.status?.type)) {
80
+ throw new Error("Codex returned an untrustworthy thread/provider inventory. Restore its connection before changing credentials.");
81
+ }
82
+ if (dedicated && thread.modelProvider !== modelProviderId) {
83
+ throw new Error("Codex's provider credential home retains another provider thread. Close its native terminal/helper and retry; unrelated threads remain running.");
84
+ }
85
+ if (thread.modelProvider === modelProviderId) threadIds.push(id);
86
+ }
87
+ if (!threadIds.length && !dedicated) return;
88
+ const error = Object.assign(new Error("This Codex provider connection changed. Work is stopped; use Send or Resume after reconnecting."), {
89
+ code: "codex_provider_connection_changed"
90
+ });
91
+ for (const { provider } of peers) for (const threadId of threadIds) provider.conversationRuntime?.invalidateThread(threadId);
92
+ await Promise.allSettled(peers.flatMap(({ provider }) => threadIds
93
+ .map(id => provider.threadEnvironmentTasks?.get(id)).filter(Boolean)));
94
+ // The same observation journal retains selected stop ownership. Peers
95
+ // with other threads must keep their admission and active work intact.
96
+ for (const record of available) await record.observation?.prepare({ provider: record.provider, error, threadIds });
97
+ for (const threadId of threadIds) {
98
+ await inspector.provider.stopThreadForObservationLoss(threadId);
99
+ for (const record of available) {
100
+ const detached = await request(record, "thread/unsubscribe", { threadId });
101
+ if (!["unsubscribed", "notSubscribed", "notLoaded"].includes(detached?.status)) throw new Error("Codex did not confirm selected-thread detachment.");
102
+ }
103
+ const { thread } = await request(inspector, "thread/read", { threadId, includeTurns: false });
104
+ if ((typeof thread?.status === "string" ? thread.status : thread?.status?.type) !== "notLoaded") {
105
+ throw new Error("Close the native Codex terminal using this provider, then retry changing its connection. Another subscriber retained the selected thread; the shared server and other conversations remain running.");
106
+ }
107
+ }
108
+ if (dedicated) {
109
+ // A curated default also lives in this exact isolated helper's
110
+ // native config cache. Reuse verified retirement only after proving
111
+ // every loaded/cached thread belongs to the selected provider.
112
+ const owned = codexAppServerOwnedRuntimes.get(runtimeKey);
113
+ if (!owned || !peers.some(record => record.provider === owned.provider)) {
114
+ throw new Error("Codex's dedicated provider runtime ownership cannot be verified. Restore its control connection and retry.");
115
+ }
116
+ await owned.scope.stopOwnedRuntime(owned, { cached: false, preserveProcessExitProof: true, requireVerifiedExit: true });
117
+ }
118
+ for (const record of available) await record.observation?.complete({ provider: record.provider, error, threadIds, sharedStop: dedicated });
119
+ if (dedicated) for (const record of peers) record.scope.closeProvider(record.providerKey);
120
+ results.push({ runtimeKey, threadIds, stopped: true, sharedProcessRetained: !dedicated,
121
+ ...(dedicated ? { dedicatedRuntimeRetired: true } : {}) });
122
+ });
123
+ } catch (error) {
124
+ failed.push({ runtimeKey, error: error.message, retryable: true });
125
+ }
126
+ }
127
+ return { ok: failed.length === 0, failed, results, providerCount: results.length, stopped: results.reduce((sum, result) => sum + result.threadIds.length, 0) };
128
+ }
129
+
130
+ // A runtime with a running turn keeps the credentials it started with until
131
+ // that turn ends. Its active goals are paused so it starts no further turn;
132
+ // retireStaleRuntime stops it once it is idle. Any other runtime returns
133
+ // null and stops at once, like an idle one.
134
+ async function deferBusyRuntime(runtimeKey, peers) {
135
+ let running;
136
+ try {
137
+ running = await runningNativeThreads(peers);
138
+ } catch (error) {
139
+ // An unreadable runtime cannot show a running turn.
140
+ debugLog("appServerRuntime.staleInspection.error", { error: debugError(error), runtimeKey });
141
+ return null;
142
+ }
143
+ // Nor can one whose control connection closed while it was read.
144
+ const client = peers.find(record => record.provider.client?.isOpen())?.provider.client;
145
+ if (!running.length || !client) return null;
146
+ const pausedGoals = [];
147
+ for (const threadId of running) {
148
+ try {
149
+ const { goal } = await nativeRequest(client, "thread/goal/get", { threadId });
150
+ if (goal?.status !== "active") continue;
151
+ const result = await nativeRequest(client, "thread/goal/set", { threadId, status: "paused" });
152
+ if (result?.goal?.status !== "paused") throw new Error("Codex did not confirm that the goal was paused.");
153
+ pausedGoals.push(threadId);
154
+ } catch (error) {
155
+ debugLog("appServerRuntime.staleGoalPause.error", { error: debugError(error), runtimeKey, threadId });
156
+ }
157
+ }
158
+ for (const peer of peers) peer.provider.markStale();
159
+ const stale = {
160
+ runtimeKey, threadIds: running, pausedGoals, retiring: null,
161
+ providerKeys: peers.map(peer => peer.providerKey)
162
+ };
163
+ codexAppServerStaleRuntimes.set(runtimeKey, stale);
164
+ return stale;
165
+ }
166
+
167
+ async function invalidateRuntimes({
168
+ deferBusy = false,
169
+ includeOwned = false,
170
+ requireVerifiedExit = false,
171
+ stopOwnedRuntimes = false,
172
+ toolHomeSource = "",
173
+ modelProviderId = ""
174
+ } = {}) {
175
+ if (modelProviderId) return withLifecycle(() => invalidateProviderThreads(modelProviderId, includeOwned, toolHomeSource));
176
+ forgetModelCatalog();
177
+ const normalizedToolHomeSource = normalizeText(toolHomeSource);
178
+ const ownedRecordsByProvider = new Map(
179
+ [...codexAppServerOwnedRuntimes.values()].filter(inScope).map((record) => [record.provider, record])
180
+ );
181
+ const targets = [...providerRecords()]
182
+ .filter(record => inScope(record) && (!normalizedToolHomeSource ||
183
+ normalizeText(record.providerOptions.toolHomeSource) === normalizedToolHomeSource))
184
+ .map(record => ({
185
+ ...record,
186
+ kind: "cached",
187
+ record: ownedRecordsByProvider.get(record.provider) || null
188
+ }));
189
+ const targetedProviders = new Set(targets.map((target) => target.provider));
190
+ if (includeOwned) {
191
+ for (const record of codexAppServerOwnedRuntimes.values()) {
192
+ if (!inScope(record) || targetedProviders.has(record.provider)) {
193
+ continue;
194
+ }
195
+ const recordToolHomeSource = normalizeText(
196
+ record.providerOptions?.toolHomeSource
197
+ );
198
+ if (
199
+ normalizedToolHomeSource &&
200
+ recordToolHomeSource !== normalizedToolHomeSource
201
+ ) {
202
+ continue;
203
+ }
204
+ targets.push({
205
+ ...record,
206
+ kind: "owned",
207
+ provider: record.provider,
208
+ providerKey: record.providerKey,
209
+ record
210
+ });
211
+ targetedProviders.add(record.provider);
212
+ }
213
+ }
214
+ targets.sort((left, right) => Number(Boolean(left.record)) - Number(Boolean(right.record)));
215
+ const failed = [];
216
+ const results = [];
217
+ const deferred = [];
218
+ if (deferBusy) {
219
+ const groups = new Map();
220
+ for (const target of targets) {
221
+ const runtimeKey = codexAppServerOwnedRuntimeKey(target.providerKey, target.providerOptions);
222
+ if (!groups.has(runtimeKey)) groups.set(runtimeKey, []);
223
+ groups.get(runtimeKey).push(target);
224
+ }
225
+ for (const [runtimeKey, peers] of groups) {
226
+ if (codexAppServerStaleRuntimes.has(runtimeKey)) {
227
+ deferred.push(codexAppServerStaleRuntimes.get(runtimeKey));
228
+ } else {
229
+ const stale = await withRuntimeLifecycle(runtimeKey, () => deferBusyRuntime(runtimeKey, peers));
230
+ if (!stale) continue;
231
+ deferred.push(stale);
232
+ }
233
+ for (const peer of peers) {
234
+ targets.splice(targets.indexOf(peer), 1);
235
+ results.push({ providerKey: peer.providerKey, deferred: true, stopped: false });
236
+ }
237
+ }
238
+ }
239
+ for (const target of targets) {
240
+ if (!target.resources?.invalidation) continue;
241
+ const error = Object.assign(new Error("Codex's native runtime was invalidated."), {
242
+ code: "codex_runtime_invalidated"
243
+ });
244
+ target.invalidation = { error };
245
+ try {
246
+ if (await target.resources.invalidation.prepare({ provider: target.provider, error }) === false) {
247
+ delete target.invalidation;
248
+ }
249
+ } catch (preparationError) {
250
+ target.invalidation.preparationError = preparationError;
251
+ }
252
+ }
253
+ for (const target of targets) {
254
+ const preserveProcessExitProof = target.preserveProcessExitProof === true;
255
+ try {
256
+ const result = stopOwnedRuntimes && target.record
257
+ ? await target.scope.stopOwnedRuntime({
258
+ ...(target.record || {}),
259
+ provider: target.provider,
260
+ providerKey: target.providerKey
261
+ }, {
262
+ cached: target.kind === "cached",
263
+ preserveProcessExitProof,
264
+ requireVerifiedExit
265
+ })
266
+ : await target.scope.stopCachedProvider(target.providerKey, {
267
+ preserveProcessExitProof,
268
+ requireStopped: stopOwnedRuntimes && target.record != null
269
+ });
270
+ if (
271
+ requireVerifiedExit &&
272
+ target.record &&
273
+ !codexAppServerRuntimeStopWasVerified(result)
274
+ ) {
275
+ throw Object.assign(new Error("Codex app-server process exit could not be verified."), {
276
+ code: exitUnverifiedCode, providerKey: target.providerKey, retryable: true
277
+ });
278
+ }
279
+ target.stopResult = result;
280
+ results.push(result);
281
+ } catch (error) {
282
+ target.stopError = error;
283
+ failed.push({
284
+ code: normalizeText(error?.code),
285
+ error: String(error?.message || "Codex app-server runtime invalidation failed."),
286
+ providerKey: target.providerKey,
287
+ retryable: error?.retryable === true
288
+ });
289
+ }
290
+ }
291
+ for (const target of targets) {
292
+ if (!target.invalidation) continue;
293
+ const runtimeKey = codexAppServerOwnedRuntimeKey(target.providerKey, target.providerOptions);
294
+ const peers = targets.filter(peer =>
295
+ codexAppServerOwnedRuntimeKey(peer.providerKey, peer.providerOptions) === runtimeKey);
296
+ const proof = peers.find(peer => codexAppServerRuntimeStopWasVerified(peer.stopResult));
297
+ const stopped = proof
298
+ ? { status: "fulfilled", value: proof.stopResult }
299
+ : { status: "rejected", reason: peers.find(peer => peer.stopError)?.stopError ||
300
+ Object.assign(new Error("Codex app-server process exit could not be verified."), {
301
+ code: exitUnverifiedCode, providerKey: target.providerKey, retryable: true
302
+ }) };
303
+ try {
304
+ await target.resources.invalidation.complete({
305
+ provider: target.provider, ...target.invalidation, stopped
306
+ });
307
+ if (target.invalidation.preparationError) throw target.invalidation.preparationError;
308
+ if (stopped.status === "rejected") throw stopped.reason;
309
+ } catch (error) {
310
+ failed.push({
311
+ code: normalizeText(error?.code),
312
+ error: String(error?.message || "Codex app-server invalidation could not be recorded."),
313
+ providerKey: target.providerKey,
314
+ retryable: true
315
+ });
316
+ }
317
+ }
318
+ const stopped = results.filter((result) => result.stopped).length;
319
+ return {
320
+ deferred: deferred.map(({ runtimeKey, providerKeys, threadIds, pausedGoals }) =>
321
+ ({ runtimeKey, providerKeys: [...providerKeys], threadIds: [...threadIds], pausedGoals: [...pausedGoals] })),
322
+ failed,
323
+ ok: failed.length === 0,
324
+ providerCount: targets.length + deferred.reduce((sum, stale) => sum + stale.providerKeys.length, 0),
325
+ results,
326
+ stopped
327
+ };
328
+ }
329
+
330
+ async function suspendUnobservedProvider(record, error) {
331
+ const targets = [record];
332
+ let sharedStop = false;
333
+ try {
334
+ const threads = await record.observation.prepare({ provider: record.provider, error });
335
+ if (!threads.size) throw new Error("The unobserved Codex thread could not be identified.");
336
+ for (const [threadId, turnId] of threads) {
337
+ await record.provider.stopThreadForObservationLoss(threadId, turnId);
338
+ }
339
+ } catch {
340
+ sharedStop = true;
341
+ const runtimeKey = codexAppServerOwnedRuntimeKey(record.providerKey, record.providerOptions);
342
+ for (const peer of providerRecords()) {
343
+ if (peer !== record && codexAppServerOwnedRuntimeKey(peer.providerKey, peer.providerOptions) === runtimeKey) {
344
+ peer.provider.observationFailure = error;
345
+ targets.push(peer);
346
+ }
347
+ }
348
+ // Every durable barrier is attempted before the native stop; a storage
349
+ // failure cannot leave work running.
350
+ for (const target of targets) {
351
+ try {
352
+ await target.observation.barrier({ provider: target.provider, error });
353
+ } catch (failure) {
354
+ target.scope.reportRecoveryEvent(target, "persistenceFailed", failure);
355
+ }
356
+ }
357
+ const owned = codexAppServerOwnedRuntimes.get(runtimeKey) || record;
358
+ await owned.scope.stopOwnedRuntime(owned, { preserveProcessExitProof: true, requireVerifiedExit: true });
359
+ }
360
+ for (const target of targets) {
361
+ // Each adapter persists, then publishes. It can release this provider
362
+ // only after its stopped state is durable.
363
+ await target.observation.complete({ provider: target.provider, error, sharedStop });
364
+ }
365
+ }
366
+
367
+ return Object.freeze({
368
+ invalidateRuntimes,
369
+ suspendUnobservedProvider
370
+ });
371
+ }