@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.
- package/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- 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
|
+
}
|