@jskit-ai/assistant-core 0.2.6 → 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.
- package/examples/conversation/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +18 -12
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -1,127 +1,77 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { stat } from "node:fs/promises";
|
|
3
2
|
import { CodexAppServerAgentProvider } from "./codexProvider.js";
|
|
4
3
|
import { codexAppServerProjectHookTrustConfig } from "./codexConfiguration.js";
|
|
4
|
+
import { createCodexRuntimeModelCatalog } from "./codexModelCatalog.js";
|
|
5
|
+
import { createCodexProviderInvalidation } from "./codexProviderInvalidation.js";
|
|
6
|
+
import { createCodexProviderRelease } from "./codexProviderRelease.js";
|
|
5
7
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const runtimeKey = codexAppServerOwnedRuntimeKey("", { runtimeDir });
|
|
47
|
-
return withRuntimeLifecycle(runtimeKey, async () => {
|
|
48
|
-
const stale = codexAppServerStaleRuntimes.get(runtimeKey);
|
|
49
|
-
if (stale) {
|
|
50
|
-
stale.superseded = true;
|
|
51
|
-
return { deferred: true, removed: false };
|
|
52
|
-
}
|
|
53
|
-
if (codexAppServerOwnedRuntimes.has(runtimeKey) || runtimePeers(runtimeKey).length) {
|
|
54
|
-
return { removed: false };
|
|
55
|
-
}
|
|
56
|
-
const metadata = await readCodexAppServerMetadata(runtimeDir);
|
|
57
|
-
if (metadata?.processState !== CODEX_APP_SERVER_PROCESS_STATE.STOPPED || !metadata.processExitVerifiedAt) {
|
|
58
|
-
return { removed: false };
|
|
59
|
-
}
|
|
60
|
-
return { removed: await removeCodexAppServerRuntimeDir(runtimeDir) };
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function codexAppServerRuntimeStopWasVerified(result = {}) {
|
|
65
|
-
return result?.stopped === true ||
|
|
66
|
-
result?.processExitVerified === true ||
|
|
67
|
-
result?.runtimeDirRemoved === true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function directoryExists(filePath = "") {
|
|
71
|
-
try {
|
|
72
|
-
return (await stat(filePath)).isDirectory();
|
|
73
|
-
} catch (error) {
|
|
74
|
-
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function* providerRecords() {
|
|
82
|
-
for (const records of codexAppServerProviders.values()) yield* records.values();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Every owner's provider records on one native runtime.
|
|
86
|
-
function runtimePeers(runtimeKey) {
|
|
87
|
-
return [...providerRecords()].filter(record =>
|
|
88
|
-
codexAppServerOwnedRuntimeKey(record.providerKey, record.providerOptions) === runtimeKey);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function nativeRequest(client, method, params) {
|
|
92
|
-
return client.request(method, params, { signal: AbortSignal.timeout(5_000) });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// The loaded threads of one native runtime that have a running turn, read from
|
|
96
|
-
// the runtime itself so Temporary, Main and native terminal turns all count.
|
|
97
|
-
async function runningNativeThreads(peers) {
|
|
98
|
-
const client = peers.find(record => record.provider.client?.isOpen?.())?.provider.client;
|
|
99
|
-
if (!client) return [];
|
|
100
|
-
const running = [];
|
|
101
|
-
const seen = new Set();
|
|
102
|
-
let cursor;
|
|
103
|
-
do {
|
|
104
|
-
const page = await nativeRequest(client, "thread/loaded/list", { limit: 100, ...(cursor ? { cursor } : {}) });
|
|
105
|
-
for (const threadId of Array.isArray(page?.data) ? page.data : []) {
|
|
106
|
-
const { thread } = await nativeRequest(client, "thread/read", { threadId, includeTurns: false });
|
|
107
|
-
if ((typeof thread?.status === "string" ? thread.status : thread?.status?.type) === "active") running.push(threadId);
|
|
108
|
-
}
|
|
109
|
-
cursor = page?.nextCursor;
|
|
110
|
-
if (cursor && seen.has(cursor)) throw new Error("Codex's loaded-thread inventory did not finish.");
|
|
111
|
-
if (cursor) seen.add(cursor);
|
|
112
|
-
} while (cursor);
|
|
113
|
-
return running;
|
|
8
|
+
codexAppServerOwnedRuntimeKey,
|
|
9
|
+
codexAppServerOwnedRuntimes,
|
|
10
|
+
codexAppServerProviders,
|
|
11
|
+
codexAppServerRuntimeStopWasVerified,
|
|
12
|
+
codexAppServerStaleRuntimes,
|
|
13
|
+
providerRecords,
|
|
14
|
+
runningNativeThreads,
|
|
15
|
+
runtimePeers,
|
|
16
|
+
withRuntimeLifecycle
|
|
17
|
+
} from "./codexRuntimeInventory.js";
|
|
18
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
19
|
+
|
|
20
|
+
/** @typedef {ReturnType<typeof createCodexAppServerProviderOwner>} CodexAppServerProviderOwner */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The provider sessions a run owner works through, supplied by its application: the provider owner, the managed
|
|
24
|
+
* sessions it keeps running, and the per-session contexts it prepares.
|
|
25
|
+
* @typedef {object} CodexProviderSessions
|
|
26
|
+
* @property {CodexAppServerProviderOwner} owner
|
|
27
|
+
* @property {Map<string, object>} managed
|
|
28
|
+
* @property {(sessionId: string, providerOptions: object) => object} context
|
|
29
|
+
* @property {(providerKey: string) => object} keyFields
|
|
30
|
+
* @property {() => void} assertOpen
|
|
31
|
+
* @property {object} [connectionPolicy]
|
|
32
|
+
* @property {(request: { sessionId: string, runtime: object, session: object }) => Promise<object | null>} [outputContext]
|
|
33
|
+
* @property {number} [wellbeingMs]
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** An owner outside an application lifecycle is always open. */
|
|
37
|
+
function ownerStaysOpen() {}
|
|
38
|
+
|
|
39
|
+
/** An owner without a recovery listener reports nothing. */
|
|
40
|
+
function ignoreRecoveryEvent() {}
|
|
41
|
+
|
|
42
|
+
/** An owner without a debug log records nothing. */
|
|
43
|
+
function ignoreDebugEvent() {}
|
|
44
|
+
|
|
45
|
+
/** A provider built without a native host description gets an empty one. */
|
|
46
|
+
function emptyNativeHost() {
|
|
47
|
+
return {};
|
|
114
48
|
}
|
|
115
49
|
|
|
116
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Internal consumer view of the one native provider owner, not a server per view.
|
|
52
|
+
* @param {object} [dependencies]
|
|
53
|
+
* @param {string} [dependencies.runtimeRoot]
|
|
54
|
+
* @param {() => void} [dependencies.assertOpen]
|
|
55
|
+
* @param {(owner: object, event: string, error: unknown) => void} [dependencies.onRecoveryEvent]
|
|
56
|
+
* @param {(event: string, details: object) => void} [dependencies.debugLog]
|
|
57
|
+
* @param {(error: unknown) => unknown} [dependencies.debugError]
|
|
58
|
+
* @param {((parameters: object) => CodexAppServerAgentProvider) | null} [dependencies.providerFactory]
|
|
59
|
+
* @param {(parameters: object) => object} [dependencies.prepareNativeHost]
|
|
60
|
+
* @param {string} [dependencies.runtimeBusyCode]
|
|
61
|
+
* @param {string} [dependencies.exitUnverifiedCode]
|
|
62
|
+
* @param {string} [dependencies.requiredStopCode]
|
|
63
|
+
* @param {string} [dependencies.runtimeCloseError]
|
|
64
|
+
* @param {number} [dependencies.transientIdleMs]
|
|
65
|
+
*/
|
|
117
66
|
export function createCodexAppServerProviderOwner({
|
|
118
67
|
runtimeRoot = "",
|
|
119
|
-
assertOpen: assertApplicationOpen =
|
|
120
|
-
onRecoveryEvent =
|
|
121
|
-
debugLog =
|
|
68
|
+
assertOpen: assertApplicationOpen = ownerStaysOpen,
|
|
69
|
+
onRecoveryEvent = ignoreRecoveryEvent,
|
|
70
|
+
debugLog = ignoreDebugEvent,
|
|
122
71
|
debugError = error => error,
|
|
72
|
+
// readable-check: allow dependency-default - input validation: an application passes null to choose the built-in provider
|
|
123
73
|
providerFactory = null,
|
|
124
|
-
prepareNativeHost =
|
|
74
|
+
prepareNativeHost = emptyNativeHost,
|
|
125
75
|
runtimeBusyCode = "codex_app_server_runtime_busy",
|
|
126
76
|
exitUnverifiedCode = "codex_runtime_exit_unverified",
|
|
127
77
|
requiredStopCode = "codex_runtime_exit_unverified",
|
|
@@ -135,7 +85,6 @@ export function createCodexAppServerProviderOwner({
|
|
|
135
85
|
let codexAppServerProviderLifecycle = Promise.resolve();
|
|
136
86
|
let closing = false;
|
|
137
87
|
let runtimeLifecycle = null;
|
|
138
|
-
let codexAppServerChatModelCatalog = null;
|
|
139
88
|
// Providers borrowed by work that is not a conversation, terminal or helper.
|
|
140
89
|
const transientReleases = new Map();
|
|
141
90
|
|
|
@@ -157,6 +106,15 @@ export function createCodexAppServerProviderOwner({
|
|
|
157
106
|
}
|
|
158
107
|
};
|
|
159
108
|
|
|
109
|
+
const modelCatalog = createCodexRuntimeModelCatalog({ withLifecycle, assertOpen, acquireRuntime, stopOwnedRuntime, forgetOwnedRuntime });
|
|
110
|
+
const invalidation = createCodexProviderInvalidation({
|
|
111
|
+
inScope, withLifecycle, forgetModelCatalog: modelCatalog.forget, exitUnverifiedCode, debugLog, debugError
|
|
112
|
+
});
|
|
113
|
+
const sessionRelease = createCodexProviderRelease({
|
|
114
|
+
records, providers, providerOwners, ownedRuntimes, withLifecycle, runtimeIsShared, forgetOwnedRuntime, closeProvider,
|
|
115
|
+
requiredStopCode, exitUnverifiedCode, runtimeCloseError, debugLog, debugError
|
|
116
|
+
});
|
|
117
|
+
|
|
160
118
|
function attachRuntimeLifecycle(lifecycle) {
|
|
161
119
|
if (runtimeLifecycle && runtimeLifecycle !== lifecycle) {
|
|
162
120
|
throw new TypeError("Codex provider owner already has a runtime lifecycle.");
|
|
@@ -207,7 +165,7 @@ export function createCodexAppServerProviderOwner({
|
|
|
207
165
|
providerKey: normalizeText(providerKey),
|
|
208
166
|
providerOptions,
|
|
209
167
|
sessionKey: consumer?.owner.sessionKey || "",
|
|
210
|
-
runtimeDir: normalizeText(providerOptions
|
|
168
|
+
runtimeDir: normalizeText(providerOptions.runtimeDir),
|
|
211
169
|
runtimeKey,
|
|
212
170
|
scope: api,
|
|
213
171
|
resources: consumer?.resources,
|
|
@@ -237,7 +195,17 @@ export function createCodexAppServerProviderOwner({
|
|
|
237
195
|
}
|
|
238
196
|
}
|
|
239
197
|
|
|
240
|
-
|
|
198
|
+
/**
|
|
199
|
+
* @param {object} request
|
|
200
|
+
* @param {string} request.providerKey
|
|
201
|
+
* @param {object} [request.providerOptions]
|
|
202
|
+
* @param {object} [request.owner]
|
|
203
|
+
* @param {(onObservationLost: (error: unknown) => Promise<void>) => CodexAppServerAgentProvider} request.create
|
|
204
|
+
* @param {object} [request.observation]
|
|
205
|
+
* @param {object} [request.resources]
|
|
206
|
+
* @param {boolean} [request.preserveProcessExitProof]
|
|
207
|
+
*/
|
|
208
|
+
function createProvider({ providerKey, providerOptions = {}, owner = {}, create, observation, resources, preserveProcessExitProof = false }) {
|
|
241
209
|
assertOpen();
|
|
242
210
|
if (!path.isAbsolute(providerOptions.runtimeDir || "")) throw new TypeError("Codex ownership requires an absolute native runtime directory.");
|
|
243
211
|
const existing = records.get(providerKey);
|
|
@@ -255,6 +223,7 @@ export function createCodexAppServerProviderOwner({
|
|
|
255
223
|
async function retireAndCloseProvider(providerKey = "", options = {}) {
|
|
256
224
|
const record = records.get(normalizeText(providerKey));
|
|
257
225
|
if (!record) return;
|
|
226
|
+
// readable-check: allow optional-call - optional operation: a driver's resources have no retire step, and a borrowed provider has no resources
|
|
258
227
|
await record.resources?.retire?.(record.provider);
|
|
259
228
|
closeProvider(normalizeText(providerKey), options);
|
|
260
229
|
}
|
|
@@ -335,8 +304,8 @@ export function createCodexAppServerProviderOwner({
|
|
|
335
304
|
for (const [threadId, binding] of provider.threadEnvironments || []) {
|
|
336
305
|
const boundEnv = binding.params.config?.shell_environment_policy?.set || {};
|
|
337
306
|
if (!Object.entries(boundEnv).some(([key, value]) => key.endsWith("_COMMAND_SOCKET") && value === event.socketPath)) continue;
|
|
338
|
-
//
|
|
339
|
-
// provider's
|
|
307
|
+
// Listener replacement and rejected command recovery stay on the
|
|
308
|
+
// provider's per-thread operation that is already running.
|
|
340
309
|
if (provider.threadEnvironmentTasks?.has(threadId)) continue;
|
|
341
310
|
void prepared.runInContext(() => provider.ensureThreadControls(threadId))
|
|
342
311
|
.catch(error => provider.failObservation(error))
|
|
@@ -376,11 +345,11 @@ export function createCodexAppServerProviderOwner({
|
|
|
376
345
|
if (transientReleases.get(providerKey) !== release || release.users > 0) return;
|
|
377
346
|
transientReleases.delete(providerKey);
|
|
378
347
|
if (closing) return;
|
|
379
|
-
stopCachedProvider(providerKey).catch(error => debugLog("appServerRuntime.transientRelease.error", {
|
|
348
|
+
sessionRelease.stopCachedProvider(providerKey).catch(error => debugLog("appServerRuntime.transientRelease.error", {
|
|
380
349
|
error: debugError(error), providerKey
|
|
381
350
|
}));
|
|
382
351
|
}, transientIdleMs);
|
|
383
|
-
release.timer.unref
|
|
352
|
+
release.timer.unref();
|
|
384
353
|
};
|
|
385
354
|
}
|
|
386
355
|
|
|
@@ -391,7 +360,7 @@ export function createCodexAppServerProviderOwner({
|
|
|
391
360
|
async function borrowSession(context) {
|
|
392
361
|
const providerKey = context.providerKey;
|
|
393
362
|
let release = transientReleases.get(providerKey);
|
|
394
|
-
if (!release && (records.has(providerKey) || context.runOwner
|
|
363
|
+
if (!release && (records.has(providerKey) || context.runOwner.runtimeLifecycle.managedThreadId(providerKey))) {
|
|
395
364
|
return { provider: await acquireSession(context), release() {} };
|
|
396
365
|
}
|
|
397
366
|
if (!release) transientReleases.set(providerKey, release = { users: 0, timer: null });
|
|
@@ -446,15 +415,7 @@ export function createCodexAppServerProviderOwner({
|
|
|
446
415
|
try {
|
|
447
416
|
context.assertAdmission();
|
|
448
417
|
await acquireRuntime({
|
|
449
|
-
operation: () => {
|
|
450
|
-
if (typeof provider.ensureAvailable === "function") {
|
|
451
|
-
return provider.ensureAvailable({ modelProviderId: providerOptions.routingModelProviderId });
|
|
452
|
-
}
|
|
453
|
-
if (typeof provider.listLoadedThreads === "function") {
|
|
454
|
-
return provider.listLoadedThreads({ limit: 1 });
|
|
455
|
-
}
|
|
456
|
-
return provider.ensureRuntime?.();
|
|
457
|
-
},
|
|
418
|
+
operation: () => provider.ensureAvailable({ modelProviderId: providerOptions.routingModelProviderId }),
|
|
458
419
|
provider,
|
|
459
420
|
providerKey,
|
|
460
421
|
providerOptions
|
|
@@ -468,7 +429,7 @@ export function createCodexAppServerProviderOwner({
|
|
|
468
429
|
}
|
|
469
430
|
if (!context.closing && mainThreadId) await provider.failObservation(error);
|
|
470
431
|
if (!context.closing && !provider.observationFailure) {
|
|
471
|
-
await stopProvider(context.providerKey, providerOptions, {
|
|
432
|
+
await sessionRelease.stopProvider(context.providerKey, providerOptions, {
|
|
472
433
|
get runtimeHost() { return context.runtimeHost; }
|
|
473
434
|
});
|
|
474
435
|
}
|
|
@@ -479,8 +440,9 @@ export function createCodexAppServerProviderOwner({
|
|
|
479
440
|
function closeProvider(providerKey = "", { closeProvider = true } = {}) {
|
|
480
441
|
const record = records.get(providerKey);
|
|
481
442
|
if (!record) return;
|
|
443
|
+
// readable-check: allow optional-call - optional operation: a provider borrowed for a catalogue read has no resources
|
|
482
444
|
record.resources?.release?.(record.provider);
|
|
483
|
-
if (closeProvider) record.provider.close
|
|
445
|
+
if (closeProvider) record.provider.close();
|
|
484
446
|
records.delete(providerKey);
|
|
485
447
|
if (!records.size) codexAppServerProviders.delete(api);
|
|
486
448
|
if (record.provider.stale) {
|
|
@@ -501,8 +463,10 @@ export function createCodexAppServerProviderOwner({
|
|
|
501
463
|
const provider = record.provider;
|
|
502
464
|
const providerKey = normalizeText(record.providerKey);
|
|
503
465
|
const resources = record.resources || records.get(providerKey)?.resources;
|
|
466
|
+
// readable-check: allow optional-call - optional operation: a driver's resources have no retire step, and a borrowed provider has no resources
|
|
504
467
|
const retirement = Promise.resolve().then(() => resources?.retire?.(provider));
|
|
505
468
|
const runtimeStop = Promise.resolve().then(async () => {
|
|
469
|
+
// readable-check: allow method-probe - safety invariant: a record without a provider that stops would leave its process running unverified
|
|
506
470
|
if (typeof provider?.stopRuntime !== "function") {
|
|
507
471
|
throw new Error("Codex app-server provider must implement stopRuntime().");
|
|
508
472
|
}
|
|
@@ -524,7 +488,7 @@ export function createCodexAppServerProviderOwner({
|
|
|
524
488
|
if (stopped.status === "fulfilled" && codexAppServerRuntimeStopWasVerified(stopped.value)) {
|
|
525
489
|
forgetOwnedRuntime(provider);
|
|
526
490
|
} else {
|
|
527
|
-
provider?.close
|
|
491
|
+
provider?.close();
|
|
528
492
|
}
|
|
529
493
|
if (retired.status === "fulfilled" && cached) {
|
|
530
494
|
(record.scope || api).closeProvider(providerKey, { closeProvider: stopped.status !== "fulfilled" });
|
|
@@ -542,327 +506,6 @@ export function createCodexAppServerProviderOwner({
|
|
|
542
506
|
};
|
|
543
507
|
}
|
|
544
508
|
|
|
545
|
-
async function stopCachedProviderUnlocked(providerKey = "", {
|
|
546
|
-
preserveProcessExitProof = false,
|
|
547
|
-
requireStopped = false
|
|
548
|
-
} = {}) {
|
|
549
|
-
const normalizedProviderKey = normalizeText(providerKey);
|
|
550
|
-
const provider = providers.get(normalizedProviderKey);
|
|
551
|
-
if (!provider) {
|
|
552
|
-
closeProvider(normalizedProviderKey);
|
|
553
|
-
return {
|
|
554
|
-
providerKey: normalizedProviderKey,
|
|
555
|
-
stopped: false
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
await records.get(normalizedProviderKey)?.resources?.retire?.(provider);
|
|
559
|
-
const sharedProcessRetained = runtimeIsShared(
|
|
560
|
-
normalizedProviderKey, providerOwners.get(normalizedProviderKey)?.providerOptions
|
|
561
|
-
);
|
|
562
|
-
if (sharedProcessRetained) {
|
|
563
|
-
closeProvider(normalizedProviderKey);
|
|
564
|
-
return {
|
|
565
|
-
providerKey: normalizedProviderKey,
|
|
566
|
-
sessionDetached: true,
|
|
567
|
-
sharedProcessRetained: true,
|
|
568
|
-
stopped: false
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
if (typeof provider.stopRuntime !== "function") {
|
|
572
|
-
closeProvider(normalizedProviderKey);
|
|
573
|
-
throw new Error("Codex app-server provider must implement stopRuntime().");
|
|
574
|
-
}
|
|
575
|
-
let providerStoppedRuntime = false;
|
|
576
|
-
try {
|
|
577
|
-
const stoppedRuntime = await provider.stopRuntime({
|
|
578
|
-
preserveProcessExitProof: preserveProcessExitProof || requireStopped
|
|
579
|
-
});
|
|
580
|
-
providerStoppedRuntime = codexAppServerRuntimeStopWasVerified(stoppedRuntime);
|
|
581
|
-
if (requireStopped && !providerStoppedRuntime) {
|
|
582
|
-
const error = new Error("Codex app-server process exit could not be verified.");
|
|
583
|
-
error.code = requiredStopCode;
|
|
584
|
-
error.retryable = true;
|
|
585
|
-
throw error;
|
|
586
|
-
}
|
|
587
|
-
if (providerStoppedRuntime) {
|
|
588
|
-
forgetOwnedRuntime(provider);
|
|
589
|
-
}
|
|
590
|
-
return {
|
|
591
|
-
...(stoppedRuntime && typeof stoppedRuntime === "object" ? stoppedRuntime : {}),
|
|
592
|
-
providerKey: normalizedProviderKey,
|
|
593
|
-
stopped: providerStoppedRuntime
|
|
594
|
-
};
|
|
595
|
-
} finally {
|
|
596
|
-
closeProvider(normalizedProviderKey, {
|
|
597
|
-
closeProvider: !providerStoppedRuntime
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function stopCachedProvider(providerKey = "", options = {}) {
|
|
603
|
-
return withLifecycle(
|
|
604
|
-
() => stopCachedProviderUnlocked(providerKey, options),
|
|
605
|
-
normalizeText(providerKey)
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function runtimeExitUnverifiedError(providerKey = "") {
|
|
610
|
-
const error = new Error("Codex app-server process exit could not be verified.");
|
|
611
|
-
error.code = exitUnverifiedCode;
|
|
612
|
-
error.providerKey = normalizeText(providerKey);
|
|
613
|
-
error.retryable = true;
|
|
614
|
-
return error;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
async function stopPreparedRuntime(context) {
|
|
618
|
-
const { runtime, parameters } = context.runtimeHost;
|
|
619
|
-
return runtime.stop(parameters);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
async function stopProvider(providerKey, providerOptions, context = {}) {
|
|
623
|
-
const { preserveProcessExitProof = false, requireStopped = false } = context;
|
|
624
|
-
const provider = providers.get(providerKey);
|
|
625
|
-
if (!provider) {
|
|
626
|
-
if (runtimeIsShared(providerKey, providerOptions)) {
|
|
627
|
-
return {
|
|
628
|
-
providerKey,
|
|
629
|
-
sessionDetached: true,
|
|
630
|
-
sharedProcessRetained: true,
|
|
631
|
-
stopped: false
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
const stoppedRuntime = await stopPreparedRuntime(context);
|
|
635
|
-
const stopped = codexAppServerRuntimeStopWasVerified(stoppedRuntime);
|
|
636
|
-
if (requireStopped && !stopped) {
|
|
637
|
-
throw runtimeExitUnverifiedError(providerKey);
|
|
638
|
-
}
|
|
639
|
-
return {
|
|
640
|
-
...(Boolean(stoppedRuntime && typeof stoppedRuntime === "object" && !Array.isArray(stoppedRuntime)) ? stoppedRuntime : {}),
|
|
641
|
-
providerKey,
|
|
642
|
-
stopped
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
return stopCachedProvider(providerKey, {
|
|
646
|
-
preserveProcessExitProof,
|
|
647
|
-
requireStopped
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
async function stopThreadBeforeRelease(providerKey, provider, threadId) {
|
|
652
|
-
try {
|
|
653
|
-
await provider.stopThreadForObservationLoss(threadId, "");
|
|
654
|
-
} catch (error) {
|
|
655
|
-
// If other sessions retain the shared process, closing this
|
|
656
|
-
// socket alone is not proof that its native goal/work stopped.
|
|
657
|
-
const shared = runtimeIsShared(
|
|
658
|
-
providerKey, providerOwners.get(providerKey)?.providerOptions
|
|
659
|
-
);
|
|
660
|
-
if (shared) throw error;
|
|
661
|
-
// The caller's existing shutdown must instead prove process exit.
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function readModelCatalog({
|
|
666
|
-
prepareProviderOptions,
|
|
667
|
-
providerFactory,
|
|
668
|
-
resources = null,
|
|
669
|
-
signal = null,
|
|
670
|
-
cacheMs = 30_000
|
|
671
|
-
} = {}) {
|
|
672
|
-
return withLifecycle(async () => {
|
|
673
|
-
assertOpen();
|
|
674
|
-
signal?.throwIfAborted();
|
|
675
|
-
const providerOptions = await prepareProviderOptions();
|
|
676
|
-
return withRuntimeLifecycle(codexAppServerOwnedRuntimeKey("", providerOptions), async () => {
|
|
677
|
-
const existing = codexAppServerOwnedRuntimes.get(
|
|
678
|
-
codexAppServerOwnedRuntimeKey("", providerOptions)
|
|
679
|
-
);
|
|
680
|
-
const provider = existing?.provider || providerFactory(providerOptions);
|
|
681
|
-
let runtime = null;
|
|
682
|
-
let catalog = null;
|
|
683
|
-
let identity = "";
|
|
684
|
-
try {
|
|
685
|
-
assertOpen();
|
|
686
|
-
identity = JSON.stringify(await provider.currentRuntimeInfo());
|
|
687
|
-
signal?.throwIfAborted();
|
|
688
|
-
if (codexAppServerChatModelCatalog?.identity === identity &&
|
|
689
|
-
codexAppServerChatModelCatalog.expiresAt > Date.now()) {
|
|
690
|
-
return codexAppServerChatModelCatalog.value;
|
|
691
|
-
}
|
|
692
|
-
if (!existing?.provider) {
|
|
693
|
-
runtime = await acquireRuntime({
|
|
694
|
-
operation: () => provider.ensureRuntime(),
|
|
695
|
-
provider,
|
|
696
|
-
providerOptions
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
catalog = await provider.listModels({ includeHidden: false, limit: 100 }, { signal });
|
|
700
|
-
} finally {
|
|
701
|
-
if (!existing?.provider) {
|
|
702
|
-
try {
|
|
703
|
-
if ((runtime || provider.runtime)?.reused === false) {
|
|
704
|
-
await stopOwnedRuntime({ provider, resources });
|
|
705
|
-
} else {
|
|
706
|
-
forgetOwnedRuntime(provider);
|
|
707
|
-
}
|
|
708
|
-
} finally {
|
|
709
|
-
provider.close();
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
codexAppServerChatModelCatalog = {
|
|
714
|
-
expiresAt: Date.now() + cacheMs,
|
|
715
|
-
identity,
|
|
716
|
-
value: catalog
|
|
717
|
-
};
|
|
718
|
-
return catalog;
|
|
719
|
-
});
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
async function invalidateProviderThreads(modelProviderId, includeOwned, toolHomeSource) {
|
|
724
|
-
const groups = new Map();
|
|
725
|
-
const inventory = [...providerRecords()];
|
|
726
|
-
if (includeOwned) for (const record of codexAppServerOwnedRuntimes.values()) {
|
|
727
|
-
if (!inventory.some(candidate => candidate.provider === record.provider)) inventory.push(record);
|
|
728
|
-
}
|
|
729
|
-
for (const record of inventory) {
|
|
730
|
-
if (!inScope(record)) continue;
|
|
731
|
-
const key = codexAppServerOwnedRuntimeKey(record.providerKey, record.providerOptions);
|
|
732
|
-
if (!groups.has(key)) groups.set(key, []);
|
|
733
|
-
groups.get(key).push(record);
|
|
734
|
-
}
|
|
735
|
-
const results = [], failed = [];
|
|
736
|
-
for (const [runtimeKey, peers] of groups) {
|
|
737
|
-
try {
|
|
738
|
-
await withRuntimeLifecycle(runtimeKey, async () => {
|
|
739
|
-
const available = peers.filter(record => record.provider.client?.isOpen());
|
|
740
|
-
if (!available.length) {
|
|
741
|
-
if (peers.some(record => record.provider.runtime || record.provider.runtimeStopOwner ||
|
|
742
|
-
codexAppServerOwnedRuntimes.get(runtimeKey)?.provider === record.provider)) {
|
|
743
|
-
throw new Error("Codex's existing control connection is unavailable. Restore it before changing this provider connection.");
|
|
744
|
-
}
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
const request = (record, method, params) => {
|
|
748
|
-
const client = record.provider.client;
|
|
749
|
-
if (!client?.isOpen()) throw new Error("Codex's control connection closed during provider retirement. Restore it and retry.");
|
|
750
|
-
return client.request(method, params, { signal: AbortSignal.timeout(5_000) });
|
|
751
|
-
};
|
|
752
|
-
const selectedHome = normalizeText(toolHomeSource);
|
|
753
|
-
const dedicated = Boolean(selectedHome && peers.some(record =>
|
|
754
|
-
normalizeText(record.providerOptions.toolHomeSource) === selectedHome));
|
|
755
|
-
if (dedicated && peers.some(record => normalizeText(record.providerOptions.toolHomeSource) !== selectedHome)) {
|
|
756
|
-
throw new Error("Codex's provider credential home has other native consumers. Close its native terminal/helper and retry; unrelated runtimes remain running.");
|
|
757
|
-
}
|
|
758
|
-
const inspector = available[0], candidates = new Set(), seen = new Set();
|
|
759
|
-
let cursor;
|
|
760
|
-
do {
|
|
761
|
-
const page = await request(inspector, "thread/loaded/list", { limit: 100, ...(cursor ? { cursor } : {}) });
|
|
762
|
-
if (!Array.isArray(page?.data)) throw new Error("Codex returned an invalid loaded-thread inventory. Restore its connection before retrying.");
|
|
763
|
-
for (const id of page.data) {
|
|
764
|
-
if (typeof id !== "string" || !id) throw new Error("Codex returned an invalid loaded-thread identity.");
|
|
765
|
-
candidates.add(id);
|
|
766
|
-
}
|
|
767
|
-
cursor = page.nextCursor;
|
|
768
|
-
if (cursor && (typeof cursor !== "string" || seen.has(cursor) || seen.size >= 100)) throw new Error("Codex's loaded-thread inventory did not finish.");
|
|
769
|
-
if (cursor) seen.add(cursor);
|
|
770
|
-
} while (cursor);
|
|
771
|
-
for (const { provider } of peers) for (const [id, binding] of provider.threadEnvironments || []) {
|
|
772
|
-
if (dedicated || binding.params?.modelProvider === modelProviderId) candidates.add(id);
|
|
773
|
-
}
|
|
774
|
-
const threadIds = [];
|
|
775
|
-
for (const id of candidates) {
|
|
776
|
-
const { thread } = await request(inspector, "thread/read", { threadId: id, includeTurns: false });
|
|
777
|
-
if (!thread || thread.id !== id || typeof thread.modelProvider !== "string" || !thread.modelProvider ||
|
|
778
|
-
!(typeof thread.status === "string" ? thread.status : thread.status?.type)) {
|
|
779
|
-
throw new Error("Codex returned an untrustworthy thread/provider inventory. Restore its connection before changing credentials.");
|
|
780
|
-
}
|
|
781
|
-
if (dedicated && thread.modelProvider !== modelProviderId) {
|
|
782
|
-
throw new Error("Codex's provider credential home retains another provider thread. Close its native terminal/helper and retry; unrelated threads remain running.");
|
|
783
|
-
}
|
|
784
|
-
if (thread.modelProvider === modelProviderId) threadIds.push(id);
|
|
785
|
-
}
|
|
786
|
-
if (!threadIds.length && !dedicated) return;
|
|
787
|
-
const error = Object.assign(new Error("This Codex provider connection changed. Work is stopped; use Send or Resume after reconnecting."), {
|
|
788
|
-
code: "codex_provider_connection_changed"
|
|
789
|
-
});
|
|
790
|
-
for (const { provider } of peers) for (const threadId of threadIds) provider.conversationRuntime?.invalidateThread(threadId);
|
|
791
|
-
await Promise.allSettled(peers.flatMap(({ provider }) => threadIds
|
|
792
|
-
.map(id => provider.threadEnvironmentTasks?.get(id)).filter(Boolean)));
|
|
793
|
-
// The same observation journal retains selected stop ownership. Peers
|
|
794
|
-
// with other threads must keep their admission and active work intact.
|
|
795
|
-
for (const record of available) await record.observation?.prepare({ provider: record.provider, error, threadIds });
|
|
796
|
-
for (const threadId of threadIds) {
|
|
797
|
-
await inspector.provider.stopThreadForObservationLoss(threadId);
|
|
798
|
-
for (const record of available) {
|
|
799
|
-
const detached = await request(record, "thread/unsubscribe", { threadId });
|
|
800
|
-
if (!["unsubscribed", "notSubscribed", "notLoaded"].includes(detached?.status)) throw new Error("Codex did not confirm selected-thread detachment.");
|
|
801
|
-
}
|
|
802
|
-
const { thread } = await request(inspector, "thread/read", { threadId, includeTurns: false });
|
|
803
|
-
if ((typeof thread?.status === "string" ? thread.status : thread?.status?.type) !== "notLoaded") {
|
|
804
|
-
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.");
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
if (dedicated) {
|
|
808
|
-
// A curated default also lives in this exact isolated helper's
|
|
809
|
-
// native config cache. Reuse verified retirement only after proving
|
|
810
|
-
// every loaded/cached thread belongs to the selected provider.
|
|
811
|
-
const owned = codexAppServerOwnedRuntimes.get(runtimeKey);
|
|
812
|
-
if (!owned || !peers.some(record => record.provider === owned.provider)) {
|
|
813
|
-
throw new Error("Codex's dedicated provider runtime ownership cannot be verified. Restore its control connection and retry.");
|
|
814
|
-
}
|
|
815
|
-
await owned.scope.stopOwnedRuntime(owned, { cached: false, preserveProcessExitProof: true, requireVerifiedExit: true });
|
|
816
|
-
}
|
|
817
|
-
for (const record of available) await record.observation?.complete({ provider: record.provider, error, threadIds, sharedStop: dedicated });
|
|
818
|
-
if (dedicated) for (const record of peers) record.scope.closeProvider(record.providerKey);
|
|
819
|
-
results.push({ runtimeKey, threadIds, stopped: true, sharedProcessRetained: !dedicated,
|
|
820
|
-
...(dedicated ? { dedicatedRuntimeRetired: true } : {}) });
|
|
821
|
-
});
|
|
822
|
-
} catch (error) {
|
|
823
|
-
failed.push({ runtimeKey, error: error.message, retryable: true });
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return { ok: failed.length === 0, failed, results, providerCount: results.length, stopped: results.reduce((sum, result) => sum + result.threadIds.length, 0) };
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// A runtime with a running turn keeps the credentials it started with until
|
|
830
|
-
// that turn ends. Its active goals are paused so it starts no further turn;
|
|
831
|
-
// retireStaleRuntime stops it once it is idle. Any other runtime returns
|
|
832
|
-
// null and stops at once, like an idle one.
|
|
833
|
-
async function deferBusyRuntime(runtimeKey, peers) {
|
|
834
|
-
let running;
|
|
835
|
-
try {
|
|
836
|
-
running = await runningNativeThreads(peers);
|
|
837
|
-
} catch (error) {
|
|
838
|
-
// An unreadable runtime cannot show a running turn.
|
|
839
|
-
debugLog("appServerRuntime.staleInspection.error", { error: debugError(error), runtimeKey });
|
|
840
|
-
return null;
|
|
841
|
-
}
|
|
842
|
-
// Nor can one whose control connection closed while it was read.
|
|
843
|
-
const client = peers.find(record => record.provider.client?.isOpen?.())?.provider.client;
|
|
844
|
-
if (!running.length || !client) return null;
|
|
845
|
-
const pausedGoals = [];
|
|
846
|
-
for (const threadId of running) {
|
|
847
|
-
try {
|
|
848
|
-
const { goal } = await nativeRequest(client, "thread/goal/get", { threadId });
|
|
849
|
-
if (goal?.status !== "active") continue;
|
|
850
|
-
const result = await nativeRequest(client, "thread/goal/set", { threadId, status: "paused" });
|
|
851
|
-
if (result?.goal?.status !== "paused") throw new Error("Codex did not confirm that the goal was paused.");
|
|
852
|
-
pausedGoals.push(threadId);
|
|
853
|
-
} catch (error) {
|
|
854
|
-
debugLog("appServerRuntime.staleGoalPause.error", { error: debugError(error), runtimeKey, threadId });
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
for (const peer of peers) peer.provider.markStale?.();
|
|
858
|
-
const stale = {
|
|
859
|
-
runtimeKey, threadIds: running, pausedGoals, retiring: null,
|
|
860
|
-
providerKeys: peers.map(peer => peer.providerKey)
|
|
861
|
-
};
|
|
862
|
-
codexAppServerStaleRuntimes.set(runtimeKey, stale);
|
|
863
|
-
return stale;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
509
|
// Stops a stale runtime once no loaded thread is running, through the same
|
|
867
510
|
// verified stop an idle runtime gets at the account change.
|
|
868
511
|
function retireStaleRuntime(runtimeKey = "") {
|
|
@@ -873,7 +516,7 @@ export function createCodexAppServerProviderOwner({
|
|
|
873
516
|
const peers = runtimePeers(runtimeKey);
|
|
874
517
|
const running = await runningNativeThreads(peers).catch(() => []);
|
|
875
518
|
// A Temporary turn or goal holding a borrowed provider also keeps it.
|
|
876
|
-
if (running.length || peers.some(peer => peer.scope.isHeld
|
|
519
|
+
if (running.length || peers.some(peer => peer.scope.isHeld(peer.providerKey))) {
|
|
877
520
|
return { ...stale, busy: true, stopped: false };
|
|
878
521
|
}
|
|
879
522
|
const owned = codexAppServerOwnedRuntimes.get(runtimeKey);
|
|
@@ -897,206 +540,6 @@ export function createCodexAppServerProviderOwner({
|
|
|
897
540
|
return retiring;
|
|
898
541
|
}
|
|
899
542
|
|
|
900
|
-
async function invalidateRuntimes({
|
|
901
|
-
deferBusy = false,
|
|
902
|
-
includeOwned = false,
|
|
903
|
-
requireVerifiedExit = false,
|
|
904
|
-
stopOwnedRuntimes = false,
|
|
905
|
-
toolHomeSource = "",
|
|
906
|
-
modelProviderId = ""
|
|
907
|
-
} = {}) {
|
|
908
|
-
if (modelProviderId) return withLifecycle(() => invalidateProviderThreads(modelProviderId, includeOwned, toolHomeSource));
|
|
909
|
-
codexAppServerChatModelCatalog = null;
|
|
910
|
-
const normalizedToolHomeSource = normalizeText(toolHomeSource);
|
|
911
|
-
const ownedRecordsByProvider = new Map(
|
|
912
|
-
[...codexAppServerOwnedRuntimes.values()].filter(inScope).map((record) => [record.provider, record])
|
|
913
|
-
);
|
|
914
|
-
const targets = [...providerRecords()]
|
|
915
|
-
.filter(record => inScope(record) && (!normalizedToolHomeSource ||
|
|
916
|
-
normalizeText(record.providerOptions.toolHomeSource) === normalizedToolHomeSource))
|
|
917
|
-
.map(record => ({
|
|
918
|
-
...record,
|
|
919
|
-
kind: "cached",
|
|
920
|
-
record: ownedRecordsByProvider.get(record.provider) || null
|
|
921
|
-
}));
|
|
922
|
-
const targetedProviders = new Set(targets.map((target) => target.provider));
|
|
923
|
-
if (includeOwned) {
|
|
924
|
-
for (const record of codexAppServerOwnedRuntimes.values()) {
|
|
925
|
-
if (!inScope(record) || targetedProviders.has(record.provider)) {
|
|
926
|
-
continue;
|
|
927
|
-
}
|
|
928
|
-
const recordToolHomeSource = normalizeText(
|
|
929
|
-
record.providerOptions?.toolHomeSource
|
|
930
|
-
);
|
|
931
|
-
if (
|
|
932
|
-
normalizedToolHomeSource &&
|
|
933
|
-
recordToolHomeSource !== normalizedToolHomeSource
|
|
934
|
-
) {
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
targets.push({
|
|
938
|
-
...record,
|
|
939
|
-
kind: "owned",
|
|
940
|
-
provider: record.provider,
|
|
941
|
-
providerKey: record.providerKey,
|
|
942
|
-
record
|
|
943
|
-
});
|
|
944
|
-
targetedProviders.add(record.provider);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
targets.sort((left, right) => Number(Boolean(left.record)) - Number(Boolean(right.record)));
|
|
948
|
-
const failed = [];
|
|
949
|
-
const results = [];
|
|
950
|
-
const deferred = [];
|
|
951
|
-
if (deferBusy) {
|
|
952
|
-
const groups = new Map();
|
|
953
|
-
for (const target of targets) {
|
|
954
|
-
const runtimeKey = codexAppServerOwnedRuntimeKey(target.providerKey, target.providerOptions);
|
|
955
|
-
if (!groups.has(runtimeKey)) groups.set(runtimeKey, []);
|
|
956
|
-
groups.get(runtimeKey).push(target);
|
|
957
|
-
}
|
|
958
|
-
for (const [runtimeKey, peers] of groups) {
|
|
959
|
-
if (codexAppServerStaleRuntimes.has(runtimeKey)) {
|
|
960
|
-
deferred.push(codexAppServerStaleRuntimes.get(runtimeKey));
|
|
961
|
-
} else {
|
|
962
|
-
const stale = await withRuntimeLifecycle(runtimeKey, () => deferBusyRuntime(runtimeKey, peers));
|
|
963
|
-
if (!stale) continue;
|
|
964
|
-
deferred.push(stale);
|
|
965
|
-
}
|
|
966
|
-
for (const peer of peers) {
|
|
967
|
-
targets.splice(targets.indexOf(peer), 1);
|
|
968
|
-
results.push({ providerKey: peer.providerKey, deferred: true, stopped: false });
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
for (const target of targets) {
|
|
973
|
-
if (!target.resources?.invalidation) continue;
|
|
974
|
-
const error = Object.assign(new Error("Codex's native runtime was invalidated."), {
|
|
975
|
-
code: "codex_runtime_invalidated"
|
|
976
|
-
});
|
|
977
|
-
target.invalidation = { error };
|
|
978
|
-
try {
|
|
979
|
-
if (await target.resources.invalidation.prepare({ provider: target.provider, error }) === false) {
|
|
980
|
-
delete target.invalidation;
|
|
981
|
-
}
|
|
982
|
-
} catch (preparationError) {
|
|
983
|
-
target.invalidation.preparationError = preparationError;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
for (const target of targets) {
|
|
987
|
-
const preserveProcessExitProof = target.preserveProcessExitProof === true;
|
|
988
|
-
try {
|
|
989
|
-
const result = stopOwnedRuntimes && target.record
|
|
990
|
-
? await target.scope.stopOwnedRuntime({
|
|
991
|
-
...(target.record || {}),
|
|
992
|
-
provider: target.provider,
|
|
993
|
-
providerKey: target.providerKey
|
|
994
|
-
}, {
|
|
995
|
-
cached: target.kind === "cached",
|
|
996
|
-
preserveProcessExitProof,
|
|
997
|
-
requireVerifiedExit
|
|
998
|
-
})
|
|
999
|
-
: await target.scope.stopCachedProvider(target.providerKey, {
|
|
1000
|
-
preserveProcessExitProof,
|
|
1001
|
-
requireStopped: stopOwnedRuntimes && target.record != null
|
|
1002
|
-
});
|
|
1003
|
-
if (
|
|
1004
|
-
requireVerifiedExit &&
|
|
1005
|
-
target.record &&
|
|
1006
|
-
!codexAppServerRuntimeStopWasVerified(result)
|
|
1007
|
-
) {
|
|
1008
|
-
throw Object.assign(new Error("Codex app-server process exit could not be verified."), {
|
|
1009
|
-
code: exitUnverifiedCode, providerKey: target.providerKey, retryable: true
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
target.stopResult = result;
|
|
1013
|
-
results.push(result);
|
|
1014
|
-
} catch (error) {
|
|
1015
|
-
target.stopError = error;
|
|
1016
|
-
failed.push({
|
|
1017
|
-
code: normalizeText(error?.code),
|
|
1018
|
-
error: String(error?.message || "Codex app-server runtime invalidation failed."),
|
|
1019
|
-
providerKey: target.providerKey,
|
|
1020
|
-
retryable: error?.retryable === true
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
for (const target of targets) {
|
|
1025
|
-
if (!target.invalidation) continue;
|
|
1026
|
-
const runtimeKey = codexAppServerOwnedRuntimeKey(target.providerKey, target.providerOptions);
|
|
1027
|
-
const peers = targets.filter(peer =>
|
|
1028
|
-
codexAppServerOwnedRuntimeKey(peer.providerKey, peer.providerOptions) === runtimeKey);
|
|
1029
|
-
const proof = peers.find(peer => codexAppServerRuntimeStopWasVerified(peer.stopResult));
|
|
1030
|
-
const stopped = proof
|
|
1031
|
-
? { status: "fulfilled", value: proof.stopResult }
|
|
1032
|
-
: { status: "rejected", reason: peers.find(peer => peer.stopError)?.stopError ||
|
|
1033
|
-
Object.assign(new Error("Codex app-server process exit could not be verified."), {
|
|
1034
|
-
code: exitUnverifiedCode, providerKey: target.providerKey, retryable: true
|
|
1035
|
-
}) };
|
|
1036
|
-
try {
|
|
1037
|
-
await target.resources.invalidation.complete({
|
|
1038
|
-
provider: target.provider, ...target.invalidation, stopped
|
|
1039
|
-
});
|
|
1040
|
-
if (target.invalidation.preparationError) throw target.invalidation.preparationError;
|
|
1041
|
-
if (stopped.status === "rejected") throw stopped.reason;
|
|
1042
|
-
} catch (error) {
|
|
1043
|
-
failed.push({
|
|
1044
|
-
code: normalizeText(error?.code),
|
|
1045
|
-
error: String(error?.message || "Codex app-server invalidation could not be recorded."),
|
|
1046
|
-
providerKey: target.providerKey,
|
|
1047
|
-
retryable: true
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
const stopped = results.filter((result) => result.stopped).length;
|
|
1052
|
-
return {
|
|
1053
|
-
deferred: deferred.map(({ runtimeKey, providerKeys, threadIds, pausedGoals }) =>
|
|
1054
|
-
({ runtimeKey, providerKeys: [...providerKeys], threadIds: [...threadIds], pausedGoals: [...pausedGoals] })),
|
|
1055
|
-
failed,
|
|
1056
|
-
ok: failed.length === 0,
|
|
1057
|
-
providerCount: targets.length + deferred.reduce((sum, stale) => sum + stale.providerKeys.length, 0),
|
|
1058
|
-
results,
|
|
1059
|
-
stopped
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
async function suspendUnobservedProvider(record, error) {
|
|
1064
|
-
const targets = [record];
|
|
1065
|
-
let sharedStop = false;
|
|
1066
|
-
try {
|
|
1067
|
-
const threads = await record.observation.prepare({ provider: record.provider, error });
|
|
1068
|
-
if (!threads.size) throw new Error("The unobserved Codex thread could not be identified.");
|
|
1069
|
-
for (const [threadId, turnId] of threads) {
|
|
1070
|
-
await record.provider.stopThreadForObservationLoss(threadId, turnId);
|
|
1071
|
-
}
|
|
1072
|
-
} catch {
|
|
1073
|
-
sharedStop = true;
|
|
1074
|
-
const runtimeKey = codexAppServerOwnedRuntimeKey(record.providerKey, record.providerOptions);
|
|
1075
|
-
for (const peer of providerRecords()) {
|
|
1076
|
-
if (peer !== record && codexAppServerOwnedRuntimeKey(peer.providerKey, peer.providerOptions) === runtimeKey) {
|
|
1077
|
-
peer.provider.observationFailure = error;
|
|
1078
|
-
targets.push(peer);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
// Preserve the original ordering and failure policy: attempt every durable
|
|
1082
|
-
// barrier before native stop; a storage failure cannot leave work running.
|
|
1083
|
-
for (const target of targets) {
|
|
1084
|
-
try {
|
|
1085
|
-
await target.observation.barrier({ provider: target.provider, error });
|
|
1086
|
-
} catch (failure) {
|
|
1087
|
-
target.scope.reportRecoveryEvent(target, "persistenceFailed", failure);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
const owned = codexAppServerOwnedRuntimes.get(runtimeKey) || record;
|
|
1091
|
-
await owned.scope.stopOwnedRuntime(owned, { preserveProcessExitProof: true, requireVerifiedExit: true });
|
|
1092
|
-
}
|
|
1093
|
-
for (const target of targets) {
|
|
1094
|
-
// Each adapter retains its original persistence/publication order. It can
|
|
1095
|
-
// release this provider only after its stopped state is durable.
|
|
1096
|
-
await target.observation.complete({ provider: target.provider, error, sharedStop });
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
543
|
function reportRecoveryEvent(record, event, error) {
|
|
1101
544
|
onRecoveryEvent(record.owner, event, error);
|
|
1102
545
|
}
|
|
@@ -1107,7 +550,7 @@ export function createCodexAppServerProviderOwner({
|
|
|
1107
550
|
assertOpen();
|
|
1108
551
|
if (!record.observationStop || record.observationStop.error !== error) {
|
|
1109
552
|
reportRecoveryEvent(record, "lost", error);
|
|
1110
|
-
const stop = { error, promise: suspendUnobservedProvider(record, error) };
|
|
553
|
+
const stop = { error, promise: invalidation.suspendUnobservedProvider(record, error) };
|
|
1111
554
|
record.observationStop = stop;
|
|
1112
555
|
const tracked = stop.promise.catch(failure => {
|
|
1113
556
|
reportRecoveryEvent(record, "stopFailed", failure);
|
|
@@ -1125,294 +568,6 @@ export function createCodexAppServerProviderOwner({
|
|
|
1125
568
|
return record && record.provider === provider ? record.observationStop?.promise || null : null;
|
|
1126
569
|
}
|
|
1127
570
|
|
|
1128
|
-
async function stopProvidersForProject({
|
|
1129
|
-
preserveProcessExitProof = false,
|
|
1130
|
-
projectContextRoot = "",
|
|
1131
|
-
reason = "",
|
|
1132
|
-
} = {}, { managed, keyFields, helperThreads, sessionKeyPrefix = "" }) {
|
|
1133
|
-
const normalizedProjectContextRoot = normalizeText(projectContextRoot);
|
|
1134
|
-
if (!normalizedProjectContextRoot) {
|
|
1135
|
-
return {
|
|
1136
|
-
failed: [],
|
|
1137
|
-
ok: true,
|
|
1138
|
-
projectContextRoot: normalizedProjectContextRoot,
|
|
1139
|
-
providerCount: 0,
|
|
1140
|
-
reason: normalizeText(reason),
|
|
1141
|
-
results: [],
|
|
1142
|
-
stopped: 0
|
|
1143
|
-
};
|
|
1144
|
-
}
|
|
1145
|
-
const helperProviders = new Set(
|
|
1146
|
-
helperThreads.records({
|
|
1147
|
-
projectContextRoot: normalizedProjectContextRoot
|
|
1148
|
-
}).map((record) => record.provider)
|
|
1149
|
-
);
|
|
1150
|
-
// Main threads name their project; every other provider of the project's
|
|
1151
|
-
// sessions (Temporary chats, reads) is found by its session key.
|
|
1152
|
-
const providerKeys = [...providers.keys()]
|
|
1153
|
-
.filter((providerKey) => {
|
|
1154
|
-
const current = managed.get(providerKey);
|
|
1155
|
-
return normalizeText(current?.projectContext?.targetRoot) === normalizedProjectContextRoot ||
|
|
1156
|
-
helperProviders.has(providers.get(providerKey)) ||
|
|
1157
|
-
Boolean(sessionKeyPrefix && normalizeText(providerOwners.get(providerKey)?.sessionKey).startsWith(sessionKeyPrefix));
|
|
1158
|
-
});
|
|
1159
|
-
const failed = [];
|
|
1160
|
-
const results = [];
|
|
1161
|
-
for (const providerKey of providerKeys) {
|
|
1162
|
-
try {
|
|
1163
|
-
const fields = keyFields(providerKey);
|
|
1164
|
-
results.push(await stopCachedProvider(providerKey, {
|
|
1165
|
-
preserveProcessExitProof: Boolean(
|
|
1166
|
-
preserveProcessExitProof &&
|
|
1167
|
-
fields.sessionId &&
|
|
1168
|
-
fields.executionMode !== "helper"
|
|
1169
|
-
)
|
|
1170
|
-
}));
|
|
1171
|
-
} catch (error) {
|
|
1172
|
-
failed.push({
|
|
1173
|
-
code: normalizeText(error?.code),
|
|
1174
|
-
error: normalizeText(error?.error || error?.message || error) || runtimeCloseError,
|
|
1175
|
-
providerKey,
|
|
1176
|
-
retryable: error?.retryable === true
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
const stopped = results.filter((result) => result.stopped).length;
|
|
1181
|
-
debugLog("appServerRuntime.closeProject.done", {
|
|
1182
|
-
failedCount: failed.length,
|
|
1183
|
-
providerCount: providerKeys.length,
|
|
1184
|
-
projectContextRoot: normalizedProjectContextRoot,
|
|
1185
|
-
reason: normalizeText(reason),
|
|
1186
|
-
stopped
|
|
1187
|
-
});
|
|
1188
|
-
return {
|
|
1189
|
-
failed,
|
|
1190
|
-
ok: failed.length === 0,
|
|
1191
|
-
projectContextRoot: normalizedProjectContextRoot,
|
|
1192
|
-
providerCount: providerKeys.length,
|
|
1193
|
-
reason: normalizeText(reason),
|
|
1194
|
-
results,
|
|
1195
|
-
stopped
|
|
1196
|
-
};
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
async function stopCachedProvidersForSession(sessionKey = "", options = {}) {
|
|
1200
|
-
if (!sessionKey) {
|
|
1201
|
-
return {
|
|
1202
|
-
failed: [],
|
|
1203
|
-
ok: true,
|
|
1204
|
-
providerCount: 0,
|
|
1205
|
-
results: [],
|
|
1206
|
-
stopped: 0
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
const providerKeys = [...providers.keys()]
|
|
1210
|
-
.filter((providerKey) => (
|
|
1211
|
-
providerOwners.get(providerKey)?.sessionKey === sessionKey
|
|
1212
|
-
));
|
|
1213
|
-
const failed = [];
|
|
1214
|
-
const results = [];
|
|
1215
|
-
for (const providerKey of providerKeys) {
|
|
1216
|
-
try {
|
|
1217
|
-
results.push(await stopCachedProvider(providerKey, options));
|
|
1218
|
-
} catch (error) {
|
|
1219
|
-
failed.push({
|
|
1220
|
-
code: normalizeText(error?.code),
|
|
1221
|
-
error: normalizeText(error?.error || error?.message || error) || runtimeCloseError,
|
|
1222
|
-
providerKey,
|
|
1223
|
-
retryable: error?.retryable === true
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
return {
|
|
1228
|
-
failed,
|
|
1229
|
-
ok: failed.length === 0,
|
|
1230
|
-
providerCount: providerKeys.length,
|
|
1231
|
-
results,
|
|
1232
|
-
stopped: results.filter((result) => result.stopped).length
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
function drainThreadEnvironmentTasks(sessionKey) {
|
|
1237
|
-
return Promise.all([...providers.entries()]
|
|
1238
|
-
.filter(([key]) => providerOwners.get(key)?.sessionKey === sessionKey)
|
|
1239
|
-
.flatMap(([, provider]) => [...(provider.threadEnvironmentTasks?.values() || [])])
|
|
1240
|
-
.map((task) => task.catch(() => null)));
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
async function stopPersistedRuntime({ runtime, parameters }, runtimeDir) {
|
|
1244
|
-
const result = await runtime.stop(parameters);
|
|
1245
|
-
|
|
1246
|
-
return {
|
|
1247
|
-
...result,
|
|
1248
|
-
runtimeDirExists: await directoryExists(runtimeDir),
|
|
1249
|
-
verifiedStopped: result?.stopped === true ||
|
|
1250
|
-
result?.processExitVerified === true ||
|
|
1251
|
-
result?.runtimeDirRemoved === true
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
function sessionExitProof(sessionKey, context) {
|
|
1256
|
-
const { cachedProviders, persistedRuntime } = context;
|
|
1257
|
-
const noRuntimeAcquired = cachedProviders.providerCount === 0 &&
|
|
1258
|
-
!context.hasRuntime &&
|
|
1259
|
-
!context.threadId &&
|
|
1260
|
-
![...ownedRuntimes.values()].some((record) => record.sessionKey === sessionKey);
|
|
1261
|
-
const cachedRuntimeExitVerified = cachedProviders.providerCount > 0 &&
|
|
1262
|
-
cachedProviders.results.length === cachedProviders.providerCount &&
|
|
1263
|
-
cachedProviders.results.every((result) => (
|
|
1264
|
-
result?.stopped === true ||
|
|
1265
|
-
(result?.sessionDetached === true && result?.sharedProcessRetained === true)
|
|
1266
|
-
));
|
|
1267
|
-
const persistedRuntimeExitVerified = persistedRuntime?.verifiedStopped === true ||
|
|
1268
|
-
(
|
|
1269
|
-
persistedRuntime?.sessionDetached === true &&
|
|
1270
|
-
persistedRuntime?.sharedProcessRetained === true
|
|
1271
|
-
);
|
|
1272
|
-
return { noRuntimeAcquired, cachedRuntimeExitVerified, persistedRuntimeExitVerified };
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
async function unsubscribeSessionThread(sessionKey, sessionId, session) {
|
|
1276
|
-
if (!sessionId) {
|
|
1277
|
-
return { ok: true, sessionId, status: "notSubscribed" };
|
|
1278
|
-
}
|
|
1279
|
-
const { providerOptions, workdir, threadId } = session.unsubscribeParameters;
|
|
1280
|
-
if (!threadId) {
|
|
1281
|
-
return { ok: true, providerOptions, sessionId, status: "notSubscribed" };
|
|
1282
|
-
}
|
|
1283
|
-
// The existing record carries the workdir captured in its provider key.
|
|
1284
|
-
// Never prepare new controls while releasing a retained connection.
|
|
1285
|
-
const provider = [...records.values()].find(record =>
|
|
1286
|
-
record.owner.sessionKey === sessionKey && record.owner.workdir === normalizeText(workdir))?.provider;
|
|
1287
|
-
if (!provider || typeof provider.unsubscribeThread !== "function") {
|
|
1288
|
-
return { ok: true, providerOptions, sessionId, status: "notSubscribed" };
|
|
1289
|
-
}
|
|
1290
|
-
const result = await provider.unsubscribeThread(threadId);
|
|
1291
|
-
debugLog("appServerThread.unsubscribe.done", {
|
|
1292
|
-
sessionId,
|
|
1293
|
-
status: normalizeText(result?.status),
|
|
1294
|
-
threadId
|
|
1295
|
-
});
|
|
1296
|
-
return {
|
|
1297
|
-
ok: true,
|
|
1298
|
-
providerOptions,
|
|
1299
|
-
result,
|
|
1300
|
-
sessionId,
|
|
1301
|
-
status: normalizeText(result?.status) || "unsubscribed",
|
|
1302
|
-
threadId
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
async function stopPersistedSessionRuntime(session) {
|
|
1307
|
-
const host = session.persistedRuntimeHost;
|
|
1308
|
-
if (!host) return { stopped: false };
|
|
1309
|
-
return stopPersistedRuntime(host, host.runtimeDir);
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
async function releasePersistedRuntimeProof(runtimeOptions, session) {
|
|
1313
|
-
// Session closure already detached this participant. Its archived runtime
|
|
1314
|
-
// path may now belong to the successor or another live conversation.
|
|
1315
|
-
// Releasing the old proof must not stop or delete that shared process.
|
|
1316
|
-
if (runtimeIsShared("", runtimeOptions)) {
|
|
1317
|
-
return { sharedProcessRetained: true };
|
|
1318
|
-
}
|
|
1319
|
-
const existed = await directoryExists(runtimeOptions.runtimeDir);
|
|
1320
|
-
if (!existed) return { existed };
|
|
1321
|
-
const result = await stopPersistedSessionRuntime(session);
|
|
1322
|
-
return { existed, result };
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
function releasePreparedRuntimeProof(preparation) {
|
|
1326
|
-
return withLifecycle(async () => {
|
|
1327
|
-
const prepared = preparation.read();
|
|
1328
|
-
if (Object.hasOwn(prepared, "value")) return prepared.value;
|
|
1329
|
-
return withRuntimeLifecycle(codexAppServerOwnedRuntimeKey("", prepared.runtimeOptions), async () => {
|
|
1330
|
-
const evidence = await releasePersistedRuntimeProof(prepared.runtimeOptions, prepared);
|
|
1331
|
-
return prepared.complete(evidence);
|
|
1332
|
-
});
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
async function releaseSession(sessionKey, context) {
|
|
1337
|
-
const { sessionId, runOwner, session } = context;
|
|
1338
|
-
if (context.changeover) {
|
|
1339
|
-
const threadId = session.threadId;
|
|
1340
|
-
for (const [key, provider] of providers) {
|
|
1341
|
-
if (!threadId || providerOwners.get(key)?.sessionKey !== sessionKey) continue;
|
|
1342
|
-
await stopThreadBeforeRelease(key, provider, threadId);
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
let exitFailure = null;
|
|
1346
|
-
try {
|
|
1347
|
-
const unsubscribeResult = await unsubscribeSessionThread(sessionKey, sessionId, session);
|
|
1348
|
-
session.providerOptions = unsubscribeResult?.providerOptions || session.providerOptions;
|
|
1349
|
-
} catch (error) {
|
|
1350
|
-
debugLog("appServerThread.unsubscribe.error", {
|
|
1351
|
-
error: debugError(error),
|
|
1352
|
-
sessionId
|
|
1353
|
-
});
|
|
1354
|
-
} finally {
|
|
1355
|
-
await runOwner.notificationQueue.drain(sessionId);
|
|
1356
|
-
const cachedProviders = await stopCachedProvidersForSession(sessionId ? sessionKey : "", {
|
|
1357
|
-
preserveProcessExitProof: context.preserveProcessExitProof,
|
|
1358
|
-
requireStopped: context.requireStopped
|
|
1359
|
-
});
|
|
1360
|
-
if (cachedProviders.ok === false) {
|
|
1361
|
-
debugLog("appServerRuntime.closeSession.cached.error", {
|
|
1362
|
-
failed: cachedProviders.failed,
|
|
1363
|
-
sessionId
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
1366
|
-
let persistedRuntime = null;
|
|
1367
|
-
if (session.exists) {
|
|
1368
|
-
persistedRuntime = cachedProviders.stopped > 0
|
|
1369
|
-
? {
|
|
1370
|
-
stopped: true,
|
|
1371
|
-
verifiedStopped: true
|
|
1372
|
-
}
|
|
1373
|
-
: runtimeIsShared("", session.providerOptions || {})
|
|
1374
|
-
? {
|
|
1375
|
-
sessionDetached: true,
|
|
1376
|
-
sharedProcessRetained: true,
|
|
1377
|
-
stopped: false,
|
|
1378
|
-
verifiedStopped: false
|
|
1379
|
-
}
|
|
1380
|
-
: session.hasRuntime
|
|
1381
|
-
? await stopPersistedSessionRuntime(session)
|
|
1382
|
-
: {
|
|
1383
|
-
stopped: false,
|
|
1384
|
-
verifiedStopped: false
|
|
1385
|
-
};
|
|
1386
|
-
debugLog("appServerRuntime.closeSession.persisted.done", {
|
|
1387
|
-
removed: persistedRuntime?.removed === true,
|
|
1388
|
-
runtimeDirRemoved: persistedRuntime?.runtimeDirRemoved === true,
|
|
1389
|
-
sessionId,
|
|
1390
|
-
stopped: persistedRuntime?.removed === true || persistedRuntime?.runtimeDirRemoved === true
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
if (session.requiresExitProof) {
|
|
1394
|
-
// A fresh session with no acquired native owner needs no process proof.
|
|
1395
|
-
// Losing a cache or recorded runtime does not make it a fresh session.
|
|
1396
|
-
const { noRuntimeAcquired, cachedRuntimeExitVerified, persistedRuntimeExitVerified } =
|
|
1397
|
-
sessionExitProof(sessionKey, {
|
|
1398
|
-
cachedProviders,
|
|
1399
|
-
persistedRuntime,
|
|
1400
|
-
get hasRuntime() { return session.hasRuntime; },
|
|
1401
|
-
get threadId() { return session.threadId; }
|
|
1402
|
-
});
|
|
1403
|
-
if (cachedProviders.failed.length > 0 ||
|
|
1404
|
-
(!noRuntimeAcquired && !cachedRuntimeExitVerified && !persistedRuntimeExitVerified)) {
|
|
1405
|
-
exitFailure = {
|
|
1406
|
-
cachedFailures: cachedProviders.failed,
|
|
1407
|
-
cachedRuntimeExitVerified,
|
|
1408
|
-
persistedRuntime: persistedRuntime || null
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
return { exitFailure };
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
571
|
// A borrowed provider (a read, storage work or a Temporary chat) is not the
|
|
1417
572
|
// session's conversation.
|
|
1418
573
|
async function readPlanUsage(sessionKey) {
|
|
@@ -1437,21 +592,58 @@ export function createCodexAppServerProviderOwner({
|
|
|
1437
592
|
return { status: "unavailable", goal: null };
|
|
1438
593
|
}
|
|
1439
594
|
|
|
595
|
+
function isHeld(providerKey) {
|
|
596
|
+
return (transientReleases.get(normalizeText(providerKey))?.users || 0) > 0;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// The exported ways to reach a provider for real use claim it first.
|
|
600
|
+
function acquireClaimedRuntime(options) {
|
|
601
|
+
claimTransient(options?.providerKey);
|
|
602
|
+
return acquireRuntime(options);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function createClaimedProvider(options) {
|
|
606
|
+
claimTransient(options?.providerKey);
|
|
607
|
+
return createProvider(options);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function claimedProviderForSession(context) {
|
|
611
|
+
claimTransient(context.providerKey);
|
|
612
|
+
return providerForSession(context);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function beginShutdown() {
|
|
616
|
+
closing = true;
|
|
617
|
+
for (const release of transientReleases.values()) clearTimeout(release.timer);
|
|
618
|
+
transientReleases.clear();
|
|
619
|
+
}
|
|
620
|
+
|
|
1440
621
|
const api = Object.freeze({
|
|
1441
622
|
providers, providerOwners, ownedRuntimes, lifecycleTasks, runtimeAcquisitions,
|
|
1442
|
-
isHeld
|
|
623
|
+
isHeld,
|
|
1443
624
|
withLifecycle, runtimeIsShared, rememberOwnedRuntime, forgetOwnedRuntime, attachRuntimeLifecycle,
|
|
1444
|
-
acquireRuntime:
|
|
1445
|
-
createProvider:
|
|
1446
|
-
providerForSession:
|
|
1447
|
-
createNativeProvider, ensureSession, borrowSession, withTransientSession, holdProvider, retireAndCloseProvider, closeProvider,
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
625
|
+
acquireRuntime: acquireClaimedRuntime,
|
|
626
|
+
createProvider: createClaimedProvider,
|
|
627
|
+
providerForSession: claimedProviderForSession,
|
|
628
|
+
createNativeProvider, ensureSession, borrowSession, withTransientSession, holdProvider, retireAndCloseProvider, closeProvider,
|
|
629
|
+
stopOwnedRuntime,
|
|
630
|
+
stopCachedProvider: sessionRelease.stopCachedProvider,
|
|
631
|
+
stopProvider: sessionRelease.stopProvider,
|
|
632
|
+
stopThreadBeforeRelease: sessionRelease.stopThreadBeforeRelease,
|
|
633
|
+
readModelCatalog: modelCatalog.read,
|
|
634
|
+
invalidateRuntimes: invalidation.invalidateRuntimes,
|
|
635
|
+
recover, reportRecoveryEvent, pendingRecovery, readSessionGoal, readPlanUsage,
|
|
636
|
+
stopCachedProvidersForSession: sessionRelease.stopCachedProvidersForSession,
|
|
637
|
+
stopProvidersForProject: sessionRelease.stopProvidersForProject,
|
|
638
|
+
drainThreadEnvironmentTasks: sessionRelease.drainThreadEnvironmentTasks,
|
|
639
|
+
stopPersistedRuntime: sessionRelease.stopPersistedRuntime,
|
|
640
|
+
sessionExitProof: sessionRelease.sessionExitProof,
|
|
641
|
+
unsubscribeSessionThread: sessionRelease.unsubscribeSessionThread,
|
|
642
|
+
releasePersistedRuntimeProof: sessionRelease.releasePersistedRuntimeProof,
|
|
643
|
+
releasePreparedRuntimeProof: sessionRelease.releasePreparedRuntimeProof,
|
|
644
|
+
releaseSession: sessionRelease.releaseSession,
|
|
645
|
+
retireStaleRuntime,
|
|
646
|
+
beginShutdown
|
|
1455
647
|
});
|
|
1456
648
|
return api;
|
|
1457
649
|
}
|