@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,406 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
1
|
+
import { CODEX_APP_SERVER_EXECUTION_MODES, CODEX_APP_SERVER_PROCESS_STATE, CODEX_APP_SERVER_PROVIDER_ID, CODEX_APP_SERVER_TRANSPORT, codexAppServerExecutionMode, codexAppServerHelperWorkspaceDir, codexAppServerIsHelper, codexAppServerRuntimeIdentity, codexAppServerTerminalEnvHash, ensureWritablePrivateDirectory, tailTextFile } from "./codexProcess.js";
|
|
4
2
|
import { CodexAppServerJsonRpcClient } from "./codexClient.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
createCodexAccountReader, codexAuthOutputRequiresReconnect, codexAppServerProjectHookTrustConfig,
|
|
8
|
-
codexAppServerTextHasControlCharacters, CODEX_APP_SERVER_ACCOUNT_ID_MAX_LENGTH
|
|
9
|
-
} from "./codexConfiguration.js";
|
|
10
|
-
import {
|
|
11
|
-
CODEX_APP_SERVER_PROCESS_STATE, CODEX_APP_SERVER_TRANSPORT,
|
|
12
|
-
codexAppServerExecutionMode, codexAppServerHelperWorkspaceDir, codexAppServerIsHelper,
|
|
13
|
-
codexAppServerRuntimeIdentity, codexAppServerTerminalEnvHash,
|
|
14
|
-
ensureWritablePrivateDirectory, normalizeCodexAppServerTerminalEnv, tailTextFile
|
|
15
|
-
} from "./codexProcess.js";
|
|
16
|
-
import { assertCodexAuthPreflightReady } from "./codexAuthPreflight.js";
|
|
3
|
+
import { codexAppServerTextHasControlCharacters } from "./codexConfiguration.js";
|
|
4
|
+
import { codexAuthOutputRequiresReconnect, createCodexAccountReader } from "./codexAccountReader.js";
|
|
17
5
|
import { codexAppServerModelCatalogError } from "./codexModelCatalog.js";
|
|
18
|
-
import { prepareCodexHistory } from "./codexHistoryAdapter.js";
|
|
19
6
|
import { createCodexConversationAdapter } from "./providers/codex.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
7
|
+
import { codexAppServerHelperRequestError, createCodexProviderAuth } from "./codexProviderAuth.js";
|
|
8
|
+
import { codexAppServerThreadRequestParams, createCodexProviderThreads, keepCodexThreadParams, resumeCodexThreadAsIs } from "./codexProviderThreads.js";
|
|
9
|
+
import { createCodexProviderInventories } from "./codexProviderInventories.js";
|
|
10
|
+
import { createCodexProviderTurns } from "./codexProviderTurns.js";
|
|
11
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
23
12
|
import { normalizeNullableText as normalizeAgentText } from "./normalize.js";
|
|
24
|
-
import
|
|
25
|
-
codexAppServerProviderThreadTurns,
|
|
26
|
-
codexAppServerProviderTurnItems,
|
|
27
|
-
codexAppServerProviderTurnClientIds,
|
|
28
|
-
codexAppServerThreadRawValue,
|
|
29
|
-
codexAppServerErrorText
|
|
30
|
-
} from "./codexEvents.js";
|
|
31
|
-
|
|
32
|
-
const CODEX_APP_SERVER_PROVIDER_ID = "codex_app_server";
|
|
33
|
-
const CODEX_APP_SERVER_EXECUTION_MODES = Object.freeze({ HELPER: "helper", INTERACTIVE: "interactive" });
|
|
34
|
-
const codexAppServerHelperAuthError = createCodexAccountReader().error;
|
|
13
|
+
import path from "node:path";
|
|
35
14
|
|
|
36
|
-
const CODEX_APP_SERVER_INVALID_REQUEST_CODE = -32600;
|
|
37
|
-
const CODEX_APP_SERVER_MODEL_CATALOG_PAGE_LIMIT = 100;
|
|
38
|
-
const CODEX_APP_SERVER_MODEL_CATALOG_MAX_PAGES = 100;
|
|
39
|
-
const CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRIES = 1000;
|
|
40
|
-
const CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRY_BYTES = 32 * 1024;
|
|
41
|
-
const CODEX_APP_SERVER_MODEL_CATALOG_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
42
|
-
const CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH = 512;
|
|
43
|
-
const CODEX_APP_SERVER_THREAD_INVENTORY_PAGE_LIMIT = 100;
|
|
44
|
-
const CODEX_APP_SERVER_THREAD_INVENTORY_MAX_PAGES = 100;
|
|
45
|
-
export const CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT = 1000;
|
|
46
|
-
export const CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH = 512;
|
|
47
15
|
const CODEX_APP_SERVER_HELPER_MAX_MESSAGE_BYTES = 4 * 1024 * 1024;
|
|
48
|
-
const CODEX_APP_SERVER_SERVER_INFO_USER_AGENT_MAX_LENGTH = 512;
|
|
49
|
-
const CODEX_APP_SERVER_CHATGPT_REFRESH_METHOD = "account/chatgptAuthTokens/refresh";
|
|
50
|
-
const CODEX_NATIVE_STORAGE_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer", "subAgent", "subAgentReview",
|
|
51
|
-
"subAgentCompact", "subAgentThreadSpawn", "subAgentOther", "unknown"];
|
|
52
|
-
|
|
53
|
-
function codexAppServerRequestIsInvalid(error = null, method = "") {
|
|
54
|
-
const expectedMethod = normalizeAgentText(method);
|
|
55
|
-
const actualMethod = normalizeAgentText(error?.method);
|
|
56
|
-
return Number(error?.code) === CODEX_APP_SERVER_INVALID_REQUEST_CODE &&
|
|
57
|
-
(!expectedMethod || !actualMethod || actualMethod === expectedMethod);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function codexAppServerThreadIsMissing(error = null, threadId = "") {
|
|
61
|
-
const normalizedThreadId = normalizeAgentText(threadId || "").toLowerCase();
|
|
62
|
-
if (
|
|
63
|
-
!normalizedThreadId ||
|
|
64
|
-
!codexAppServerRequestIsInvalid(error, "thread/read")
|
|
65
|
-
) {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
// Codex also uses -32600 for unrelated invalid requests, so recovery must
|
|
69
|
-
// match both the missing-thread wording and the exact durable thread id.
|
|
70
|
-
const message = normalizeAgentText(error?.message || "").toLowerCase();
|
|
71
|
-
return [
|
|
72
|
-
`thread not loaded: ${normalizedThreadId}`,
|
|
73
|
-
`thread ${normalizedThreadId} not found`,
|
|
74
|
-
`no rollout found for thread id ${normalizedThreadId}`
|
|
75
|
-
].includes(message);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function retireCodexConversationHistory(provider, binding, {
|
|
79
|
-
toolHomeSource, beforeDelete, signal, errorPrefix = ""
|
|
80
|
-
} = {}) {
|
|
81
|
-
const inspect = async () => {
|
|
82
|
-
const ids = [...new Set([binding.conversationId, ...await provider.listThreadDescendants(binding.conversationId)])].sort();
|
|
83
|
-
const records = [];
|
|
84
|
-
for (const conversationId of ids) {
|
|
85
|
-
let thread;
|
|
86
|
-
try { thread = codexAppServerThreadRawValue(await provider.readThreadStatus(conversationId)); }
|
|
87
|
-
catch (error) {
|
|
88
|
-
// Native thread/read also says "not loaded" after deletion. Only
|
|
89
|
-
// an exhaustive native inventory can distinguish absence from an
|
|
90
|
-
// existing unloaded thread, including one moved to another cwd.
|
|
91
|
-
if (codexAppServerThreadIsMissing(error, conversationId) &&
|
|
92
|
-
!await provider.nativeThreadExists(conversationId, { signal })) continue;
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
-
if (thread.id !== conversationId || thread.cwd !== binding.workdir ||
|
|
96
|
-
!["idle", "notLoaded"].includes(thread.status?.type)) {
|
|
97
|
-
throw new Error("Codex retirement requires an idle native family in the exact saved directory.");
|
|
98
|
-
}
|
|
99
|
-
if (thread.historyMode !== "paginated") {
|
|
100
|
-
throw Object.assign(new Error("Codex native retirement requires paginated history. This unsupported conversation needs operator cleanup."),
|
|
101
|
-
{ code: `${errorPrefix}codex_paginated_history_required` });
|
|
102
|
-
}
|
|
103
|
-
const relative = path.isAbsolute(thread.path || "") && toolHomeSource
|
|
104
|
-
? path.relative(path.join(toolHomeSource, ".codex"), thread.path) : "";
|
|
105
|
-
if (thread.path && (!/^(?:sessions|archived_sessions)\//u.test(relative) || !/\.jsonl(?:\.zst)?$/u.test(thread.path))) {
|
|
106
|
-
throw new Error("Codex returned an unsafe or unknown rollout path.");
|
|
107
|
-
}
|
|
108
|
-
const files = [];
|
|
109
|
-
const plainPath = thread.path?.replace(/\.zst$/u, "");
|
|
110
|
-
for (const file of plainPath ? [plainPath, `${plainPath}.zst`] : []) {
|
|
111
|
-
try {
|
|
112
|
-
const info = await lstat(file);
|
|
113
|
-
if (!info.isFile() || info.isSymbolicLink() || await realpath(file) !== path.resolve(file)) {
|
|
114
|
-
throw new Error("Codex rollout is not a regular file at its exact native path.");
|
|
115
|
-
}
|
|
116
|
-
files.push({ path: file, inode: info.ino, size: info.size, modified: info.mtimeMs });
|
|
117
|
-
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
118
|
-
}
|
|
119
|
-
records.push({ conversationId, workdir: thread.cwd, historyMode: thread.historyMode, ...(files[0] || {}), files,
|
|
120
|
-
nativePath: thread.path ?? null, status: thread.status.type,
|
|
121
|
-
...(Number.isFinite(thread.createdAt) ? { createdAt: new Date(thread.createdAt * 1000).toISOString() } : {}),
|
|
122
|
-
...(Number.isFinite(thread.updatedAt) ? { updatedAt: new Date(thread.updatedAt * 1000).toISOString() } : {}) });
|
|
123
|
-
}
|
|
124
|
-
return records;
|
|
125
|
-
};
|
|
126
|
-
return retireNativeConversation({ binding, inspect, beforeDelete,
|
|
127
|
-
exportConversation: (id, onRecord) => provider.exportThreadHistory(id, onRecord, { signal }),
|
|
128
|
-
remove: () => provider.deleteThread(binding.conversationId) });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Native Codex0.159.1 keeps goals in its separate goals_1.sqlite store.
|
|
132
|
-
// Return its original SQL fields; the host owns stopped conversion eligibility,
|
|
133
|
-
// including rejecting active/blocked/limited goals and resolving SQLite home.
|
|
134
|
-
async function readCodexNativeGoal(databasePath, { threadId, signal } = {}) {
|
|
135
|
-
if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/u.test(threadId || "") ||
|
|
136
|
-
path.basename(databasePath || "") !== "goals_1.sqlite") {
|
|
137
|
-
throw new TypeError("Codex goal inspection requires its exact thread ID and supported goals_1.sqlite path.");
|
|
138
|
-
}
|
|
139
|
-
return readStoppedNativeDatabase(databasePath, database => {
|
|
140
|
-
const columns = database.prepare("PRAGMA table_info(thread_goals)").all();
|
|
141
|
-
const text = ["thread_id", "goal_id", "objective", "status"];
|
|
142
|
-
const integers = ["token_budget", "tokens_used", "time_used_seconds", "created_at_ms", "updated_at_ms"];
|
|
143
|
-
if (!columns.some(column => column.name === "thread_id" && column.pk === 1) ||
|
|
144
|
-
text.some(name => !columns.some(column => column.name === name && column.type === "TEXT")) ||
|
|
145
|
-
integers.some(name => !columns.some(column => column.name === name && column.type === "INTEGER"))) {
|
|
146
|
-
throw new Error("Codex's saved native goal schema is unsupported; no history was changed.");
|
|
147
|
-
}
|
|
148
|
-
const rows = database.prepare(`SELECT thread_id, goal_id, objective, status, token_budget,
|
|
149
|
-
tokens_used, time_used_seconds, created_at_ms, updated_at_ms
|
|
150
|
-
FROM thread_goals WHERE thread_id = ? LIMIT 2`).all(threadId);
|
|
151
|
-
if (!rows.length) return null;
|
|
152
|
-
const row = rows[0];
|
|
153
|
-
if (rows.length !== 1 || row.thread_id !== threadId ||
|
|
154
|
-
!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/u.test(row.goal_id || "") ||
|
|
155
|
-
typeof row.objective !== "string" ||
|
|
156
|
-
!["active", "paused", "blocked", "usage_limited", "budget_limited", "complete"].includes(row.status) ||
|
|
157
|
-
row.token_budget !== null && (!Number.isSafeInteger(row.token_budget) || row.token_budget < 0) ||
|
|
158
|
-
["tokens_used", "time_used_seconds", "created_at_ms", "updated_at_ms"].some(key =>
|
|
159
|
-
!Number.isSafeInteger(row[key]) || row[key] < 0)) {
|
|
160
|
-
throw new Error("Codex's saved native goal is unsupported or ambiguous; no history was changed.");
|
|
161
|
-
}
|
|
162
|
-
return row;
|
|
163
|
-
}, { signal });
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function codexAppServerThreadHasReadableHistory(provider = null, threadId = "") {
|
|
167
|
-
const normalizedThreadId = normalizeAgentText(threadId);
|
|
168
|
-
if (!normalizedThreadId || typeof provider?.readThread !== "function") {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
try {
|
|
172
|
-
await provider.readThread(normalizedThreadId);
|
|
173
|
-
return true;
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (codexAppServerRequestIsInvalid(error, "thread/read")) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// The caller has already checked the requested thread against its current
|
|
183
|
-
// conversation. History can confirm this exact authored input, never infer it
|
|
184
|
-
// from an assistant item or from a failed read.
|
|
185
|
-
async function inspectCodexAppServerMessageAdmission({ provider, threadId, messageId } = {}) {
|
|
186
|
-
try {
|
|
187
|
-
const thread = await provider.readThread(threadId);
|
|
188
|
-
const accepted = codexAppServerProviderThreadTurns(thread).find((turn) =>
|
|
189
|
-
codexAppServerProviderTurnItems(turn).some((item) => item.type === "userMessage" &&
|
|
190
|
-
codexAppServerProviderTurnClientIds({ items: [item] }).includes(messageId)));
|
|
191
|
-
return { ok: true, admission: accepted ? "accepted" : "unknown", messageId, threadId,
|
|
192
|
-
turnId: normalizeAgentText(accepted?.id) };
|
|
193
|
-
} catch {
|
|
194
|
-
return { ok: true, admission: "unknown", messageId, threadId };
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
16
|
|
|
198
|
-
|
|
199
|
-
// proof and durable cleanup receipt.
|
|
200
|
-
async function deleteCodexAppServerThread({ provider, threadId } = {}) {
|
|
201
|
-
const result = await provider.deleteThread(threadId);
|
|
202
|
-
if (!isRecord(result)) {
|
|
203
|
-
const error = new Error("Codex app-server returned an invalid thread deletion result.");
|
|
204
|
-
error.code = "codex_helper_thread_delete_unconfirmed";
|
|
205
|
-
throw error;
|
|
206
|
-
}
|
|
207
|
-
return result;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// The caller persists cleanup ownership before this operation and removes that
|
|
211
|
-
// receipt only after success. Native failure details remain local to that owner.
|
|
212
|
-
async function deleteCodexAppServerHelperThread({ provider, threadId, turnId = "" } = {}) {
|
|
213
|
-
let interruptError = null;
|
|
214
|
-
if (turnId) {
|
|
215
|
-
if (typeof provider?.interruptTurn === "function") {
|
|
216
|
-
try {
|
|
217
|
-
await provider.interruptTurn(threadId, turnId);
|
|
218
|
-
} catch (error) {
|
|
219
|
-
interruptError = error;
|
|
220
|
-
}
|
|
221
|
-
} else {
|
|
222
|
-
interruptError = new Error("Codex provider cannot interrupt an active helper turn.");
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (typeof provider?.deleteThread !== "function") {
|
|
226
|
-
return { ok: false, error: new Error("Codex provider cannot delete a helper thread."), interruptError };
|
|
227
|
-
}
|
|
228
|
-
try {
|
|
229
|
-
const result = await deleteCodexAppServerThread({ provider, threadId });
|
|
230
|
-
return {
|
|
231
|
-
deleted: true,
|
|
232
|
-
interrupted: Boolean(turnId) && !interruptError,
|
|
233
|
-
ok: true,
|
|
234
|
-
result,
|
|
235
|
-
status: "deleted",
|
|
236
|
-
threadId,
|
|
237
|
-
turnId,
|
|
238
|
-
interruptError
|
|
239
|
-
};
|
|
240
|
-
} catch (error) {
|
|
241
|
-
let absent = false;
|
|
242
|
-
if (codexAppServerRequestIsInvalid(error, "thread/delete")) {
|
|
243
|
-
try {
|
|
244
|
-
absent = !await codexAppServerThreadHasReadableHistory(provider, threadId);
|
|
245
|
-
} catch {
|
|
246
|
-
absent = false;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
if (absent) {
|
|
250
|
-
return {
|
|
251
|
-
deleted: false,
|
|
252
|
-
interrupted: Boolean(turnId) && !interruptError,
|
|
253
|
-
ok: true,
|
|
254
|
-
status: "notFound",
|
|
255
|
-
threadId,
|
|
256
|
-
turnId,
|
|
257
|
-
interruptError
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
return { ok: false, error, interruptError };
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function codexApplicationToolConfiguration(tools) {
|
|
265
|
-
const dynamicTools = (tools?.schemas || []).map(({ function: tool }) => ({
|
|
266
|
-
type: "function", name: tool.name, description: tool.description, inputSchema: tool.parameters
|
|
267
|
-
}));
|
|
268
|
-
return { dynamicTools, toolSchemaIdentity: createHash("sha256").update(JSON.stringify(dynamicTools)).digest("hex") };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function assertCodexApplicationToolSchemaIdentity(threadId, savedIdentity, toolSchemaIdentity) {
|
|
272
|
-
if (threadId && (savedIdentity || codexApplicationToolConfiguration().toolSchemaIdentity) !== toolSchemaIdentity) {
|
|
273
|
-
throw new Error("This Codex conversation was created with different application tool entry points. Start a new conversation to enable or remove application tools.");
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Retained thread selection keeps observation ahead of a resume that can start work. */
|
|
278
|
-
async function ensureCodexAppServerThread({
|
|
279
|
-
observeThread,
|
|
280
|
-
provider,
|
|
281
|
-
settings,
|
|
282
|
-
projectHooks = false,
|
|
283
|
-
identity,
|
|
284
|
-
applicationTools,
|
|
285
|
-
providerReady,
|
|
286
|
-
workdir = "",
|
|
287
|
-
onStage = () => {}
|
|
288
|
-
} = {}) {
|
|
289
|
-
if (typeof observeThread !== "function") {
|
|
290
|
-
throw new TypeError("Main Codex threads require an observer before they can resume.");
|
|
291
|
-
}
|
|
292
|
-
const normalizedWorkdir = normalizeAgentText(workdir);
|
|
293
|
-
let stageStartedAt = Date.now();
|
|
294
|
-
const availability = typeof provider.ensureAvailable === "function"
|
|
295
|
-
? await provider.ensureAvailable()
|
|
296
|
-
: null;
|
|
297
|
-
const appServerRuntime = availability?.runtime || await provider.ensureRuntime();
|
|
298
|
-
onStage({ durationMs: Date.now() - stageStartedAt, stage: "runtime" });
|
|
299
|
-
const existingThreadId = identity.read(normalizedWorkdir);
|
|
300
|
-
const toolConfiguration = applicationTools ? codexApplicationToolConfiguration(applicationTools) : null;
|
|
301
|
-
if (toolConfiguration) {
|
|
302
|
-
if (typeof identity.readToolSchemaIdentity !== "function") throw new TypeError("Bound Codex application tools require the original schema identity reader and writer.");
|
|
303
|
-
assertCodexApplicationToolSchemaIdentity(existingThreadId, await identity.readToolSchemaIdentity(normalizedWorkdir), toolConfiguration.toolSchemaIdentity);
|
|
304
|
-
}
|
|
305
|
-
let config = null;
|
|
306
|
-
if (projectHooks) {
|
|
307
|
-
stageStartedAt = Date.now();
|
|
308
|
-
config = await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir);
|
|
309
|
-
onStage({ durationMs: Date.now() - stageStartedAt, stage: "hook-config" });
|
|
310
|
-
}
|
|
311
|
-
let { threadSettings, threadStartSettings } = await settings(normalizedWorkdir, config);
|
|
312
|
-
if (toolConfiguration) {
|
|
313
|
-
threadSettings = { ...threadSettings, dynamicTools: toolConfiguration.dynamicTools };
|
|
314
|
-
threadStartSettings = { ...threadStartSettings, dynamicTools: toolConfiguration.dynamicTools };
|
|
315
|
-
}
|
|
316
|
-
if (providerReady) await providerReady({ provider, threadId: existingThreadId });
|
|
317
|
-
let thread = null;
|
|
318
|
-
stageStartedAt = Date.now();
|
|
319
|
-
if (existingThreadId) {
|
|
320
|
-
if (identity.pauseGoal) {
|
|
321
|
-
const { goal } = await provider.readGoal(existingThreadId);
|
|
322
|
-
if (goal?.status === "active") await provider.setGoalStatus(existingThreadId, "paused");
|
|
323
|
-
}
|
|
324
|
-
// Resuming can immediately start an active goal before the RPC returns.
|
|
325
|
-
await observeThread(existingThreadId);
|
|
326
|
-
thread = await provider.resumeThread(existingThreadId, threadSettings);
|
|
327
|
-
if (identity.pauseGoal) {
|
|
328
|
-
await identity.clearPause();
|
|
329
|
-
}
|
|
330
|
-
} else {
|
|
331
|
-
thread = await provider.startThread(threadStartSettings);
|
|
332
|
-
}
|
|
333
|
-
onStage({ durationMs: Date.now() - stageStartedAt, stage: existingThreadId ? "resume" : "start" });
|
|
334
|
-
const threadId = normalizeAgentText(thread.id || existingThreadId);
|
|
335
|
-
if (!threadId) {
|
|
336
|
-
throw new Error("Codex app-server did not return a thread id.");
|
|
337
|
-
}
|
|
338
|
-
stageStartedAt = Date.now();
|
|
339
|
-
const recorded = await identity.write({ appServerRuntime, threadId, workdir: normalizedWorkdir,
|
|
340
|
-
...(toolConfiguration ? { toolSchemaIdentity: toolConfiguration.toolSchemaIdentity } : {}) });
|
|
341
|
-
// The session no longer uses the runtime directory it recorded before (an
|
|
342
|
-
// earlier sign-in's). Releasing it is cleanup: a failure keeps the directory.
|
|
343
|
-
const superseded = normalizeAgentText(recorded?.supersededRuntimeDir);
|
|
344
|
-
if (superseded) await releaseSupersededCodexRuntime(superseded).catch(() => null);
|
|
345
|
-
if (providerReady) await providerReady({ provider, threadId });
|
|
346
|
-
onStage({ durationMs: Date.now() - stageStartedAt, stage: "identity-metadata" });
|
|
347
|
-
return {
|
|
348
|
-
appServerRuntime,
|
|
349
|
-
thread,
|
|
350
|
-
threadId
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function codexAppServerHelperLoginParams(auth = {}, errorPrefix = "") {
|
|
355
|
-
if (auth.authMode === "chatgpt") {
|
|
356
|
-
return {
|
|
357
|
-
accessToken: auth.accessToken,
|
|
358
|
-
chatgptAccountId: auth.accountId,
|
|
359
|
-
chatgptPlanType: auth.planType || null,
|
|
360
|
-
type: "chatgptAuthTokens"
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
if (auth.authMode === "apiKey") {
|
|
364
|
-
return {
|
|
365
|
-
apiKey: auth.apiKey,
|
|
366
|
-
type: "apiKey"
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
throw codexAppServerHelperAuthError(
|
|
370
|
-
`${errorPrefix}codex_helper_auth_invalid`,
|
|
371
|
-
"The selected Codex authentication mode is unsupported. Reconnect Codex and retry."
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function codexAppServerHelperLoginResponseType(auth = {}) {
|
|
376
|
-
return auth.authMode === "chatgpt" ? "chatgptAuthTokens" : "apiKey";
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function codexAppServerHelperAccountType(auth = {}) {
|
|
380
|
-
return auth.authMode === "chatgpt" ? "chatgpt" : "apiKey";
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function codexAppServerHelperRequestError(error = null, reason = "", errorPrefix = "") {
|
|
384
|
-
if (
|
|
385
|
-
error?.name === "AbortError" ||
|
|
386
|
-
error?.code === "ABORT_ERR" ||
|
|
387
|
-
(typeof error?.code === "string" && error.code.startsWith(`${errorPrefix}codex_helper_`)) ||
|
|
388
|
-
error?.code === `${errorPrefix}codex_model_catalog_invalid`
|
|
389
|
-
) {
|
|
390
|
-
return error;
|
|
391
|
-
}
|
|
392
|
-
const failure = new Error("Codex isolated helper execution failed. Retry the task.");
|
|
393
|
-
failure.code = `${errorPrefix}codex_helper_provider_request_failed`;
|
|
394
|
-
const providerCode = Number(error?.code);
|
|
395
|
-
if (Number.isSafeInteger(providerCode)) {
|
|
396
|
-
failure.providerCode = providerCode;
|
|
397
|
-
}
|
|
398
|
-
const normalizedReason = normalizeAgentText(reason);
|
|
399
|
-
if (normalizedReason && normalizedReason.length <= 128) {
|
|
400
|
-
failure.operation = normalizedReason;
|
|
401
|
-
}
|
|
402
|
-
return failure;
|
|
403
|
-
}
|
|
17
|
+
const CODEX_APP_SERVER_SERVER_INFO_USER_AGENT_MAX_LENGTH = 512;
|
|
404
18
|
|
|
405
19
|
function normalizeCodexAppServerInfo(initializeResult = null) {
|
|
406
20
|
if (!isRecord(initializeResult)) {
|
|
@@ -420,305 +34,39 @@ function normalizeCodexAppServerInfo(initializeResult = null) {
|
|
|
420
34
|
});
|
|
421
35
|
}
|
|
422
36
|
|
|
423
|
-
|
|
424
|
-
const normalizedEndpoint = normalizeAgentText(endpoint);
|
|
425
|
-
if (!normalizedEndpoint) {
|
|
426
|
-
return "";
|
|
427
|
-
}
|
|
428
|
-
return normalizedEndpoint;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function codexAppServerThreadRequestParams(params = {}, threadEnv = {}) {
|
|
432
|
-
const source = isRecord(params) ? params : {};
|
|
433
|
-
const config = isRecord(source.config) ? source.config : {};
|
|
434
|
-
if (Object.hasOwn(config, "shell_environment_policy")) {
|
|
435
|
-
return source;
|
|
436
|
-
}
|
|
437
|
-
return {
|
|
438
|
-
...source,
|
|
439
|
-
config: {
|
|
440
|
-
...config,
|
|
441
|
-
shell_environment_policy: {
|
|
442
|
-
inherit: "none",
|
|
443
|
-
set: normalizeCodexAppServerTerminalEnv(threadEnv)
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function codexTextInput(text = "") {
|
|
450
|
-
return {
|
|
451
|
-
text: String(text ?? ""),
|
|
452
|
-
text_elements: [],
|
|
453
|
-
type: "text"
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function codexLocalImageInput(attachments = []) {
|
|
458
|
-
return attachments
|
|
459
|
-
.filter((attachment) => attachment.contentType?.startsWith("image/"))
|
|
460
|
-
.map((attachment) => ({ type: "localImage", path: attachment.path }));
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
async function sendCodexAppServerPrompt({
|
|
464
|
-
attachments = [],
|
|
465
|
-
clientUserMessageId = "",
|
|
466
|
-
outputSchema = null,
|
|
467
|
-
provider,
|
|
468
|
-
prompt = "",
|
|
469
|
-
threadId = "",
|
|
470
|
-
readOnly = false
|
|
471
|
-
} = {}, authorized = {}) {
|
|
472
|
-
const authoredInput = String(prompt ?? "");
|
|
473
|
-
const preparedInput = authorized.preparedInput;
|
|
474
|
-
if (preparedInput === undefined && !authoredInput.trim()) {
|
|
475
|
-
throw new Error("Codex app-server prompt is empty.");
|
|
476
|
-
}
|
|
477
|
-
const input = preparedInput === undefined ? [authoredInput, ...codexLocalImageInput(attachments)] : preparedInput;
|
|
478
|
-
const turnSettings = {
|
|
479
|
-
...authorized.turnSettings,
|
|
480
|
-
...(readOnly
|
|
481
|
-
? {
|
|
482
|
-
sandboxPolicy: {
|
|
483
|
-
networkAccess: false,
|
|
484
|
-
type: "readOnly"
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
: {}),
|
|
488
|
-
...(normalizeAgentText(clientUserMessageId)
|
|
489
|
-
? { clientUserMessageId: normalizeAgentText(clientUserMessageId) }
|
|
490
|
-
: {})
|
|
491
|
-
};
|
|
492
|
-
if (outputSchema && typeof outputSchema === "object" && !Array.isArray(outputSchema)) {
|
|
493
|
-
turnSettings.outputSchema = outputSchema;
|
|
494
|
-
}
|
|
495
|
-
const turn = await provider.sendTurn(threadId, input, turnSettings);
|
|
496
|
-
return {
|
|
497
|
-
input,
|
|
498
|
-
turn
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function codexTurnInput(input = []) {
|
|
503
|
-
const values = Array.isArray(input) ? input : [input];
|
|
504
|
-
return values.map((item) => {
|
|
505
|
-
if (isRecord(item) && item.type === "localImage" && typeof item.path === "string") {
|
|
506
|
-
return { type: "localImage", path: item.path };
|
|
507
|
-
}
|
|
508
|
-
if (isRecord(item) && item.type === "image" && typeof item.url === "string") {
|
|
509
|
-
return { type: "image", url: item.url };
|
|
510
|
-
}
|
|
511
|
-
if (isRecord(item) && item.type === "text") {
|
|
512
|
-
return codexTextInput(item.text);
|
|
513
|
-
}
|
|
514
|
-
return codexTextInput(item);
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function shellQuote(value = "") {
|
|
519
|
-
const text = String(value ?? "");
|
|
520
|
-
if (/^[A-Za-z0-9_./:=@+-]+$/u.test(text)) {
|
|
521
|
-
return text;
|
|
522
|
-
}
|
|
523
|
-
return `'${text.replaceAll("'", "'\"'\"'")}'`;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function codexControlProbeCommand(keys, marker) {
|
|
527
|
-
const source = [
|
|
528
|
-
"const {createHash}=require('node:crypto');",
|
|
529
|
-
`const keys=${JSON.stringify(keys)};`,
|
|
530
|
-
`process.stdout.write(${JSON.stringify(marker)}+createHash('sha256').update(JSON.stringify(keys.map(key=>[key,process.env[key]]))).digest('hex'))`
|
|
531
|
-
].join("");
|
|
532
|
-
return `${shellQuote(process.execPath)} -e ${shellQuote(source)}`;
|
|
533
|
-
}
|
|
37
|
+
const CODEX_APP_SERVER_CHATGPT_REFRESH_METHOD = "account/chatgptAuthTokens/refresh";
|
|
534
38
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
endpoint = "",
|
|
538
|
-
threadId = ""
|
|
539
|
-
} = {}) {
|
|
540
|
-
const normalizedEndpoint = codexAppServerEndpointForTarget(endpoint);
|
|
541
|
-
const resolvedCodexCommand = normalizeAgentText(codexCommand) || "codex";
|
|
542
|
-
const normalizedThreadId = normalizeAgentText(threadId);
|
|
543
|
-
if (!normalizedEndpoint) {
|
|
544
|
-
throw new Error("Codex app-server endpoint is required for the native CLI command.");
|
|
545
|
-
}
|
|
546
|
-
if (!normalizedThreadId) {
|
|
547
|
-
throw new Error("Codex thread id is required for the native CLI command.");
|
|
548
|
-
}
|
|
549
|
-
const argv = [
|
|
550
|
-
resolvedCodexCommand,
|
|
551
|
-
"-c",
|
|
552
|
-
"check_for_update_on_startup=false",
|
|
553
|
-
"--remote",
|
|
554
|
-
normalizedEndpoint,
|
|
555
|
-
"resume",
|
|
556
|
-
normalizedThreadId
|
|
557
|
-
];
|
|
558
|
-
return {
|
|
559
|
-
argv,
|
|
560
|
-
command: argv.map(shellQuote).join(" ")
|
|
561
|
-
};
|
|
562
|
-
}
|
|
39
|
+
// Option callbacks an application leaves out.
|
|
40
|
+
function readNoInstructions() {}
|
|
563
41
|
|
|
564
|
-
function
|
|
565
|
-
|
|
566
|
-
error.code = errorCode;
|
|
567
|
-
return error;
|
|
42
|
+
function keepThreadEnvironment(environment) {
|
|
43
|
+
return environment;
|
|
568
44
|
}
|
|
569
45
|
|
|
570
|
-
|
|
571
|
-
function codexAppServerThreadInventoryRefusal(entry, threadId, { cwd, verifyCwd }) {
|
|
572
|
-
if (!isRecord(entry)) return "listed an entry that is not a thread";
|
|
573
|
-
if (!threadId) return "listed a thread without an id";
|
|
574
|
-
if (threadId.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH) {
|
|
575
|
-
return `listed a thread id longer than ${CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH} characters`;
|
|
576
|
-
}
|
|
577
|
-
if (codexAppServerTextHasControlCharacters(threadId)) return "listed a thread id with control characters";
|
|
578
|
-
if (verifyCwd && normalizeAgentText(entry.cwd) !== cwd) return `listed thread ${threadId} outside the requested directory`;
|
|
579
|
-
return "";
|
|
580
|
-
}
|
|
46
|
+
function prepareNoThreadResume() {}
|
|
581
47
|
|
|
582
|
-
|
|
583
|
-
ancestorThreadId = "",
|
|
584
|
-
archived = false,
|
|
585
|
-
client,
|
|
586
|
-
cwd = "",
|
|
587
|
-
errorCode = "",
|
|
588
|
-
findThreadId = "",
|
|
589
|
-
label = "",
|
|
590
|
-
requestLabel = "",
|
|
591
|
-
runRequest,
|
|
592
|
-
signal = null,
|
|
593
|
-
sourceKinds = ["appServer"],
|
|
594
|
-
state,
|
|
595
|
-
verifyCwd = false
|
|
596
|
-
} = {}) {
|
|
597
|
-
const seenCursors = new Set();
|
|
598
|
-
let cursor = "";
|
|
599
|
-
for (let page = 0; page < CODEX_APP_SERVER_THREAD_INVENTORY_MAX_PAGES; page += 1) {
|
|
600
|
-
if (cursor && seenCursors.has(cursor)) {
|
|
601
|
-
throw codexAppServerThreadInventoryError(
|
|
602
|
-
errorCode,
|
|
603
|
-
label,
|
|
604
|
-
"repeated a pagination cursor"
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
if (cursor) {
|
|
608
|
-
seenCursors.add(cursor);
|
|
609
|
-
}
|
|
610
|
-
const response = await runRequest(
|
|
611
|
-
() => client.request("thread/list", {
|
|
612
|
-
archived,
|
|
613
|
-
...(cursor ? { cursor } : {}),
|
|
614
|
-
...(ancestorThreadId ? { ancestorThreadId } : cwd ? { cwd } : {}),
|
|
615
|
-
limit: CODEX_APP_SERVER_THREAD_INVENTORY_PAGE_LIMIT,
|
|
616
|
-
modelProviders: [],
|
|
617
|
-
sourceKinds,
|
|
618
|
-
useStateDbOnly: false
|
|
619
|
-
}, { signal }),
|
|
620
|
-
requestLabel
|
|
621
|
-
);
|
|
622
|
-
if (!Array.isArray(response?.data) || response.data.length > CODEX_APP_SERVER_THREAD_INVENTORY_PAGE_LIMIT) {
|
|
623
|
-
throw codexAppServerThreadInventoryError(errorCode, label, "returned a malformed page");
|
|
624
|
-
}
|
|
625
|
-
// Only thread ids are retained, so only they are bounded. A search for one
|
|
626
|
-
// thread retains none and is bounded by the page limit alone.
|
|
627
|
-
if (!findThreadId && state.entryCount + response.data.length > CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT) {
|
|
628
|
-
throw codexAppServerThreadInventoryError(errorCode, label,
|
|
629
|
-
`exceeded its ${CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT}-thread limit`);
|
|
630
|
-
}
|
|
631
|
-
for (const entry of response.data) {
|
|
632
|
-
const threadId = isRecord(entry) ? normalizeAgentText(entry.id) : "";
|
|
633
|
-
const refusal = codexAppServerThreadInventoryRefusal(entry, threadId, { cwd, verifyCwd });
|
|
634
|
-
if (refusal) {
|
|
635
|
-
throw codexAppServerThreadInventoryError(errorCode, label, refusal);
|
|
636
|
-
}
|
|
637
|
-
if (findThreadId) {
|
|
638
|
-
if (threadId === findThreadId) {
|
|
639
|
-
state.found = true;
|
|
640
|
-
return state;
|
|
641
|
-
}
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
state.entryCount += 1;
|
|
645
|
-
state.threadIds.add(threadId);
|
|
646
|
-
}
|
|
647
|
-
const nextCursor = response.nextCursor === undefined || response.nextCursor === null || response.nextCursor === ""
|
|
648
|
-
? ""
|
|
649
|
-
: typeof response.nextCursor === "string"
|
|
650
|
-
? response.nextCursor.trim()
|
|
651
|
-
: null;
|
|
652
|
-
if (
|
|
653
|
-
nextCursor === null ||
|
|
654
|
-
nextCursor.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH ||
|
|
655
|
-
(nextCursor && seenCursors.has(nextCursor))
|
|
656
|
-
) {
|
|
657
|
-
throw codexAppServerThreadInventoryError(
|
|
658
|
-
errorCode,
|
|
659
|
-
label,
|
|
660
|
-
"returned an invalid pagination cursor"
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
if (!nextCursor) {
|
|
664
|
-
return state;
|
|
665
|
-
}
|
|
666
|
-
cursor = nextCursor;
|
|
667
|
-
}
|
|
668
|
-
throw codexAppServerThreadInventoryError(
|
|
669
|
-
errorCode,
|
|
670
|
-
label,
|
|
671
|
-
"exceeded its page limit"
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function codexPlanUsage(limits = {}) {
|
|
676
|
-
if (limits?.limitId && limits.limitId !== "codex") return { status: "unavailable", windows: [], checkedAt: Date.now() };
|
|
677
|
-
const windows = ["primary", "secondary"].flatMap((id) => {
|
|
678
|
-
const value = limits?.[id];
|
|
679
|
-
if (typeof value?.usedPercent !== "number" || !Number.isFinite(value.usedPercent) || value.usedPercent < 0) return [];
|
|
680
|
-
return [{
|
|
681
|
-
id,
|
|
682
|
-
remainingPercent: Math.max(0, 100 - Math.min(100, value.usedPercent)),
|
|
683
|
-
windowDurationMins: Number.isInteger(value.windowDurationMins) && value.windowDurationMins > 0 ? value.windowDurationMins : null,
|
|
684
|
-
resetsAt: Number.isSafeInteger(value.resetsAt) && value.resetsAt > 0 ? value.resetsAt : null
|
|
685
|
-
}];
|
|
686
|
-
});
|
|
687
|
-
return { status: windows.length ? "available" : "unavailable", windows, checkedAt: Date.now() };
|
|
688
|
-
}
|
|
48
|
+
function logNothing() {}
|
|
689
49
|
|
|
690
|
-
function
|
|
691
|
-
const thread = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
692
|
-
return {
|
|
693
|
-
id: normalizeAgentText(thread.id || thread.threadId),
|
|
694
|
-
provider: normalizeAgentText(thread.provider),
|
|
695
|
-
raw: thread.raw || thread
|
|
696
|
-
};
|
|
697
|
-
}
|
|
50
|
+
function reportNoObservationLoss() {}
|
|
698
51
|
|
|
699
|
-
function
|
|
700
|
-
const turn = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
701
|
-
return {
|
|
702
|
-
id: normalizeAgentText(turn.id || turn.turnId),
|
|
703
|
-
provider: normalizeAgentText(turn.provider),
|
|
704
|
-
raw: turn.raw || turn,
|
|
705
|
-
status: normalizeAgentText(turn.status || turn.raw?.status)
|
|
706
|
-
};
|
|
707
|
-
}
|
|
52
|
+
function closeNothing() {}
|
|
708
53
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
function codexAppServerDeliveredTurn(delivery = {}) {
|
|
712
|
-
const turn = delivery?.turn || {};
|
|
713
|
-
return {
|
|
714
|
-
turnId: normalizeAgentText(turn.id),
|
|
715
|
-
status: normalizeAgentText(turn.status || turn.raw?.status),
|
|
716
|
-
error: codexAppServerErrorText(turn.raw?.error || turn.response?.turn?.error || turn.error)
|
|
717
|
-
};
|
|
54
|
+
function runDirectly(operation) {
|
|
55
|
+
return operation();
|
|
718
56
|
}
|
|
719
57
|
|
|
58
|
+
/**
|
|
59
|
+
* One account's connection to its native Codex app-server: the runtime it ensures, the client it opens, the
|
|
60
|
+
* notifications it publishes and the server requests it routes. Its auth, turns, threads and inventories are
|
|
61
|
+
* composed from their own modules; their operations are this provider's members.
|
|
62
|
+
*/
|
|
720
63
|
export class CodexAppServerAgentProvider {
|
|
721
|
-
|
|
64
|
+
/**
|
|
65
|
+
* @param {object} options The provider options: execution mode, account selection and the application's
|
|
66
|
+
* thread callbacks.
|
|
67
|
+
* @param {object} host The native host: credentials, runtime, launch parameters, execution and configuration.
|
|
68
|
+
*/
|
|
69
|
+
constructor(options, host) {
|
|
722
70
|
this.host = host;
|
|
723
71
|
this.credentials = host.credentials;
|
|
724
72
|
this.configuration = {
|
|
@@ -732,9 +80,6 @@ export class CodexAppServerAgentProvider {
|
|
|
732
80
|
this.errorPrefix = this.configuration.errorPrefix;
|
|
733
81
|
this.accountReader = createCodexAccountReader({ errorPrefix: this.errorPrefix, ...this.configuration.accountReader });
|
|
734
82
|
this.modelCatalogError = (message) => codexAppServerModelCatalogError(message, this.errorPrefix);
|
|
735
|
-
this.planUsage = null;
|
|
736
|
-
this.planUsagePending = null;
|
|
737
|
-
this.planUsageAuthGeneration = 0;
|
|
738
83
|
this.availabilityPromise = null;
|
|
739
84
|
this.stale = false;
|
|
740
85
|
this.options = options;
|
|
@@ -743,48 +88,93 @@ export class CodexAppServerAgentProvider {
|
|
|
743
88
|
this.connectionGeneration = 0;
|
|
744
89
|
this.notificationSubscribers = new Set();
|
|
745
90
|
this.settingsWaiters = new Set();
|
|
746
|
-
this.helperAuth = null;
|
|
747
|
-
this.helperAuthBlocked = false;
|
|
748
91
|
this.initializeResult = null;
|
|
749
92
|
this.runtime = null;
|
|
750
93
|
this.runtimeStopOwner = null;
|
|
751
94
|
this.runtimePromise = null;
|
|
752
95
|
this.serverRequestHandler = null;
|
|
753
96
|
this.threadRequestHandlers = new Map();
|
|
97
|
+
|
|
98
|
+
const auth = createCodexProviderAuth({ provider: this });
|
|
99
|
+
const turns = createCodexProviderTurns({ provider: this });
|
|
100
|
+
const threads = createCodexProviderThreads({ provider: this });
|
|
101
|
+
const inventories = createCodexProviderInventories({ provider: this });
|
|
102
|
+
this.auth = auth;
|
|
103
|
+
this.turns = turns;
|
|
104
|
+
|
|
105
|
+
const {
|
|
106
|
+
readInstructions = readNoInstructions,
|
|
107
|
+
prepareThreadEnvironment = keepThreadEnvironment,
|
|
108
|
+
prepareThreadParams = keepCodexThreadParams,
|
|
109
|
+
prepareThreadResumeParams = prepareNoThreadResume,
|
|
110
|
+
beforeResumeThread = resumeCodexThreadAsIs
|
|
111
|
+
} = options;
|
|
112
|
+
const { log = logNothing } = host;
|
|
754
113
|
this.conversationRuntime = createCodexConversationAdapter({
|
|
755
|
-
readInstructions: (params, threadId) =>
|
|
114
|
+
readInstructions: (params, threadId) => readInstructions(params, threadId),
|
|
756
115
|
client: () => this.activeClient(),
|
|
757
116
|
runtime: () => this.runtime,
|
|
758
117
|
runRequest: (run, reason) => this.runRequest(run, reason),
|
|
759
118
|
timeoutMs: options.threadControlTimeoutMs,
|
|
760
119
|
environment: options.threadEnv,
|
|
761
|
-
prepareEnvironment: (environment) =>
|
|
762
|
-
prepareParameters: (params) =>
|
|
763
|
-
prepareResume: (id, params, context) =>
|
|
764
|
-
beforeResume: (id) =>
|
|
120
|
+
prepareEnvironment: (environment) => prepareThreadEnvironment(environment) || environment,
|
|
121
|
+
prepareParameters: (params) => prepareThreadParams(params),
|
|
122
|
+
prepareResume: (id, params, context) => prepareThreadResumeParams(id, params, context),
|
|
123
|
+
beforeResume: (id) => beforeResumeThread(id),
|
|
765
124
|
prepareHistory: (params, client, context) => this.withHistoryAdapter(params, client, context),
|
|
766
125
|
threadParameters: codexAppServerThreadRequestParams,
|
|
767
|
-
verifyEnvironment:
|
|
768
|
-
recordInterruption:
|
|
769
|
-
|
|
770
|
-
this.interruptedTurns.set(key, this.interruptedTurns.get(key) || "control_reconfiguration");
|
|
771
|
-
},
|
|
772
|
-
onRecoveryFailure: (id) => {
|
|
773
|
-
const probe = this.threadControlProbes.get(id);
|
|
774
|
-
if (probe && !probe.verified) {
|
|
775
|
-
this.threadControlProbes.delete(id);
|
|
776
|
-
for (const pending of probe.pending) this.publishNotification(pending);
|
|
777
|
-
}
|
|
778
|
-
},
|
|
126
|
+
verifyEnvironment: turns.verifyThreadEnvironment,
|
|
127
|
+
recordInterruption: turns.recordInterruption,
|
|
128
|
+
onRecoveryFailure: turns.abandonControlProbe,
|
|
779
129
|
publishGoal: (threadId, goal) => this.publishNotification({ method: "thread/goal/updated", params: { threadId, goal } }),
|
|
780
|
-
log: (event, fields, context) =>
|
|
781
|
-
...context, connectionGeneration: this.connectionGeneration
|
|
782
|
-
})
|
|
130
|
+
log: (event, fields, context) => log(event, fields, { ...context, connectionGeneration: this.connectionGeneration })
|
|
783
131
|
});
|
|
784
|
-
|
|
785
|
-
this.
|
|
786
|
-
this.
|
|
787
|
-
this.
|
|
132
|
+
|
|
133
|
+
this.selectedHelperAuth = auth.selectedHelperAuth;
|
|
134
|
+
this.assertHelperAccountIdentityCurrent = auth.assertHelperAccountIdentityCurrent;
|
|
135
|
+
this.authenticateHelperClient = auth.authenticateHelperClient;
|
|
136
|
+
this.refreshHelperChatgptAuth = auth.refreshHelperChatgptAuth;
|
|
137
|
+
this.preflightAuth = auth.preflightAuth;
|
|
138
|
+
this.assertRuntimeAuthReady = auth.assertRuntimeAuthReady;
|
|
139
|
+
this.markReconnectRequired = auth.markReconnectRequired;
|
|
140
|
+
this.readPlanUsage = auth.readPlanUsage;
|
|
141
|
+
|
|
142
|
+
this.sendTurn = turns.sendTurn;
|
|
143
|
+
this.steerTurn = turns.steerTurn;
|
|
144
|
+
this.interruptionOutcome = turns.interruptionOutcome;
|
|
145
|
+
this.interruptTurn = turns.interruptTurn;
|
|
146
|
+
this.nativeCliResumeCommand = turns.nativeCliResumeCommand;
|
|
147
|
+
this.isControlProbeTurn = turns.isControlProbeTurn;
|
|
148
|
+
this.stopThreadForObservationLoss = turns.stopThreadForObservationLoss;
|
|
149
|
+
|
|
150
|
+
this.startThread = threads.startThread;
|
|
151
|
+
this.resumeThread = threads.resumeThread;
|
|
152
|
+
this.withHistoryAdapter = threads.withHistoryAdapter;
|
|
153
|
+
this.withThreadEnvironment = threads.withThreadEnvironment;
|
|
154
|
+
this.ensureThreadControls = threads.ensureThreadControls;
|
|
155
|
+
this.readGoal = threads.readGoal;
|
|
156
|
+
this.updateThreadSettings = threads.updateThreadSettings;
|
|
157
|
+
this.setGoal = threads.setGoal;
|
|
158
|
+
this.setGoalStatus = threads.setGoalStatus;
|
|
159
|
+
this.clearGoal = threads.clearGoal;
|
|
160
|
+
this.readThread = threads.readThread;
|
|
161
|
+
this.readThreadStatus = threads.readThreadStatus;
|
|
162
|
+
this.listThreadTurns = threads.listThreadTurns;
|
|
163
|
+
this.exportThreadHistory = threads.exportThreadHistory;
|
|
164
|
+
this.unsubscribeThread = threads.unsubscribeThread;
|
|
165
|
+
this.deleteThread = threads.deleteThread;
|
|
166
|
+
|
|
167
|
+
this.listLoadedThreads = inventories.listLoadedThreads;
|
|
168
|
+
this.listAppServerThreadsForCwd = inventories.listAppServerThreadsForCwd;
|
|
169
|
+
this.listThreadDescendants = inventories.listThreadDescendants;
|
|
170
|
+
this.listNativeThreadsForCwd = inventories.listNativeThreadsForCwd;
|
|
171
|
+
this.nativeThreadExists = inventories.nativeThreadExists;
|
|
172
|
+
this.listHelperThreads = inventories.listHelperThreads;
|
|
173
|
+
this.listModels = inventories.listModels;
|
|
174
|
+
this.readConfig = inventories.readConfig;
|
|
175
|
+
this.trustProject = inventories.trustProject;
|
|
176
|
+
this.listHooks = inventories.listHooks;
|
|
177
|
+
this.writeHookTrustState = inventories.writeHookTrustState;
|
|
788
178
|
}
|
|
789
179
|
|
|
790
180
|
get threadEnvironments() { return this.conversationRuntime.threadEnvironments; }
|
|
@@ -795,123 +185,25 @@ export class CodexAppServerAgentProvider {
|
|
|
795
185
|
return codexAppServerIsHelper(this.options);
|
|
796
186
|
}
|
|
797
187
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
return null;
|
|
801
|
-
}
|
|
802
|
-
if (this.host.parameters.providerHome) {
|
|
803
|
-
return { identitySignature: await this.credentials.identity(this.host.parameters) };
|
|
804
|
-
}
|
|
805
|
-
return this.accountReader.readSelected(this.options);
|
|
188
|
+
currentConnectionGeneration() {
|
|
189
|
+
return this.connectionGeneration;
|
|
806
190
|
}
|
|
807
191
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
if (this.helperAuthBlocked) {
|
|
813
|
-
throw this.accountReader.error(
|
|
814
|
-
`${this.errorPrefix}codex_helper_auth_changed`,
|
|
815
|
-
"The selected Codex account changed during helper work. Retry the task with the current account."
|
|
816
|
-
);
|
|
817
|
-
}
|
|
818
|
-
const current = await this.selectedHelperAuth();
|
|
819
|
-
if (current.identitySignature !== this.helperAuth.identitySignature) {
|
|
820
|
-
this.helperAuthBlocked = true;
|
|
821
|
-
throw this.accountReader.error(
|
|
822
|
-
`${this.errorPrefix}codex_helper_auth_changed`,
|
|
823
|
-
"The selected Codex account changed during helper work. Retry the task with the current account."
|
|
824
|
-
);
|
|
825
|
-
}
|
|
192
|
+
currentServerInfo() {
|
|
193
|
+
return this.initializeResult;
|
|
826
194
|
}
|
|
827
195
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
return null;
|
|
831
|
-
}
|
|
832
|
-
const auth = await this.selectedHelperAuth();
|
|
833
|
-
if (
|
|
834
|
-
!runtime.accountIdentitySignature ||
|
|
835
|
-
auth.identitySignature !== runtime.accountIdentitySignature
|
|
836
|
-
) {
|
|
837
|
-
throw this.accountReader.error(
|
|
838
|
-
`${this.errorPrefix}codex_helper_auth_changed`,
|
|
839
|
-
"The selected Codex account changed while helper execution was starting. Retry the task."
|
|
840
|
-
);
|
|
841
|
-
}
|
|
842
|
-
this.helperAuth = auth;
|
|
843
|
-
this.helperAuthBlocked = false;
|
|
844
|
-
if (this.host.parameters.providerHome) return auth;
|
|
845
|
-
try {
|
|
846
|
-
const login = await client.request(
|
|
847
|
-
"account/login/start",
|
|
848
|
-
codexAppServerHelperLoginParams(auth, this.errorPrefix)
|
|
849
|
-
);
|
|
850
|
-
if (normalizeAgentText(login?.type) !== codexAppServerHelperLoginResponseType(auth)) {
|
|
851
|
-
throw new Error("invalid login response");
|
|
852
|
-
}
|
|
853
|
-
const account = await client.request("account/read", {
|
|
854
|
-
refreshToken: false
|
|
855
|
-
});
|
|
856
|
-
if (
|
|
857
|
-
account?.requiresOpenaiAuth !== true ||
|
|
858
|
-
normalizeAgentText(account?.account?.type) !== codexAppServerHelperAccountType(auth)
|
|
859
|
-
) {
|
|
860
|
-
throw new Error("invalid account response");
|
|
861
|
-
}
|
|
862
|
-
return auth;
|
|
863
|
-
} catch {
|
|
864
|
-
this.helperAuth = null;
|
|
865
|
-
this.helperAuthBlocked = true;
|
|
866
|
-
throw this.accountReader.error(
|
|
867
|
-
`${this.errorPrefix}codex_helper_auth_unavailable`,
|
|
868
|
-
"Codex could not activate the selected account for isolated helper work. Reconnect Codex and retry."
|
|
869
|
-
);
|
|
870
|
-
}
|
|
196
|
+
isAvailable() {
|
|
197
|
+
return Boolean(this.client?.isOpen() && this.runtime);
|
|
871
198
|
}
|
|
872
199
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
if (
|
|
877
|
-
!this.isHelperProvider() ||
|
|
878
|
-
currentAuth?.authMode !== "chatgpt" ||
|
|
879
|
-
params.reason !== "unauthorized" ||
|
|
880
|
-
typeof previousAccountId !== "string" ||
|
|
881
|
-
previousAccountId.length === 0 ||
|
|
882
|
-
previousAccountId.length > CODEX_APP_SERVER_ACCOUNT_ID_MAX_LENGTH ||
|
|
883
|
-
previousAccountId !== currentAuth.accountId
|
|
884
|
-
) {
|
|
885
|
-
this.helperAuthBlocked = true;
|
|
886
|
-
throw this.accountReader.error(
|
|
887
|
-
`${this.errorPrefix}codex_helper_auth_refresh_rejected`,
|
|
888
|
-
"Codex helper authentication refresh was rejected. Reconnect Codex and retry."
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
const refreshed = await this.selectedHelperAuth();
|
|
892
|
-
if (
|
|
893
|
-
refreshed.authMode !== "chatgpt" ||
|
|
894
|
-
refreshed.identitySignature !== currentAuth.identitySignature
|
|
895
|
-
) {
|
|
896
|
-
this.helperAuthBlocked = true;
|
|
897
|
-
throw this.accountReader.error(
|
|
898
|
-
`${this.errorPrefix}codex_helper_auth_changed`,
|
|
899
|
-
"The selected Codex account changed during helper work. Retry the task with the current account."
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
if (refreshed.secretSignature === currentAuth.secretSignature) {
|
|
903
|
-
this.helperAuthBlocked = true;
|
|
904
|
-
throw this.accountReader.error(
|
|
905
|
-
`${this.errorPrefix}codex_helper_auth_refresh_pending`,
|
|
906
|
-
"The selected Codex account has not produced a newer access token yet. Reconnect Codex and retry."
|
|
907
|
-
);
|
|
200
|
+
subscribe(callback) {
|
|
201
|
+
if (typeof callback !== "function") {
|
|
202
|
+
throw new TypeError("Codex app-server observer must be a function.");
|
|
908
203
|
}
|
|
909
|
-
this
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
chatgptAccountId: refreshed.accountId,
|
|
913
|
-
chatgptPlanType: refreshed.planType || null
|
|
914
|
-
};
|
|
204
|
+
// Observation belongs to this provider, not one replaceable socket.
|
|
205
|
+
this.notificationSubscribers.add(callback);
|
|
206
|
+
return () => this.notificationSubscribers.delete(callback);
|
|
915
207
|
}
|
|
916
208
|
|
|
917
209
|
async handleServerRequest(request = {}, sourceClient = this.client) {
|
|
@@ -935,7 +227,7 @@ export class CodexAppServerAgentProvider {
|
|
|
935
227
|
}
|
|
936
228
|
return threadHandler.handler(request);
|
|
937
229
|
}
|
|
938
|
-
if (
|
|
230
|
+
if (this.serverRequestHandler) {
|
|
939
231
|
return this.serverRequestHandler(request);
|
|
940
232
|
}
|
|
941
233
|
const error = new Error("Codex app-server request is not supported by this client.");
|
|
@@ -958,10 +250,12 @@ export class CodexAppServerAgentProvider {
|
|
|
958
250
|
}
|
|
959
251
|
}
|
|
960
252
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
253
|
+
/**
|
|
254
|
+
* An account change leaves a runtime that is still working on the
|
|
255
|
+
* credentials it started with. Codex reads them once, so this provider
|
|
256
|
+
* finishes its running turn on them and then stops; it never re-checks,
|
|
257
|
+
* replaces or restarts its runtime, and never marks the new account invalid.
|
|
258
|
+
*/
|
|
965
259
|
markStale() {
|
|
966
260
|
this.stale = true;
|
|
967
261
|
}
|
|
@@ -992,63 +286,6 @@ export class CodexAppServerAgentProvider {
|
|
|
992
286
|
return this.runtime;
|
|
993
287
|
}
|
|
994
288
|
|
|
995
|
-
async preflightAuth(reason = "codex-auth-preflight", modelProviderId = "") {
|
|
996
|
-
if (await this.options.prepareAuth?.(modelProviderId) === "external") {
|
|
997
|
-
await this.assertRuntimeAuthReady(reason);
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
if (this.options.modelProviderId && this.options.modelProviderId !== "openai") {
|
|
1001
|
-
await this.credentials.assertCurrent("");
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
await this.assertRuntimeAuthReady(reason);
|
|
1005
|
-
try {
|
|
1006
|
-
return await assertCodexAuthPreflightReady({ ...this.options, codexCommand: this.host.parameters.codexCommand }, {
|
|
1007
|
-
execution: this.host.execution, credentials: this.credentials, reason
|
|
1008
|
-
});
|
|
1009
|
-
} catch (error) {
|
|
1010
|
-
if (error?.code === this.credentials.reconnectRequiredCode && this.runtime?.runtimeDir) {
|
|
1011
|
-
await this.stopRuntimeAndRequireDrain(reason);
|
|
1012
|
-
}
|
|
1013
|
-
throw error;
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
async assertRuntimeAuthReady(reason = "codex-app-server") {
|
|
1018
|
-
if (this.isHelperProvider() || this.stale) {
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
const runtime = this.runtime || {};
|
|
1022
|
-
let generationError = null;
|
|
1023
|
-
try {
|
|
1024
|
-
await this.credentials.assertCurrent(runtime.authStateSignature);
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
generationError = error;
|
|
1027
|
-
}
|
|
1028
|
-
const logTail = await tailTextFile(runtime.logPath || "");
|
|
1029
|
-
const reconnectRequired = codexAuthOutputRequiresReconnect(logTail);
|
|
1030
|
-
if (!generationError && !reconnectRequired) {
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
await this.stopRuntimeAndRequireDrain(reason);
|
|
1034
|
-
if (reconnectRequired) {
|
|
1035
|
-
await this.markReconnectRequired({
|
|
1036
|
-
toolHomeSource: runtime.toolHomeSource || this.options.toolHomeSource,
|
|
1037
|
-
observed: logTail,
|
|
1038
|
-
reason
|
|
1039
|
-
});
|
|
1040
|
-
throw this.credentials.reconnectError({
|
|
1041
|
-
observed: logTail
|
|
1042
|
-
});
|
|
1043
|
-
}
|
|
1044
|
-
throw generationError;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
async markReconnectRequired({ reason = "codex-app-server", observed = "", toolHomeSource } = {}) {
|
|
1048
|
-
await this.credentials.markInvalid({ reason, toolHomeSource });
|
|
1049
|
-
throw this.credentials.reconnectError({ modelProviderId: this.options.modelProviderId, observed });
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
289
|
async runRequest(operation, reason = "codex-app-server-request") {
|
|
1053
290
|
try {
|
|
1054
291
|
await this.assertHelperAccountIdentityCurrent();
|
|
@@ -1103,7 +340,7 @@ export class CodexAppServerAgentProvider {
|
|
|
1103
340
|
|
|
1104
341
|
async connect() {
|
|
1105
342
|
if (this.observationFailure) throw this.observationFailure;
|
|
1106
|
-
if (this.client?.isOpen
|
|
343
|
+
if (this.client?.isOpen() && this.runtime) {
|
|
1107
344
|
return {
|
|
1108
345
|
initializeResult: null,
|
|
1109
346
|
reusedClient: true,
|
|
@@ -1125,7 +362,7 @@ export class CodexAppServerAgentProvider {
|
|
|
1125
362
|
}
|
|
1126
363
|
|
|
1127
364
|
async openConnection() {
|
|
1128
|
-
if (this.client?.isOpen
|
|
365
|
+
if (this.client?.isOpen() && this.runtime) {
|
|
1129
366
|
return {
|
|
1130
367
|
initializeResult: null,
|
|
1131
368
|
reusedClient: true,
|
|
@@ -1133,14 +370,14 @@ export class CodexAppServerAgentProvider {
|
|
|
1133
370
|
};
|
|
1134
371
|
}
|
|
1135
372
|
const runtime = await this.ensureRuntime();
|
|
1136
|
-
if (this.client?.isOpen
|
|
373
|
+
if (this.client?.isOpen()) {
|
|
1137
374
|
return {
|
|
1138
375
|
initializeResult: null,
|
|
1139
376
|
reusedClient: true,
|
|
1140
377
|
runtime
|
|
1141
378
|
};
|
|
1142
379
|
}
|
|
1143
|
-
this.client?.close
|
|
380
|
+
this.client?.close();
|
|
1144
381
|
this.client = null;
|
|
1145
382
|
this.initializeResult = null;
|
|
1146
383
|
const client = new CodexAppServerJsonRpcClient({
|
|
@@ -1186,10 +423,7 @@ export class CodexAppServerAgentProvider {
|
|
|
1186
423
|
}
|
|
1187
424
|
: runtime;
|
|
1188
425
|
this.runtime = null;
|
|
1189
|
-
if (helper)
|
|
1190
|
-
this.helperAuth = null;
|
|
1191
|
-
this.helperAuthBlocked = true;
|
|
1192
|
-
}
|
|
426
|
+
if (helper) this.auth.blockHelper();
|
|
1193
427
|
if (cleanup.cleanupFailed) {
|
|
1194
428
|
const failure = new Error(
|
|
1195
429
|
helper
|
|
@@ -1207,18 +441,10 @@ export class CodexAppServerAgentProvider {
|
|
|
1207
441
|
throw error;
|
|
1208
442
|
}
|
|
1209
443
|
this.client = client;
|
|
1210
|
-
this.
|
|
1211
|
-
this.planUsagePending = null;
|
|
444
|
+
this.auth.resetPlanUsage();
|
|
1212
445
|
client.subscribe((notification = {}) => {
|
|
1213
446
|
if (this.client !== client || this.observationFailure) return;
|
|
1214
|
-
|
|
1215
|
-
this.planUsage = null;
|
|
1216
|
-
this.planUsageAuthGeneration += 1;
|
|
1217
|
-
}
|
|
1218
|
-
if (notification.method === "account/rateLimits/updated") {
|
|
1219
|
-
const limits = notification.params?.rateLimits;
|
|
1220
|
-
if (!limits?.limitId || limits.limitId === "codex") this.planUsage = codexPlanUsage(limits);
|
|
1221
|
-
}
|
|
447
|
+
this.auth.observeAccountNotification(notification);
|
|
1222
448
|
this.publishNotification(notification);
|
|
1223
449
|
});
|
|
1224
450
|
this.initializeResult = normalizeCodexAppServerInfo(initializeResult);
|
|
@@ -1229,68 +455,12 @@ export class CodexAppServerAgentProvider {
|
|
|
1229
455
|
};
|
|
1230
456
|
}
|
|
1231
457
|
|
|
1232
|
-
currentConnectionGeneration() {
|
|
1233
|
-
return this.connectionGeneration;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
458
|
publishNotification(notification) {
|
|
1237
|
-
const threadId = notification.params?.threadId;
|
|
1238
459
|
if (notification.method === "thread/goal/updated") {
|
|
1239
460
|
notification = { ...notification, params: { ...notification.params,
|
|
1240
|
-
goal: this.conversationRuntime.projectGoal(threadId, notification.params.goal) } };
|
|
1241
|
-
}
|
|
1242
|
-
const turnId = notification.params?.turnId;
|
|
1243
|
-
const item = notification.params?.item;
|
|
1244
|
-
if (item?.type === "commandExecution" && item.processId && item.source === "unifiedExecStartup") {
|
|
1245
|
-
const key = JSON.stringify([threadId, turnId, item.id]);
|
|
1246
|
-
if (notification.method === "item/completed") {
|
|
1247
|
-
const command = this.commandExecutions.get(key);
|
|
1248
|
-
if (command) command.completed = true;
|
|
1249
|
-
this.commandExecutions.delete(key);
|
|
1250
|
-
} else if (notification.method === "item/started") {
|
|
1251
|
-
const command = this.commandExecutions.get(key) || {
|
|
1252
|
-
threadId, turnId, itemId: item.id, processId: item.processId, client: this.client
|
|
1253
|
-
};
|
|
1254
|
-
this.commandExecutions.set(key, command);
|
|
1255
|
-
// Native startup may finish after turn/interrupt has already returned.
|
|
1256
|
-
if (this.interruptedTurns.has(JSON.stringify([threadId, turnId]))) {
|
|
1257
|
-
void this.#stopCommand(command).catch(() => null);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
const probe = this.threadControlProbes.get(threadId);
|
|
1262
|
-
const method = notification.method;
|
|
1263
|
-
if (probe && (/^(turn|item)\//u.test(method) || method === "thread/status/changed")) {
|
|
1264
|
-
const turnId = notification.params?.turnId || notification.params?.turn?.id;
|
|
1265
|
-
if (method === "turn/started" && probe.turnId && probe.turnId !== turnId) {
|
|
1266
|
-
this.threadControlProbes.delete(threadId);
|
|
1267
|
-
} else if (probe.verified) {
|
|
1268
|
-
if (turnId === probe.turnId) {
|
|
1269
|
-
if (method === "item/completed" && notification.params.item?.id === probe.itemId) {
|
|
1270
|
-
probe.result = notification.params.item;
|
|
1271
|
-
}
|
|
1272
|
-
if (method === "turn/completed") probe.completed = true;
|
|
1273
|
-
}
|
|
1274
|
-
return;
|
|
1275
|
-
} else {
|
|
1276
|
-
if (method === "turn/started") probe.turnId = turnId;
|
|
1277
|
-
probe.pending.push(notification);
|
|
1278
|
-
if (method === "item/started" || probe.pending.length >= 32) {
|
|
1279
|
-
const item = notification.params?.item;
|
|
1280
|
-
if (item?.type === "commandExecution" && item.command?.includes(probe.marker)) {
|
|
1281
|
-
probe.verified = true;
|
|
1282
|
-
probe.itemId = item.id;
|
|
1283
|
-
probe.pending = [];
|
|
1284
|
-
} else {
|
|
1285
|
-
// A concurrent native user turn must retain every event. Only a
|
|
1286
|
-
// positively identified control probe may be hidden from chat.
|
|
1287
|
-
this.threadControlProbes.delete(threadId);
|
|
1288
|
-
for (const pending of probe.pending) this.publishNotification(pending);
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
461
|
+
goal: this.conversationRuntime.projectGoal(notification.params?.threadId, notification.params.goal) } };
|
|
1293
462
|
}
|
|
463
|
+
if (!this.turns.observeNotification(notification)) return;
|
|
1294
464
|
for (const subscriber of this.notificationSubscribers) {
|
|
1295
465
|
try {
|
|
1296
466
|
subscriber(notification);
|
|
@@ -1300,15 +470,6 @@ export class CodexAppServerAgentProvider {
|
|
|
1300
470
|
}
|
|
1301
471
|
}
|
|
1302
472
|
|
|
1303
|
-
currentServerInfo() {
|
|
1304
|
-
return this.initializeResult;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
isControlProbeTurn(threadId, turnId) {
|
|
1308
|
-
const probe = this.threadControlProbes.get(threadId);
|
|
1309
|
-
return Boolean(turnId && probe?.verified && probe.turnId === turnId);
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
473
|
async currentRuntimeInfo() {
|
|
1313
474
|
const runtime = this.runtime || {};
|
|
1314
475
|
const executionMode = codexAppServerExecutionMode(this.options);
|
|
@@ -1358,13 +519,9 @@ export class CodexAppServerAgentProvider {
|
|
|
1358
519
|
});
|
|
1359
520
|
}
|
|
1360
521
|
|
|
1361
|
-
isAvailable() {
|
|
1362
|
-
return Boolean(this.client?.isOpen?.() && this.runtime);
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
522
|
async activeClient() {
|
|
1366
523
|
if (this.observationFailure) throw this.observationFailure;
|
|
1367
|
-
if (this.client?.isOpen
|
|
524
|
+
if (this.client?.isOpen()) {
|
|
1368
525
|
return this.client;
|
|
1369
526
|
}
|
|
1370
527
|
await this.connect();
|
|
@@ -1406,45 +563,6 @@ export class CodexAppServerAgentProvider {
|
|
|
1406
563
|
}
|
|
1407
564
|
}
|
|
1408
565
|
|
|
1409
|
-
async readPlanUsage() {
|
|
1410
|
-
const client = this.client;
|
|
1411
|
-
if (!client?.isOpen?.()) return { status: "unavailable", windows: [] };
|
|
1412
|
-
if (this.planUsage && Date.now() - this.planUsage.checkedAt < 60_000) return this.planUsage;
|
|
1413
|
-
if (this.planUsagePending) return this.planUsagePending;
|
|
1414
|
-
const previous = this.planUsage;
|
|
1415
|
-
const authGeneration = this.planUsageAuthGeneration;
|
|
1416
|
-
const signal = AbortSignal.timeout(10_000);
|
|
1417
|
-
const operation = (async () => {
|
|
1418
|
-
try {
|
|
1419
|
-
const account = await client.request("account/read", { refreshToken: false }, { signal });
|
|
1420
|
-
if (this.client !== client || this.planUsageAuthGeneration !== authGeneration) return { status: "unavailable", windows: [] };
|
|
1421
|
-
if (account?.account?.type !== "chatgpt") {
|
|
1422
|
-
this.planUsage = { status: "unsupported", windows: [], checkedAt: Date.now() };
|
|
1423
|
-
} else {
|
|
1424
|
-
const result = await client.request("account/rateLimits/read", {}, { signal });
|
|
1425
|
-
if (this.client !== client || this.planUsageAuthGeneration !== authGeneration) return { status: "unavailable", windows: [] };
|
|
1426
|
-
// A live event received during the read is newer than this request.
|
|
1427
|
-
if (this.planUsage === previous) this.planUsage = codexPlanUsage(result.rateLimitsByLimitId?.codex || result.rateLimits);
|
|
1428
|
-
}
|
|
1429
|
-
return this.planUsage;
|
|
1430
|
-
} catch {
|
|
1431
|
-
return { status: "unavailable", windows: [] };
|
|
1432
|
-
}
|
|
1433
|
-
})();
|
|
1434
|
-
this.planUsagePending = operation;
|
|
1435
|
-
try { return await operation; }
|
|
1436
|
-
finally { if (this.planUsagePending === operation) this.planUsagePending = null; }
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
subscribe(callback) {
|
|
1440
|
-
if (typeof callback !== "function") {
|
|
1441
|
-
throw new TypeError("Codex app-server observer must be a function.");
|
|
1442
|
-
}
|
|
1443
|
-
// Observation belongs to this provider, not one replaceable socket.
|
|
1444
|
-
this.notificationSubscribers.add(callback);
|
|
1445
|
-
return () => this.notificationSubscribers.delete(callback);
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
566
|
failObservation(cause) {
|
|
1449
567
|
const message = cause?.code === `${this.errorPrefix}agent_control_recovery_failed`
|
|
1450
568
|
? cause.message
|
|
@@ -1453,51 +571,20 @@ export class CodexAppServerAgentProvider {
|
|
|
1453
571
|
new Error(message, { cause }),
|
|
1454
572
|
{ code: `${this.errorPrefix}codex_observation_lost` }
|
|
1455
573
|
);
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
async stopThreadForObservationLoss(threadId, turnId) {
|
|
1460
|
-
// Control uses only this existing socket. It must not spawn or resume work
|
|
1461
|
-
// to find out whether the work we lost sight of has stopped.
|
|
1462
|
-
const client = this.client;
|
|
1463
|
-
if (!client?.isOpen()) throw new Error("Codex control connection is unavailable.");
|
|
1464
|
-
if (this.commandStopFailure) throw this.commandStopFailure;
|
|
1465
|
-
const signal = AbortSignal.timeout(5_000);
|
|
1466
|
-
const request = (method, params) => client.request(method, params, { signal });
|
|
1467
|
-
let { goal } = await request("thread/goal/get", { threadId });
|
|
1468
|
-
if (goal?.status === "active") {
|
|
1469
|
-
const result = await request("thread/goal/set", { threadId, status: "paused" });
|
|
1470
|
-
if (result.goal?.status !== "paused" || result.goal?.createdAt !== goal.createdAt || result.goal?.objective !== goal.objective) {
|
|
1471
|
-
throw new Error("Codex did not confirm that the same goal was paused.");
|
|
1472
|
-
}
|
|
1473
|
-
goal = result.goal;
|
|
1474
|
-
}
|
|
1475
|
-
const readStatus = async () => {
|
|
1476
|
-
const { thread } = await request("thread/read", { threadId, includeTurns: false });
|
|
1477
|
-
return typeof thread?.status === "string" ? thread.status : thread?.status?.type;
|
|
1478
|
-
};
|
|
1479
|
-
if (!["idle", "notLoaded"].includes(await readStatus())) {
|
|
1480
|
-
if (!turnId) {
|
|
1481
|
-
const page = await request("thread/turns/list", { threadId, limit: 1, sortDirection: "desc", itemsView: "summary" });
|
|
1482
|
-
turnId = page.data?.[0]?.id;
|
|
1483
|
-
}
|
|
1484
|
-
if (!turnId) throw new Error("Codex's running turn could not be identified.");
|
|
1485
|
-
await this.#interruptTurn(client, threadId, turnId, signal);
|
|
1486
|
-
if (await readStatus() !== "idle") throw new Error("Codex did not confirm that its turn stopped.");
|
|
1487
|
-
}
|
|
1488
|
-
return { goal };
|
|
574
|
+
const { onObservationLost = reportNoObservationLoss } = this.options;
|
|
575
|
+
return onObservationLost(this.observationFailure);
|
|
1489
576
|
}
|
|
1490
577
|
|
|
1491
578
|
setServerRequestHandler(callback) {
|
|
1492
579
|
const handler = typeof callback === "function" ? callback : null;
|
|
1493
580
|
this.serverRequestHandler = handler;
|
|
1494
581
|
const client = this.client;
|
|
1495
|
-
client?.setRequestHandler
|
|
582
|
+
client?.setRequestHandler((request) => this.handleServerRequest(request, client));
|
|
1496
583
|
return () => {
|
|
1497
584
|
if (this.serverRequestHandler === handler) {
|
|
1498
585
|
this.serverRequestHandler = null;
|
|
1499
586
|
const client = this.client;
|
|
1500
|
-
client?.setRequestHandler
|
|
587
|
+
client?.setRequestHandler((request) => this.handleServerRequest(request, client));
|
|
1501
588
|
}
|
|
1502
589
|
};
|
|
1503
590
|
}
|
|
@@ -1507,12 +594,11 @@ export class CodexAppServerAgentProvider {
|
|
|
1507
594
|
throw new TypeError("Codex tool dispatch requires its exact thread and handler.");
|
|
1508
595
|
}
|
|
1509
596
|
if (this.threadRequestHandlers.has(threadId)) throw new Error("This Codex thread already has an application tool owner.");
|
|
1510
|
-
const runInContext = this.options
|
|
1511
|
-
const registration = { handler:
|
|
1512
|
-
? request => runInContext(() => handler(request)) : handler };
|
|
597
|
+
const { runInContext = runDirectly } = this.options;
|
|
598
|
+
const registration = { handler: request => runInContext(() => handler(request)) };
|
|
1513
599
|
this.threadRequestHandlers.set(threadId, registration);
|
|
1514
600
|
const client = this.client;
|
|
1515
|
-
client?.setRequestHandler
|
|
601
|
+
client?.setRequestHandler(request => this.handleServerRequest(request, client));
|
|
1516
602
|
return Object.freeze({
|
|
1517
603
|
isCurrent: () => this.threadRequestHandlers.get(threadId) === registration,
|
|
1518
604
|
release: () => {
|
|
@@ -1521,756 +607,26 @@ export class CodexAppServerAgentProvider {
|
|
|
1521
607
|
});
|
|
1522
608
|
}
|
|
1523
609
|
|
|
1524
|
-
async startThread({ hostContext, ...params } = {}) {
|
|
1525
|
-
params = await this.conversationRuntime.prepareInstructions({ ...params, hostContext });
|
|
1526
|
-
delete params.hostContext;
|
|
1527
|
-
params = await this.options.prepareThreadParams?.(params) || params;
|
|
1528
|
-
const client = await this.activeClient();
|
|
1529
|
-
params = await this.withHistoryAdapter(params, client);
|
|
1530
|
-
const environment = this.options.prepareThreadEnvironment
|
|
1531
|
-
? await this.options.prepareThreadEnvironment(this.options.threadEnv || {})
|
|
1532
|
-
: this.options.threadEnv;
|
|
1533
|
-
const requestParams = this.conversationRuntime.threadParameters(params, environment);
|
|
1534
|
-
if (params.ephemeral !== true) requestParams.historyMode = "paginated";
|
|
1535
|
-
const response = await this.runRequest(
|
|
1536
|
-
() => client.request("thread/start", requestParams),
|
|
1537
|
-
"codex-app-server-thread-start"
|
|
1538
|
-
);
|
|
1539
|
-
if (hostContext) {
|
|
1540
|
-
await this.options.bindThreadContext(response?.thread?.id, hostContext, params);
|
|
1541
|
-
}
|
|
1542
|
-
// Native ephemeral helpers have no stored history and report "legacy".
|
|
1543
|
-
// Paginated history is required only for persistent conversations.
|
|
1544
|
-
if (params.ephemeral !== true && response?.thread?.historyMode === "legacy") {
|
|
1545
|
-
throw Object.assign(new Error("Codex did not create the required paginated conversation history. Update Codex before starting a conversation."), {
|
|
1546
|
-
code: `${this.errorPrefix}codex_history_unsupported`
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
if (response?.thread?.historyMode === "paginated" && response.thread.ephemeral !== true) {
|
|
1550
|
-
const threadId = normalizeAgentText(response.thread.id);
|
|
1551
|
-
// A new paginated thread needs its metadata and empty rollout persisted
|
|
1552
|
-
// before it can be resumed. This initializes only the just-created,
|
|
1553
|
-
// empty thread; it never hydrates an existing conversation.
|
|
1554
|
-
await this.runRequest(async () => {
|
|
1555
|
-
await client.request("thread/name/set", { threadId, name: threadId });
|
|
1556
|
-
await client.request("thread/read", { threadId, includeTurns: true });
|
|
1557
|
-
}, "codex-app-server-thread-initialize-history");
|
|
1558
|
-
}
|
|
1559
|
-
if (this.options.prepareThreadEnvironment && response?.thread?.id) {
|
|
1560
|
-
this.conversationRuntime.registerThread(response.thread.id, {
|
|
1561
|
-
client, executionId: normalizeAgentText(this.runtime?.executionId),
|
|
1562
|
-
params: requestParams, managed: !Object.hasOwn(params.config || {}, "shell_environment_policy")
|
|
1563
|
-
});
|
|
1564
|
-
}
|
|
1565
|
-
return {
|
|
1566
|
-
...normalizeAgentThread({
|
|
1567
|
-
id: response?.thread?.id,
|
|
1568
|
-
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
1569
|
-
raw: response?.thread
|
|
1570
|
-
}),
|
|
1571
|
-
response
|
|
1572
|
-
};
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
async resumeThread(threadId = "", { hostContext, ...params } = {}) {
|
|
1576
|
-
if (hostContext) await this.options.bindThreadContext(threadId, hostContext, params);
|
|
1577
|
-
params = await this.conversationRuntime.prepareInstructions({ ...params, hostContext }, threadId);
|
|
1578
|
-
delete params.hostContext;
|
|
1579
|
-
params = await this.options.prepareThreadParams?.(params) || params;
|
|
1580
|
-
await this.options.beforeResumeThread?.(threadId);
|
|
1581
|
-
const client = await this.activeClient();
|
|
1582
|
-
let response;
|
|
1583
|
-
if (this.options.prepareThreadEnvironment) {
|
|
1584
|
-
response = await this.withThreadEnvironment(threadId, params, (requestParams, resumed) =>
|
|
1585
|
-
resumed || client.request("thread/resume", { excludeTurns: true, ...requestParams, threadId }));
|
|
1586
|
-
} else {
|
|
1587
|
-
params = await this.withHistoryAdapter(params, client, { threadId });
|
|
1588
|
-
const requestParams = codexAppServerThreadRequestParams(params, this.options.threadEnv);
|
|
1589
|
-
response = await this.runRequest(
|
|
1590
|
-
() => client.request("thread/resume", {
|
|
1591
|
-
excludeTurns: true,
|
|
1592
|
-
...requestParams,
|
|
1593
|
-
threadId: normalizeAgentText(threadId || requestParams.threadId)
|
|
1594
|
-
}),
|
|
1595
|
-
"codex-app-server-thread-resume"
|
|
1596
|
-
);
|
|
1597
|
-
}
|
|
1598
|
-
return {
|
|
1599
|
-
...normalizeAgentThread({
|
|
1600
|
-
id: response?.thread?.id,
|
|
1601
|
-
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
1602
|
-
raw: response?.thread
|
|
1603
|
-
}),
|
|
1604
|
-
response
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
async withHistoryAdapter(params, client, options = {}) {
|
|
1609
|
-
return prepareCodexHistory(params, client, { ...options,
|
|
1610
|
-
baseUrl: this.runtime?.historyAdapterBaseUrl, modelProviderId: this.options.modelProviderId });
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
async withThreadEnvironment(threadId, params, operation) {
|
|
1614
|
-
try {
|
|
1615
|
-
return await this.conversationRuntime.withThreadContext(threadId, params, operation);
|
|
1616
|
-
} catch (error) {
|
|
1617
|
-
if (typeof error.code === "string" && error.code.startsWith("assistant_")) error.code = `${this.errorPrefix}${error.code.slice(10)}`;
|
|
1618
|
-
throw error;
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
async #verifyThreadEnvironment(threadId, environment, request, signal, { requiredKeys = [] } = {}) {
|
|
1623
|
-
const keys = Object.keys(environment).filter((key) => key.startsWith(this.configuration.controlEnvironmentPrefix) || requiredKeys.includes(key)).sort();
|
|
1624
|
-
if (!keys.length) return;
|
|
1625
|
-
|
|
1626
|
-
const marker = `${this.configuration.controlProbePrefix}${randomUUID()}:`;
|
|
1627
|
-
const probe = { marker, pending: [], verified: false, turnId: "" };
|
|
1628
|
-
this.threadControlProbes.set(threadId, probe);
|
|
1629
|
-
const expected = createHash("sha256").update(JSON.stringify(keys.map((key) => [key, environment[key]]))).digest("hex");
|
|
1630
|
-
// This bounded native shell check uses no model and prints only a digest,
|
|
1631
|
-
// never credentials or environment values.
|
|
1632
|
-
await request("thread/shellCommand", {
|
|
1633
|
-
threadId, command: codexControlProbeCommand(keys, marker), timeoutMs: 2000
|
|
1634
|
-
});
|
|
1635
|
-
while (true) {
|
|
1636
|
-
signal.throwIfAborted();
|
|
1637
|
-
// The native completion event owns this result; saved history is not a
|
|
1638
|
-
// second acknowledgement of this live control operation.
|
|
1639
|
-
if (probe.completed && probe.result) {
|
|
1640
|
-
if (probe.result.type !== "commandExecution" || probe.result.exitCode !== 0 ||
|
|
1641
|
-
probe.result.aggregatedOutput?.trim() !== `${marker}${expected}`) {
|
|
1642
|
-
throw Object.assign(new Error("Another native subscriber retained the previous assistant settings. Close the native assistant terminal and retry Resume."), {
|
|
1643
|
-
code: "assistant_agent_control_binding_retained"
|
|
1644
|
-
});
|
|
1645
|
-
}
|
|
1646
|
-
return;
|
|
1647
|
-
}
|
|
1648
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
async ensureThreadControls(threadId) {
|
|
1653
|
-
if (!this.options.prepareThreadEnvironment) return;
|
|
1654
|
-
return this.withThreadEnvironment(threadId, {}, async (_params, resumed) => ({ ok: true, recovered: Boolean(resumed) }));
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
async readGoal(threadId = "", { signal } = {}) {
|
|
1658
|
-
const client = await this.activeClient();
|
|
1659
|
-
const result = await this.runRequest(
|
|
1660
|
-
() => client.request("thread/goal/get", { threadId: normalizeAgentText(threadId) }, { signal }),
|
|
1661
|
-
"codex-app-server-goal-read"
|
|
1662
|
-
);
|
|
1663
|
-
return { ...result, goal: this.conversationRuntime.projectGoal(threadId, result.goal) };
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
async updateThreadSettings(threadId, { model, effort } = {}, { signal } = {}) {
|
|
1667
|
-
if (!normalizeAgentText(threadId) || !normalizeAgentText(model)) {
|
|
1668
|
-
throw new TypeError("Codex settings require an existing thread and a model.");
|
|
1669
|
-
}
|
|
1670
|
-
const client = await this.activeClient();
|
|
1671
|
-
threadId = normalizeAgentText(threadId);
|
|
1672
|
-
const requested = { model, effort, ...(effort ? { config: { model_reasoning_effort: effort } } : {}) };
|
|
1673
|
-
const params = await this.options.prepareThreadParams?.(requested) || requested;
|
|
1674
|
-
const update = async selected => {
|
|
1675
|
-
const current = await this.activeClient();
|
|
1676
|
-
const applied = Promise.withResolvers();
|
|
1677
|
-
// A successful RPC only queues the update. Observe its applied snapshot
|
|
1678
|
-
// before resuming a goal or accepting a dependent settings operation.
|
|
1679
|
-
const deadline = AbortSignal.any([AbortSignal.timeout(this.options.requestTimeoutMs || 10_000), ...(signal ? [signal] : [])]);
|
|
1680
|
-
const abort = () => applied.reject(deadline.reason);
|
|
1681
|
-
const unsubscribe = current.subscribe(notification => {
|
|
1682
|
-
const settings = notification.params?.threadSettings;
|
|
1683
|
-
if (notification.method === "thread/settings/updated" && notification.params?.threadId === threadId &&
|
|
1684
|
-
settings?.model === selected.model && (!selected.effort || settings.effort === selected.effort)) applied.resolve();
|
|
1685
|
-
});
|
|
1686
|
-
this.settingsWaiters.add(applied.reject);
|
|
1687
|
-
deadline.addEventListener("abort", abort, { once: true });
|
|
1688
|
-
void applied.promise.catch(() => {});
|
|
1689
|
-
try {
|
|
1690
|
-
deadline.throwIfAborted();
|
|
1691
|
-
const { thread } = await this.runRequest(() => current.request("thread/read", {
|
|
1692
|
-
threadId, includeTurns: false
|
|
1693
|
-
}, { signal: deadline }), "codex-app-server-settings-read");
|
|
1694
|
-
// Native no-op updates may emit no settings event. A current snapshot
|
|
1695
|
-
// also confirms values already applied by the provider reload.
|
|
1696
|
-
if (thread?.model === selected.model && (!selected.effort || thread.reasoningEffort === selected.effort) &&
|
|
1697
|
-
(!selected.modelProvider || thread.modelProvider === selected.modelProvider)) return {};
|
|
1698
|
-
const result = await this.runRequest(() => current.request("thread/settings/update", {
|
|
1699
|
-
threadId, model: selected.model, ...(selected.effort ? { effort: selected.effort } : {})
|
|
1700
|
-
}, { signal: deadline }), "codex-app-server-settings-update");
|
|
1701
|
-
await applied.promise;
|
|
1702
|
-
return result;
|
|
1703
|
-
} finally {
|
|
1704
|
-
unsubscribe();
|
|
1705
|
-
deadline.removeEventListener("abort", abort);
|
|
1706
|
-
this.settingsWaiters.delete(applied.reject);
|
|
1707
|
-
}
|
|
1708
|
-
};
|
|
1709
|
-
if (params.modelProvider || this.conversationRuntime.hasPendingSettings(threadId)) {
|
|
1710
|
-
const { thread } = await client.request("thread/read", { threadId, includeTurns: false }, { signal });
|
|
1711
|
-
if (params.modelProvider !== thread?.modelProvider || this.conversationRuntime.hasPendingSettings(threadId)) {
|
|
1712
|
-
return this.conversationRuntime.scheduleSettings(threadId, params, update);
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
return update(params);
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
async setGoal(threadId, { objective, tokenBudget } = {}, { signal } = {}) {
|
|
1719
|
-
if (typeof objective !== "string" || !objective.trim() ||
|
|
1720
|
-
(tokenBudget !== undefined && (!Number.isSafeInteger(tokenBudget) || tokenBudget <= 0))) {
|
|
1721
|
-
throw new TypeError("A Codex goal requires an objective and an optional positive token budget.");
|
|
1722
|
-
}
|
|
1723
|
-
this.conversationRuntime.goalChanged(threadId);
|
|
1724
|
-
const operation = async () => {
|
|
1725
|
-
const client = await this.activeClient();
|
|
1726
|
-
return client.request("thread/goal/set", {
|
|
1727
|
-
threadId: normalizeAgentText(threadId), objective: objective.trim(), status: "active",
|
|
1728
|
-
...(tokenBudget === undefined ? {} : { tokenBudget })
|
|
1729
|
-
}, { signal });
|
|
1730
|
-
};
|
|
1731
|
-
return this.options.prepareThreadEnvironment
|
|
1732
|
-
? this.withThreadEnvironment(threadId, {}, operation)
|
|
1733
|
-
: this.runRequest(operation, "codex-app-server-goal-set");
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
async setGoalStatus(threadId = "", status = "", { signal } = {}) {
|
|
1737
|
-
if (!["active", "paused"].includes(status)) {
|
|
1738
|
-
throw new TypeError("Invalid Codex goal status.");
|
|
1739
|
-
}
|
|
1740
|
-
this.conversationRuntime.goalChanged(threadId);
|
|
1741
|
-
const operation = async () => {
|
|
1742
|
-
const client = await this.activeClient();
|
|
1743
|
-
return client.request("thread/goal/set", { threadId: normalizeAgentText(threadId), status }, { signal });
|
|
1744
|
-
};
|
|
1745
|
-
return status === "active" && this.options.prepareThreadEnvironment
|
|
1746
|
-
? this.withThreadEnvironment(threadId, {}, operation)
|
|
1747
|
-
: this.runRequest(operation, "codex-app-server-goal-status");
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
async clearGoal(threadId = "", { signal } = {}) {
|
|
1751
|
-
this.conversationRuntime.goalChanged(threadId);
|
|
1752
|
-
const client = await this.activeClient();
|
|
1753
|
-
return this.runRequest(
|
|
1754
|
-
() => client.request("thread/goal/clear", { threadId: normalizeAgentText(threadId) }, { signal }),
|
|
1755
|
-
"codex-app-server-goal-clear"
|
|
1756
|
-
);
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
async readThread(threadId = "") {
|
|
1760
|
-
const client = await this.activeClient();
|
|
1761
|
-
const response = await this.runRequest(
|
|
1762
|
-
() => client.request("thread/read", {
|
|
1763
|
-
includeTurns: true,
|
|
1764
|
-
threadId: normalizeAgentText(threadId)
|
|
1765
|
-
}),
|
|
1766
|
-
"codex-app-server-thread-read"
|
|
1767
|
-
);
|
|
1768
|
-
return {
|
|
1769
|
-
...normalizeAgentThread({
|
|
1770
|
-
id: response?.thread?.id || threadId,
|
|
1771
|
-
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
1772
|
-
raw: response?.thread || response
|
|
1773
|
-
}),
|
|
1774
|
-
response
|
|
1775
|
-
};
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
async readThreadStatus(threadId = "") {
|
|
1779
|
-
const client = await this.activeClient();
|
|
1780
|
-
const response = await this.runRequest(
|
|
1781
|
-
() => client.request("thread/read", {
|
|
1782
|
-
includeTurns: false,
|
|
1783
|
-
threadId: normalizeAgentText(threadId)
|
|
1784
|
-
}),
|
|
1785
|
-
"codex-app-server-thread-status"
|
|
1786
|
-
);
|
|
1787
|
-
return {
|
|
1788
|
-
...normalizeAgentThread({
|
|
1789
|
-
id: response?.thread?.id || threadId,
|
|
1790
|
-
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
1791
|
-
raw: response?.thread || response
|
|
1792
|
-
}),
|
|
1793
|
-
response
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
async listThreadTurns(threadId = "", params = {}) {
|
|
1798
|
-
const client = await this.activeClient();
|
|
1799
|
-
return this.runRequest(
|
|
1800
|
-
() => client.request("thread/turns/list", {
|
|
1801
|
-
...params,
|
|
1802
|
-
threadId: normalizeAgentText(threadId || params.threadId)
|
|
1803
|
-
}),
|
|
1804
|
-
"codex-app-server-thread-turns-list"
|
|
1805
|
-
);
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
async exportThreadHistory(threadId, onRecord, { signal } = {}) {
|
|
1809
|
-
const active = await this.activeClient();
|
|
1810
|
-
// Oversized native items must fail preservation without disconnecting the
|
|
1811
|
-
// shared conversation observer. Reuse JSKIT's transport on a separate socket.
|
|
1812
|
-
const client = new CodexAppServerJsonRpcClient({
|
|
1813
|
-
endpoint: active.endpoint,
|
|
1814
|
-
maxMessageBytes: 64 * 1024 ** 2,
|
|
1815
|
-
requestTimeoutMs: 30_000,
|
|
1816
|
-
WebSocketImpl: this.options.WebSocketImpl
|
|
1817
|
-
});
|
|
1818
|
-
const deadline = AbortSignal.timeout(300_000);
|
|
1819
|
-
const boundedSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
1820
|
-
boundedSignal.throwIfAborted();
|
|
1821
|
-
const abort = () => client.close();
|
|
1822
|
-
boundedSignal.addEventListener("abort", abort, { once: true });
|
|
1823
|
-
try {
|
|
1824
|
-
await client.connect();
|
|
1825
|
-
boundedSignal.throwIfAborted();
|
|
1826
|
-
await client.initialize({ clientInfo: this.configuration.historyClientInfo });
|
|
1827
|
-
boundedSignal.throwIfAborted();
|
|
1828
|
-
return await exportCodexNativeHistory(client, normalizeAgentText(threadId), onRecord, { signal: boundedSignal, errorPrefix: this.errorPrefix });
|
|
1829
|
-
} finally {
|
|
1830
|
-
boundedSignal.removeEventListener("abort", abort);
|
|
1831
|
-
client.close();
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
async listLoadedThreads(params = {}) {
|
|
1836
|
-
const client = await this.activeClient();
|
|
1837
|
-
return this.runRequest(
|
|
1838
|
-
() => client.request("thread/loaded/list", params),
|
|
1839
|
-
"codex-app-server-thread-loaded-list"
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
async listAppServerThreadsForCwd({
|
|
1844
|
-
cwd = "",
|
|
1845
|
-
signal = null
|
|
1846
|
-
} = {}) {
|
|
1847
|
-
const normalizedCwd = normalizeAgentText(cwd);
|
|
1848
|
-
if (!normalizedCwd || codexAppServerTextHasControlCharacters(normalizedCwd)) {
|
|
1849
|
-
const error = new Error("Codex session thread inventory requires an exact cwd.");
|
|
1850
|
-
error.code = `${this.errorPrefix}codex_session_thread_inventory_invalid`;
|
|
1851
|
-
throw error;
|
|
1852
|
-
}
|
|
1853
|
-
const client = await this.activeClient();
|
|
1854
|
-
const state = await listBoundedCodexAppServerThreadIds({
|
|
1855
|
-
archived: false,
|
|
1856
|
-
client,
|
|
1857
|
-
cwd: normalizedCwd,
|
|
1858
|
-
errorCode: `${this.errorPrefix}codex_session_thread_inventory_invalid`,
|
|
1859
|
-
label: "session",
|
|
1860
|
-
requestLabel: "codex-app-server-session-thread-list",
|
|
1861
|
-
runRequest: this.runRequest.bind(this),
|
|
1862
|
-
signal,
|
|
1863
|
-
state: {
|
|
1864
|
-
entryCount: 0,
|
|
1865
|
-
threadIds: new Set()
|
|
1866
|
-
},
|
|
1867
|
-
verifyCwd: true
|
|
1868
|
-
});
|
|
1869
|
-
return Object.freeze({
|
|
1870
|
-
cwd: normalizedCwd,
|
|
1871
|
-
threadIds: Object.freeze([...state.threadIds].sort())
|
|
1872
|
-
});
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
// Native thread/delete cascades through all spawned descendants, including
|
|
1876
|
-
// archived and non-app-server threads. Never infer its scope from cwd alone.
|
|
1877
|
-
async listThreadDescendants(threadId) {
|
|
1878
|
-
const ancestorThreadId = normalizeAgentText(threadId);
|
|
1879
|
-
if (!ancestorThreadId || ancestorThreadId.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH ||
|
|
1880
|
-
codexAppServerTextHasControlCharacters(ancestorThreadId)) {
|
|
1881
|
-
throw new TypeError("Codex descendant inventory requires an exact thread id.");
|
|
1882
|
-
}
|
|
1883
|
-
const client = await this.activeClient();
|
|
1884
|
-
const state = { entryCount: 0, threadIds: new Set() };
|
|
1885
|
-
for (const archived of [false, true]) {
|
|
1886
|
-
await listBoundedCodexAppServerThreadIds({
|
|
1887
|
-
ancestorThreadId, archived, client, state,
|
|
1888
|
-
sourceKinds: CODEX_NATIVE_STORAGE_SOURCE_KINDS,
|
|
1889
|
-
errorCode: `${this.errorPrefix}codex_retirement_inventory_invalid`, label: "retirement",
|
|
1890
|
-
requestLabel: "codex-app-server-descendant-list", runRequest: this.runRequest.bind(this)
|
|
1891
|
-
});
|
|
1892
|
-
}
|
|
1893
|
-
return [...state.threadIds].sort();
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
async listNativeThreadsForCwd(cwd) {
|
|
1897
|
-
if (!path.isAbsolute(cwd || "") || codexAppServerTextHasControlCharacters(cwd)) throw new TypeError("Native inventory requires an exact absolute directory.");
|
|
1898
|
-
const client = await this.activeClient();
|
|
1899
|
-
const state = { entryCount: 0, threadIds: new Set() };
|
|
1900
|
-
for (const archived of [false, true]) {
|
|
1901
|
-
await listBoundedCodexAppServerThreadIds({ archived, client, cwd, state, verifyCwd: true,
|
|
1902
|
-
sourceKinds: CODEX_NATIVE_STORAGE_SOURCE_KINDS,
|
|
1903
|
-
errorCode: `${this.errorPrefix}codex_retirement_inventory_invalid`, label: "native storage",
|
|
1904
|
-
requestLabel: "codex-app-server-native-storage-list", runRequest: this.runRequest.bind(this) });
|
|
1905
|
-
}
|
|
1906
|
-
return [...state.threadIds].sort();
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
async nativeThreadExists(threadId, { signal } = {}) {
|
|
1910
|
-
const id = normalizeAgentText(threadId);
|
|
1911
|
-
if (!id || id.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH || codexAppServerTextHasControlCharacters(id)) {
|
|
1912
|
-
throw new TypeError("Native existence checks require an exact thread id.");
|
|
1913
|
-
}
|
|
1914
|
-
const client = await this.activeClient();
|
|
1915
|
-
const state = { entryCount: 0, found: false, threadIds: new Set() };
|
|
1916
|
-
for (const archived of [false, true]) {
|
|
1917
|
-
await listBoundedCodexAppServerThreadIds({ archived, client, findThreadId: id, state, signal,
|
|
1918
|
-
sourceKinds: CODEX_NATIVE_STORAGE_SOURCE_KINDS,
|
|
1919
|
-
errorCode: `${this.errorPrefix}codex_retirement_inventory_invalid`, label: "native storage",
|
|
1920
|
-
requestLabel: "codex-app-server-native-existence", runRequest: this.runRequest.bind(this) });
|
|
1921
|
-
if (state.found) return true;
|
|
1922
|
-
}
|
|
1923
|
-
return false;
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
async listHelperThreads({
|
|
1927
|
-
signal = null
|
|
1928
|
-
} = {}) {
|
|
1929
|
-
const client = await this.activeClient();
|
|
1930
|
-
const execution = await this.currentHelperExecutionContext();
|
|
1931
|
-
const state = {
|
|
1932
|
-
entryCount: 0,
|
|
1933
|
-
threadIds: new Set()
|
|
1934
|
-
};
|
|
1935
|
-
for (const archived of [false, true]) {
|
|
1936
|
-
await listBoundedCodexAppServerThreadIds({
|
|
1937
|
-
archived,
|
|
1938
|
-
client,
|
|
1939
|
-
cwd: execution.cwd,
|
|
1940
|
-
errorCode: `${this.errorPrefix}codex_helper_thread_inventory_invalid`,
|
|
1941
|
-
label: "helper",
|
|
1942
|
-
requestLabel: "codex-app-server-helper-thread-list",
|
|
1943
|
-
runRequest: this.runRequest.bind(this),
|
|
1944
|
-
signal,
|
|
1945
|
-
state
|
|
1946
|
-
});
|
|
1947
|
-
}
|
|
1948
|
-
return Object.freeze({
|
|
1949
|
-
threadIds: Object.freeze([...state.threadIds].sort())
|
|
1950
|
-
});
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
async listModels(params = {}, {
|
|
1954
|
-
signal = null
|
|
1955
|
-
} = {}) {
|
|
1956
|
-
const client = await this.activeClient();
|
|
1957
|
-
const data = [];
|
|
1958
|
-
const seenCursors = new Set();
|
|
1959
|
-
const includeHidden = params.includeHidden === true;
|
|
1960
|
-
const limit = normalizePositiveInteger(
|
|
1961
|
-
params.limit,
|
|
1962
|
-
{ fallback: CODEX_APP_SERVER_MODEL_CATALOG_PAGE_LIMIT }
|
|
1963
|
-
);
|
|
1964
|
-
const boundedLimit = Math.min(limit, CODEX_APP_SERVER_MODEL_CATALOG_PAGE_LIMIT);
|
|
1965
|
-
let totalBytes = 0;
|
|
1966
|
-
let cursor = params.cursor === undefined || params.cursor === null || params.cursor === ""
|
|
1967
|
-
? ""
|
|
1968
|
-
: typeof params.cursor === "string"
|
|
1969
|
-
? params.cursor.trim()
|
|
1970
|
-
: null;
|
|
1971
|
-
if (cursor === null || cursor.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH) {
|
|
1972
|
-
throw this.modelCatalogError("Codex model catalog cursor is invalid.");
|
|
1973
|
-
}
|
|
1974
|
-
for (let page = 0; page < CODEX_APP_SERVER_MODEL_CATALOG_MAX_PAGES; page += 1) {
|
|
1975
|
-
if (cursor && seenCursors.has(cursor)) {
|
|
1976
|
-
throw this.modelCatalogError("Codex model catalog repeated a pagination cursor.");
|
|
1977
|
-
}
|
|
1978
|
-
if (cursor) {
|
|
1979
|
-
seenCursors.add(cursor);
|
|
1980
|
-
}
|
|
1981
|
-
const response = await this.runRequest(
|
|
1982
|
-
() => client.request("model/list", {
|
|
1983
|
-
...(cursor ? { cursor } : {}),
|
|
1984
|
-
includeHidden,
|
|
1985
|
-
limit: boundedLimit
|
|
1986
|
-
}, { signal }),
|
|
1987
|
-
"codex-app-server-model-list"
|
|
1988
|
-
);
|
|
1989
|
-
if (!Array.isArray(response?.data)) {
|
|
1990
|
-
throw this.modelCatalogError(
|
|
1991
|
-
"Codex model catalog response did not contain a data array."
|
|
1992
|
-
);
|
|
1993
|
-
}
|
|
1994
|
-
if (
|
|
1995
|
-
response.data.length > boundedLimit ||
|
|
1996
|
-
data.length + response.data.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRIES
|
|
1997
|
-
) {
|
|
1998
|
-
throw this.modelCatalogError("Codex model catalog exceeded its entry limit.");
|
|
1999
|
-
}
|
|
2000
|
-
for (const entry of response.data) {
|
|
2001
|
-
if (!isRecord(entry)) {
|
|
2002
|
-
throw this.modelCatalogError("Codex model catalog contained an invalid entry.");
|
|
2003
|
-
}
|
|
2004
|
-
let entryBytes = 0;
|
|
2005
|
-
try {
|
|
2006
|
-
entryBytes = Buffer.byteLength(JSON.stringify(entry), "utf8");
|
|
2007
|
-
} catch {
|
|
2008
|
-
throw this.modelCatalogError("Codex model catalog contained an invalid entry.");
|
|
2009
|
-
}
|
|
2010
|
-
if (
|
|
2011
|
-
entryBytes > CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRY_BYTES ||
|
|
2012
|
-
totalBytes + entryBytes > CODEX_APP_SERVER_MODEL_CATALOG_MAX_TOTAL_BYTES
|
|
2013
|
-
) {
|
|
2014
|
-
throw this.modelCatalogError("Codex model catalog exceeded its response limit.");
|
|
2015
|
-
}
|
|
2016
|
-
totalBytes += entryBytes;
|
|
2017
|
-
data.push(entry);
|
|
2018
|
-
}
|
|
2019
|
-
const nextCursor = response.nextCursor === undefined || response.nextCursor === null || response.nextCursor === ""
|
|
2020
|
-
? ""
|
|
2021
|
-
: typeof response.nextCursor === "string"
|
|
2022
|
-
? response.nextCursor.trim()
|
|
2023
|
-
: null;
|
|
2024
|
-
if (nextCursor === null || nextCursor.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH) {
|
|
2025
|
-
throw this.modelCatalogError("Codex model catalog cursor is invalid.");
|
|
2026
|
-
}
|
|
2027
|
-
if (!nextCursor) {
|
|
2028
|
-
return {
|
|
2029
|
-
data,
|
|
2030
|
-
nextCursor: null
|
|
2031
|
-
};
|
|
2032
|
-
}
|
|
2033
|
-
if (seenCursors.has(nextCursor)) {
|
|
2034
|
-
throw this.modelCatalogError("Codex model catalog repeated a pagination cursor.");
|
|
2035
|
-
}
|
|
2036
|
-
cursor = nextCursor;
|
|
2037
|
-
}
|
|
2038
|
-
throw this.modelCatalogError(
|
|
2039
|
-
`Codex model catalog exceeded ${CODEX_APP_SERVER_MODEL_CATALOG_MAX_PAGES} pages.`
|
|
2040
|
-
);
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
async readConfig(params = {}) {
|
|
2044
|
-
const client = await this.activeClient();
|
|
2045
|
-
const cwd = normalizeAgentText(params.cwd);
|
|
2046
|
-
return this.runRequest(
|
|
2047
|
-
() => client.request("config/read", {
|
|
2048
|
-
...(cwd ? { cwd } : {}),
|
|
2049
|
-
includeLayers: params.includeLayers === true
|
|
2050
|
-
}),
|
|
2051
|
-
"codex-app-server-config-read"
|
|
2052
|
-
);
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
async trustProject(cwd = "") {
|
|
2056
|
-
if (!path.isAbsolute(cwd)) throw new TypeError("Codex project trust requires an absolute worktree path.");
|
|
2057
|
-
const projectRoot = path.resolve(cwd);
|
|
2058
|
-
const client = await this.activeClient();
|
|
2059
|
-
return this.runRequest(async () => {
|
|
2060
|
-
const { config } = await client.request("config/read", { cwd: projectRoot, includeLayers: false });
|
|
2061
|
-
if (config?.projects?.[projectRoot]?.trust_level === "trusted") return;
|
|
2062
|
-
// Codex discovers project hooks before applying thread config overrides.
|
|
2063
|
-
// Trust only this managed worktree through its native configuration API.
|
|
2064
|
-
await client.request("config/batchWrite", {
|
|
2065
|
-
edits: [{
|
|
2066
|
-
keyPath: `projects.${JSON.stringify(projectRoot)}.trust_level`,
|
|
2067
|
-
value: "trusted",
|
|
2068
|
-
mergeStrategy: "upsert"
|
|
2069
|
-
}],
|
|
2070
|
-
expectedVersion: null,
|
|
2071
|
-
filePath: null,
|
|
2072
|
-
reloadUserConfig: true
|
|
2073
|
-
});
|
|
2074
|
-
}, "codex-app-server-project-trust");
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
async listHooks(cwds = []) {
|
|
2078
|
-
const client = await this.activeClient();
|
|
2079
|
-
return this.runRequest(
|
|
2080
|
-
() => client.request("hooks/list", {
|
|
2081
|
-
cwds: (Array.isArray(cwds) ? cwds : [cwds])
|
|
2082
|
-
.map(normalizeAgentText)
|
|
2083
|
-
.filter(Boolean)
|
|
2084
|
-
}),
|
|
2085
|
-
"codex-app-server-hooks-list"
|
|
2086
|
-
);
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
async writeHookTrustState(state = {}) {
|
|
2090
|
-
if (!isRecord(state) || Object.keys(state).length === 0) {
|
|
2091
|
-
return null;
|
|
2092
|
-
}
|
|
2093
|
-
const client = await this.activeClient();
|
|
2094
|
-
return this.runRequest(
|
|
2095
|
-
() => client.request("config/batchWrite", {
|
|
2096
|
-
edits: [{
|
|
2097
|
-
keyPath: "hooks.state",
|
|
2098
|
-
mergeStrategy: "upsert",
|
|
2099
|
-
value: state
|
|
2100
|
-
}],
|
|
2101
|
-
expectedVersion: null,
|
|
2102
|
-
filePath: null,
|
|
2103
|
-
reloadUserConfig: true
|
|
2104
|
-
}),
|
|
2105
|
-
"codex-app-server-hook-trust-write"
|
|
2106
|
-
);
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
async unsubscribeThread(threadId = "") {
|
|
2110
|
-
const client = await this.activeClient();
|
|
2111
|
-
return this.runRequest(
|
|
2112
|
-
() => client.request("thread/unsubscribe", {
|
|
2113
|
-
threadId: normalizeAgentText(threadId)
|
|
2114
|
-
}),
|
|
2115
|
-
"codex-app-server-thread-unsubscribe"
|
|
2116
|
-
);
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
async deleteThread(threadId = "") {
|
|
2120
|
-
const client = await this.activeClient();
|
|
2121
|
-
return this.runRequest(
|
|
2122
|
-
() => client.request("thread/delete", {
|
|
2123
|
-
threadId: normalizeAgentText(threadId)
|
|
2124
|
-
}),
|
|
2125
|
-
"codex-app-server-thread-delete"
|
|
2126
|
-
);
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
async sendTurn(threadId = "", input = [], params = {}, { signal } = {}) {
|
|
2130
|
-
const operation = async () => {
|
|
2131
|
-
const client = await this.activeClient();
|
|
2132
|
-
return client.request("turn/start", {
|
|
2133
|
-
...params,
|
|
2134
|
-
input: codexTurnInput(input),
|
|
2135
|
-
threadId: normalizeAgentText(threadId || params.threadId)
|
|
2136
|
-
}, { signal });
|
|
2137
|
-
};
|
|
2138
|
-
const response = this.options.prepareThreadEnvironment
|
|
2139
|
-
? await this.withThreadEnvironment(threadId || params.threadId, {}, operation)
|
|
2140
|
-
: await this.runRequest(operation, "codex-app-server-turn-start");
|
|
2141
|
-
return {
|
|
2142
|
-
...normalizeAgentTurn({
|
|
2143
|
-
id: response?.turn?.id || response?.turnId,
|
|
2144
|
-
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
2145
|
-
raw: response?.turn || response
|
|
2146
|
-
}),
|
|
2147
|
-
response
|
|
2148
|
-
};
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
async steerTurn(threadId = "", turnId = "", input = [], params = {}, { signal } = {}) {
|
|
2152
|
-
const client = await this.activeClient();
|
|
2153
|
-
const response = await this.runRequest(
|
|
2154
|
-
() => client.request("turn/steer", {
|
|
2155
|
-
...params,
|
|
2156
|
-
expectedTurnId: normalizeAgentText(turnId || params.expectedTurnId),
|
|
2157
|
-
input: codexTurnInput(input),
|
|
2158
|
-
threadId: normalizeAgentText(threadId || params.threadId)
|
|
2159
|
-
}, { signal }),
|
|
2160
|
-
"codex-app-server-turn-steer"
|
|
2161
|
-
);
|
|
2162
|
-
return {
|
|
2163
|
-
...normalizeAgentTurn({
|
|
2164
|
-
id: response?.turn?.id || response?.turnId || turnId,
|
|
2165
|
-
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
2166
|
-
raw: response?.turn || response
|
|
2167
|
-
}),
|
|
2168
|
-
response
|
|
2169
|
-
};
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
interruptionOutcome(threadId = "", turnId = "") {
|
|
2173
|
-
const key = JSON.stringify([normalizeAgentText(threadId), normalizeAgentText(turnId)]);
|
|
2174
|
-
return this.interruptedTurns.get(key) || "";
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
async interruptTurn(threadId = "", turnId = "", { outcome = "" } = {}) {
|
|
2178
|
-
const client = await this.activeClient();
|
|
2179
|
-
return this.runRequest(
|
|
2180
|
-
() => this.#interruptTurn(client, normalizeAgentText(threadId), normalizeAgentText(turnId), undefined, outcome),
|
|
2181
|
-
"codex-app-server-turn-interrupt"
|
|
2182
|
-
);
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
async #interruptTurn(client, threadId, turnId, signal, outcome = "") {
|
|
2186
|
-
const key = JSON.stringify([threadId, turnId]);
|
|
2187
|
-
this.interruptedTurns.set(key, outcome || this.interruptedTurns.get(key) || "");
|
|
2188
|
-
const result = await client.request("turn/interrupt", { threadId, turnId }, { signal });
|
|
2189
|
-
await Promise.all([...this.commandExecutions.values()]
|
|
2190
|
-
.filter((command) => command.threadId === threadId && command.turnId === turnId)
|
|
2191
|
-
.map((command) => this.#stopCommand(command)));
|
|
2192
|
-
return result;
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
#stopCommand(command) {
|
|
2196
|
-
if (command.stopTask) return command.stopTask;
|
|
2197
|
-
command.stopTask = (async () => {
|
|
2198
|
-
const signal = AbortSignal.timeout(5000);
|
|
2199
|
-
const request = (method, params) => command.client.request(method, params, { signal });
|
|
2200
|
-
while (!command.completed) {
|
|
2201
|
-
signal.throwIfAborted();
|
|
2202
|
-
let cursor;
|
|
2203
|
-
let terminal;
|
|
2204
|
-
const cursors = new Set();
|
|
2205
|
-
do {
|
|
2206
|
-
const page = await request("thread/backgroundTerminals/list", {
|
|
2207
|
-
threadId: command.threadId, limit: 100, ...(cursor ? { cursor } : {})
|
|
2208
|
-
});
|
|
2209
|
-
if (!Array.isArray(page.data)) throw new Error("Codex returned no command inventory.");
|
|
2210
|
-
terminal = page.data.find((entry) => entry.processId === command.processId);
|
|
2211
|
-
cursor = page.nextCursor;
|
|
2212
|
-
if (cursor && cursors.has(cursor)) throw new Error("Codex repeated its command inventory cursor.");
|
|
2213
|
-
cursors.add(cursor);
|
|
2214
|
-
} while (!terminal && cursor);
|
|
2215
|
-
if (command.completed) return;
|
|
2216
|
-
if (terminal) {
|
|
2217
|
-
// A recycled process ID must never target a different command.
|
|
2218
|
-
if (terminal.itemId !== command.itemId) throw new Error("The native command identity changed during Stop.");
|
|
2219
|
-
const stopped = await request("thread/backgroundTerminals/terminate", {
|
|
2220
|
-
threadId: command.threadId, processId: command.processId
|
|
2221
|
-
});
|
|
2222
|
-
if (stopped.terminated === true) return;
|
|
2223
|
-
}
|
|
2224
|
-
// Codex emits command-start before adding it to the process inventory.
|
|
2225
|
-
// Absence during that interval is not proof that the command exited.
|
|
2226
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
2227
|
-
}
|
|
2228
|
-
})().catch((cause) => {
|
|
2229
|
-
const error = Object.assign(new Error("Codex could not confirm that the stopped turn's command exited.", { cause }), {
|
|
2230
|
-
code: `${this.errorPrefix}codex_command_stop_unconfirmed`
|
|
2231
|
-
});
|
|
2232
|
-
this.commandStopFailure = error;
|
|
2233
|
-
// Reuse the existing verified-runtime-stop owner when native control fails.
|
|
2234
|
-
// That owner may already be awaiting this command while stopping a lost
|
|
2235
|
-
// observation. Do not make command cleanup wait on its own caller.
|
|
2236
|
-
void Promise.resolve(this.failObservation(error)).catch(() => null);
|
|
2237
|
-
throw error;
|
|
2238
|
-
});
|
|
2239
|
-
return command.stopTask;
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
nativeCliResumeCommand(threadId = "") {
|
|
2243
|
-
const runtime = this.runtime || {};
|
|
2244
|
-
return codexCliResumeCommand({
|
|
2245
|
-
codexCommand: this.options.codexCommand || "codex",
|
|
2246
|
-
endpoint: runtime.endpoint,
|
|
2247
|
-
threadId
|
|
2248
|
-
});
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
610
|
close() {
|
|
2252
|
-
this.options
|
|
611
|
+
const { onClose = closeNothing } = this.options;
|
|
612
|
+
onClose();
|
|
2253
613
|
for (const reject of this.settingsWaiters) reject(new Error("The assistant connection closed before confirming its settings."));
|
|
2254
614
|
this.settingsWaiters.clear();
|
|
2255
615
|
this.conversationRuntime.close();
|
|
2256
|
-
this.
|
|
2257
|
-
this.
|
|
2258
|
-
this.interruptedTurns.clear();
|
|
2259
|
-
this.planUsage = null;
|
|
2260
|
-
this.planUsagePending = null;
|
|
616
|
+
this.turns.clear();
|
|
617
|
+
this.auth.resetPlanUsage();
|
|
2261
618
|
this.notificationSubscribers.clear();
|
|
2262
619
|
this.threadRequestHandlers.clear();
|
|
2263
620
|
this.client?.close();
|
|
2264
621
|
this.client = null;
|
|
2265
622
|
this.initializeResult = null;
|
|
2266
|
-
this.
|
|
2267
|
-
this.helperAuthBlocked = false;
|
|
623
|
+
this.auth.forgetHelper();
|
|
2268
624
|
}
|
|
2269
625
|
|
|
2270
626
|
async stopRuntime({
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
627
|
+
expectedAccountIdentitySignature = "",
|
|
628
|
+
preserveProcessExitProof = false
|
|
629
|
+
} = {}) {
|
|
2274
630
|
this.close();
|
|
2275
631
|
this.runtimeStopOwner = this.runtime || this.runtimeStopOwner;
|
|
2276
632
|
const runtime = this.runtimeStopOwner || {};
|
|
@@ -2294,19 +650,3 @@ export class CodexAppServerAgentProvider {
|
|
|
2294
650
|
return result;
|
|
2295
651
|
}
|
|
2296
652
|
}
|
|
2297
|
-
|
|
2298
|
-
export {
|
|
2299
|
-
CODEX_APP_SERVER_INVALID_REQUEST_CODE,
|
|
2300
|
-
ensureCodexAppServerThread, codexApplicationToolConfiguration, assertCodexApplicationToolSchemaIdentity, sendCodexAppServerPrompt,
|
|
2301
|
-
codexAppServerDeliveredTurn,
|
|
2302
|
-
codexAppServerEndpointForTarget, codexAppServerRequestIsInvalid, codexAppServerThreadIsMissing,
|
|
2303
|
-
codexAppServerThreadHasReadableHistory, deleteCodexAppServerThread, deleteCodexAppServerHelperThread,
|
|
2304
|
-
inspectCodexAppServerMessageAdmission,
|
|
2305
|
-
codexCliResumeCommand, codexLocalImageInput, codexTextInput, codexTurnInput, shellQuote,
|
|
2306
|
-
exportCodexNativeHistory, retireCodexConversationHistory, readCodexNativeGoal
|
|
2307
|
-
};
|
|
2308
|
-
export {
|
|
2309
|
-
createCodexAppServerProviderOwner,
|
|
2310
|
-
codexAppServerOwnedRuntimeKey,
|
|
2311
|
-
codexAppServerRuntimeStopWasVerified
|
|
2312
|
-
} from "./codexProviderOwner.js";
|