@jskit-ai/assistant-core 0.1.199 → 0.1.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { open, realpath } from "node:fs/promises";
|
|
5
|
+
import { createCodexAppServerRuntime } from "../codexProcess.js";
|
|
6
|
+
import { CodexAppServerAgentProvider, codexLocalImageInput, ensureCodexAppServerThread, inspectCodexAppServerMessageAdmission, retireCodexConversationHistory } from "../codexProvider.js";
|
|
7
|
+
import { createCodexAppServerProviderOwner } from "../codexProviderOwner.js";
|
|
8
|
+
import { codexAuthOutputRequiresReconnect, codexProviderConfiguration, codexToolFreeConfiguration } from "../codexConfiguration.js";
|
|
9
|
+
import { nativeAiProvider, nativeAiModel } from "../../../shared/nativeProviders.js";
|
|
10
|
+
import { createCodexAppServerRunOwner, codexAppServerTurnState, codexAppServerFrozenTurnInterruptResponse } from "../codexTurn.js";
|
|
11
|
+
import { createLocalConversationExecution } from "../localExecution.js";
|
|
12
|
+
import { validateConversationConfiguration, validateConnectionModel } from "../configuration.js";
|
|
13
|
+
import { codexCommandHookCommand } from "../commandWrapper.js";
|
|
14
|
+
|
|
15
|
+
const hash = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
16
|
+
const goalValue = goal => goal ? {
|
|
17
|
+
id: hash([goal.threadId, goal.createdAt, goal.objective]), objective: goal.objective, status: goal.status,
|
|
18
|
+
tokenBudget: goal.tokenBudget ?? null, tokensUsed: goal.tokensUsed, timeUsedSeconds: goal.timeUsedSeconds,
|
|
19
|
+
...(Number.isFinite(goal.createdAt) ? { createdAt: new Date(goal.createdAt * 1000).toISOString() } : {}),
|
|
20
|
+
...(Number.isFinite(goal.updatedAt) ? { updatedAt: new Date(goal.updatedAt * 1000).toISOString() } : {})
|
|
21
|
+
} : null;
|
|
22
|
+
const goalCapabilities = Object.freeze({ goals: true, goalBudgets: true,
|
|
23
|
+
goalCommands: Object.freeze({
|
|
24
|
+
set: Object.freeze({ delivery: "control", interruptsTurn: false }),
|
|
25
|
+
resume: Object.freeze({ delivery: "control", interruptsTurn: false }),
|
|
26
|
+
pause: Object.freeze({ delivery: "control", interruptsTurn: false }),
|
|
27
|
+
cancel: Object.freeze({ delivery: "control", interruptsTurn: false })
|
|
28
|
+
}) });
|
|
29
|
+
const nativeInput = input => [
|
|
30
|
+
...(input.text || input.attachmentManifest ? [{ type: "text", text: `${input.text || ""}${input.attachmentManifest || ""}` }] : []),
|
|
31
|
+
...(input.content || []).map(part => part.type === "text" ? part :
|
|
32
|
+
{ type: "image", url: `data:${part.mediaType};base64,${part.image.toString("base64")}` }),
|
|
33
|
+
...codexLocalImageInput(input.localFiles)
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** Conversations own threads and observers on the account's shared native runtime. */
|
|
37
|
+
export function createCodexConversationDriver({ connections, host = {}, limits = {} } = {}) {
|
|
38
|
+
const env = { ...(host.env || process.env) };
|
|
39
|
+
const workdir = path.resolve(host.workdir || process.cwd());
|
|
40
|
+
const configRoot = path.resolve(workdir, env.CODEX_HOME || path.join(env.HOME || homedir(), ".codex"));
|
|
41
|
+
env.CODEX_HOME = configRoot;
|
|
42
|
+
const command = host.commands?.codex || "codex";
|
|
43
|
+
const execution = host.execution || createLocalConversationExecution();
|
|
44
|
+
if (host.runtimeDirectory !== undefined && (typeof host.runtimeDirectory !== "string" || !path.isAbsolute(host.runtimeDirectory))) {
|
|
45
|
+
throw new TypeError("The native runtime directory must be absolute.");
|
|
46
|
+
}
|
|
47
|
+
const nativeTools = host.nativeTools === true;
|
|
48
|
+
const maximumOutput = limits.maxOutputCharacters ?? 64_000;
|
|
49
|
+
if (!Number.isSafeInteger(maximumOutput) || maximumOutput < 1) throw new TypeError("Invalid Codex output limit.");
|
|
50
|
+
|
|
51
|
+
async function accountRecord() {
|
|
52
|
+
let auth = {};
|
|
53
|
+
let file;
|
|
54
|
+
try {
|
|
55
|
+
file = await open(path.join(configRoot, "auth.json"), "r");
|
|
56
|
+
const bytes = Buffer.alloc(1024 * 1024 + 1);
|
|
57
|
+
let length = 0;
|
|
58
|
+
while (length < bytes.length) {
|
|
59
|
+
const { bytesRead } = await file.read(bytes, length, bytes.length - length, null);
|
|
60
|
+
if (!bytesRead) break;
|
|
61
|
+
length += bytesRead;
|
|
62
|
+
}
|
|
63
|
+
if (length === bytes.length) throw new Error("Credential data exceeded its size limit.");
|
|
64
|
+
auth = JSON.parse(bytes.subarray(0, length).toString("utf8"));
|
|
65
|
+
if (!auth || typeof auth !== "object" || Array.isArray(auth)) throw new Error("Invalid credential data.");
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Native keychain logins can have no auth file. Never expose malformed
|
|
68
|
+
// credential contents in a parser's error message.
|
|
69
|
+
if (error.code !== "ENOENT") throw new Error("Codex credential data could not be read. Restore its account configuration.");
|
|
70
|
+
} finally { await file?.close(); }
|
|
71
|
+
return auth;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function accountIdentity(client, signal) {
|
|
75
|
+
const { account } = await client.request("account/read", { refreshToken: false }, { signal });
|
|
76
|
+
const auth = await accountRecord();
|
|
77
|
+
if (account?.type === "chatgpt" && account.email) {
|
|
78
|
+
// The native account ID distinguishes workspaces sharing the same email;
|
|
79
|
+
// token refresh does not change this binding.
|
|
80
|
+
return hash([configRoot, account.type, account.email.toLowerCase(), auth.tokens?.account_id || ""]);
|
|
81
|
+
}
|
|
82
|
+
const key = env.OPENAI_API_KEY || auth.OPENAI_API_KEY;
|
|
83
|
+
if (account?.type === "apiKey" && typeof key === "string" && key) return hash([configRoot, account.type, key]);
|
|
84
|
+
throw new Error("Sign in with codex login before using this conversation.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function nativeHost(selected) {
|
|
88
|
+
// Managed applications supply their existing execution/account scope. This
|
|
89
|
+
// facility contains host policy; the provider and process algorithms stay here.
|
|
90
|
+
if (host.codex) return host.codex({ providerId: selected.providerId });
|
|
91
|
+
if (typeof execution.run !== "function") {
|
|
92
|
+
throw new TypeError("The Codex execution host must support capture and detached native execution.");
|
|
93
|
+
}
|
|
94
|
+
const processExecution = { ...execution, run: request => execution.run({ ...request, limits: host.limits }) };
|
|
95
|
+
const scope = hash([configRoot, selected.providerId, selected.accountIdentity || "", command, host.commandWrapper || null]).slice(0, 16);
|
|
96
|
+
const runtimeDir = path.join(host.runtimeDirectory || path.join(tmpdir(), `jskit-${process.getuid?.() ?? "user"}`),
|
|
97
|
+
`codex-app-server-${scope}`);
|
|
98
|
+
async function generation() {
|
|
99
|
+
if (selected.accountIdentity) return selected.accountIdentity;
|
|
100
|
+
const auth = await accountRecord();
|
|
101
|
+
// Token refresh is not a new account. Native keychain accounts are also
|
|
102
|
+
// checked through account/read before this conversation can send work.
|
|
103
|
+
return hash([configRoot, auth.auth_mode, auth.tokens?.account_id || "", env.OPENAI_API_KEY || auth.OPENAI_API_KEY || ""]);
|
|
104
|
+
}
|
|
105
|
+
const credentials = {
|
|
106
|
+
identity: generation,
|
|
107
|
+
generation,
|
|
108
|
+
async assertCurrent(captured) {
|
|
109
|
+
if (captured && captured !== await generation()) throw new Error("Codex authentication changed. Reopen the conversation with its original account.");
|
|
110
|
+
},
|
|
111
|
+
markInvalid() {},
|
|
112
|
+
reconnectRequiredCode: "codex_reconnect_required",
|
|
113
|
+
reconnectError: () => Object.assign(new Error("Sign in with codex login before continuing."), { code: "codex_reconnect_required" }),
|
|
114
|
+
observeFailure(output) { if (codexAuthOutputRequiresReconnect(output)) throw this.reconnectError(); }
|
|
115
|
+
};
|
|
116
|
+
const configuration = { commandHookCommand: host.commandWrapper ? codexCommandHookCommand(host.commandWrapper) : undefined };
|
|
117
|
+
const parameters = { runtimeDir, codexCommand: command, env, workdir: "", terminalEnv: {},
|
|
118
|
+
readyTimeoutMs: limits.admissionTimeoutMs || 30_000,
|
|
119
|
+
compatibility: { attachmentHostRoot: "", executionContextHash: scope, runtimesHash: hash([command, configuration]) } };
|
|
120
|
+
return { credentials, execution: processExecution, configuration, parameters,
|
|
121
|
+
runtime: createCodexAppServerRuntime({ credentials, execution: processExecution, configuration }) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function nativeStorageProvider({ sessionId, native, binding, context }) {
|
|
125
|
+
const prepared = await native.preparation.storage(sessionId, binding, context);
|
|
126
|
+
return { provider: await native.providerOwner.ensureSession(prepared.providerContext),
|
|
127
|
+
toolHomeSource: prepared.toolHomeSource };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function disposeNative({ sessionId, namespace, native, options }) {
|
|
131
|
+
if (options?.assistantScope) {
|
|
132
|
+
if (options.assistantScope.id !== sessionId) throw new TypeError("Codex helper scope does not match.");
|
|
133
|
+
const result = await native.providerOwner.stopCachedProvidersForSession(native.namespace || namespace, { requireStopped: true });
|
|
134
|
+
return { ...result, closed: result.stopped };
|
|
135
|
+
}
|
|
136
|
+
return native.runOwner.closeSession(sessionId, native.preparation.cleanup(options));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
// Codex fixes dynamic tool definitions when the thread is created. The three
|
|
141
|
+
// discovery entry points stay stable while the authorized action set changes.
|
|
142
|
+
toolDiscovery: true,
|
|
143
|
+
canonicalTranscript: true,
|
|
144
|
+
attachmentTypes: ["text", "image", "localFile"],
|
|
145
|
+
capabilities: Object.freeze({ streaming: true, cancellation: true, history: true,
|
|
146
|
+
instructions: true, configuration: true, steering: true, ...goalCapabilities,
|
|
147
|
+
attachments: true, tools: true, nativeTools, structuredOutput: true }),
|
|
148
|
+
validateConfiguration(config) {
|
|
149
|
+
validateConversationConfiguration(config, { engine: "Codex", connections, structuredOutput: true, maxOutputCharacters: maximumOutput });
|
|
150
|
+
},
|
|
151
|
+
disposeNative,
|
|
152
|
+
interruptDetachedConversation({ sessionId, native, input, context }) {
|
|
153
|
+
return native.runOwner.interruptDetachedTurn(sessionId, input, context);
|
|
154
|
+
},
|
|
155
|
+
deleteDetachedConversation({ sessionId, native, input, context }) {
|
|
156
|
+
return native.runOwner.deleteDetachedThread(sessionId, input, context);
|
|
157
|
+
},
|
|
158
|
+
async listConversationStorage(prepared) {
|
|
159
|
+
const { provider } = await nativeStorageProvider(prepared);
|
|
160
|
+
const { binding } = prepared;
|
|
161
|
+
return (await provider.listNativeThreadsForCwd(binding.workdir)).map((conversationId) => ({ ...binding, conversationId }));
|
|
162
|
+
},
|
|
163
|
+
async retireConversationHistory(prepared) {
|
|
164
|
+
const { provider, toolHomeSource } = await nativeStorageProvider(prepared);
|
|
165
|
+
return retireCodexConversationHistory(provider, prepared.binding, {
|
|
166
|
+
toolHomeSource, beforeDelete: prepared.context.beforeDelete, signal: prepared.context.signal,
|
|
167
|
+
errorPrefix: prepared.native.errorPrefix
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
invalidateRuntimes({ native, input }) {
|
|
171
|
+
return native.runOwner.invalidateRuntimes(input, native.preparation);
|
|
172
|
+
},
|
|
173
|
+
reconcileSessions({ native, sessions, options }) {
|
|
174
|
+
return native.runOwner.reconcileSessions(sessions, options, native.preparation);
|
|
175
|
+
},
|
|
176
|
+
unsubscribeSessions({ native, sessions }) {
|
|
177
|
+
return native.runOwner.unsubscribeSessions(sessions, native.preparation);
|
|
178
|
+
},
|
|
179
|
+
async closeProject({ native, input }) {
|
|
180
|
+
const prepared = await native.preparation.projectCleanup();
|
|
181
|
+
if (Object.hasOwn(prepared, "value")) return prepared.value;
|
|
182
|
+
return native.runOwner.closeProject(input, prepared);
|
|
183
|
+
},
|
|
184
|
+
inspectTemporaryActivity({ sessionId, native }) {
|
|
185
|
+
return native.runOwner.hasActiveTemporaryConversation(sessionId);
|
|
186
|
+
},
|
|
187
|
+
releaseRenewalPredecessorProcessExitProof({ native, context }) {
|
|
188
|
+
const prepared = native.preparation.predecessor(context);
|
|
189
|
+
return native.providerOwner.releasePreparedRuntimeProof(prepared);
|
|
190
|
+
},
|
|
191
|
+
releaseRenewalSuccessorProcessExitProof({ native, context }) {
|
|
192
|
+
const prepared = native.preparation.successor(context);
|
|
193
|
+
return native.providerOwner.releasePreparedRuntimeProof(prepared);
|
|
194
|
+
},
|
|
195
|
+
async generateRenewalHandover({ sessionId, native, input, context }) {
|
|
196
|
+
const prepared = native.preparation.handover(input, context);
|
|
197
|
+
const acquired = await native.runOwner.conversationContext(sessionId, input, prepared.context);
|
|
198
|
+
if (acquired.ok === false) return acquired;
|
|
199
|
+
const turn = await prepared.turn(acquired);
|
|
200
|
+
const execution = await native.runOwner.runRenewalHandover(sessionId, turn.input, turn.context);
|
|
201
|
+
return turn.completeResult(execution);
|
|
202
|
+
},
|
|
203
|
+
async seedRenewalHandover({ sessionId, native, input, context }) {
|
|
204
|
+
const prepared = native.preparation.seed(input, context);
|
|
205
|
+
const acquired = await native.runOwner.conversationContext(sessionId, input, prepared.context);
|
|
206
|
+
if (acquired.ok === false) return acquired;
|
|
207
|
+
const turn = prepared.turn(acquired);
|
|
208
|
+
const execution = await native.runOwner.runRenewalSeed(turn.input, turn.context);
|
|
209
|
+
return turn.completeResult(execution);
|
|
210
|
+
},
|
|
211
|
+
async ensureConversation({ sessionId, native }) {
|
|
212
|
+
return native.runOwner.ensureSessionReadiness(sessionId, await native.preparation.readiness());
|
|
213
|
+
},
|
|
214
|
+
async createConversation({ sessionId, native, input, context }) {
|
|
215
|
+
return native.runOwner.createConversation(sessionId, input, context);
|
|
216
|
+
},
|
|
217
|
+
async createBinding() {
|
|
218
|
+
return { threadId: "", workdir: await realpath(workdir), configRoot, executionId: "" };
|
|
219
|
+
},
|
|
220
|
+
async open({ binding, writeBinding, onFailure: reportFailure, conversation }) {
|
|
221
|
+
const supplied = conversation?.native;
|
|
222
|
+
if (supplied?.scoped) {
|
|
223
|
+
const { conversationId, context: openingContext } = supplied.scoped;
|
|
224
|
+
const { runOwner: owner } = supplied;
|
|
225
|
+
const sessionId = conversation.sessionId;
|
|
226
|
+
const inputFor = (input = {}) => {
|
|
227
|
+
if (input.conversationId && String(input.conversationId).trim() !== conversationId) {
|
|
228
|
+
throw new TypeError("This operation belongs to a different scoped conversation.");
|
|
229
|
+
}
|
|
230
|
+
return { ...input, conversationId };
|
|
231
|
+
};
|
|
232
|
+
const unsupported = () => { throw Object.assign(new Error("This original scoped conversation has no canonical message or goal receipt."), {
|
|
233
|
+
code: "conversation_unsupported"
|
|
234
|
+
}); };
|
|
235
|
+
return Object.freeze({
|
|
236
|
+
capabilities: Object.freeze({ streaming: false, instructions: false, configuration: false,
|
|
237
|
+
goals: false, goalBudgets: false, goalCommands: Object.freeze({}), attachments: false, tools: false, nativeTools: false }),
|
|
238
|
+
// These are the original scoped commands, including their native
|
|
239
|
+
// start dedup, watcher/deadline and Stop/delete proof. No main journal
|
|
240
|
+
// or additional command queue is introduced for this representation.
|
|
241
|
+
sendNative: ({ input, context }) => owner.startConversationTurn(sessionId, inputFor(input), context),
|
|
242
|
+
readNative: ({ input, context }) => owner.readConversation(sessionId, inputFor(input), context),
|
|
243
|
+
waitNative: ({ input, context }) => owner.waitForConversationTurn(sessionId, inputFor(input), context),
|
|
244
|
+
cancel: ({ input, context }) => owner.stopConversation(sessionId, inputFor(input), context),
|
|
245
|
+
readGoal: unsupported, updateGoal: unsupported, inspectAdmission: unsupported,
|
|
246
|
+
async dispose(options) {
|
|
247
|
+
if (options) return owner.deleteConversation(sessionId, inputFor(options.input), options.context);
|
|
248
|
+
// Runtime shutdown retains the parent's cleanup receipt. Explicit
|
|
249
|
+
// scoped deletion above owns native deletion and its proof.
|
|
250
|
+
const result = await disposeNative({ sessionId, namespace: conversation.namespace, native: supplied, options: openingContext });
|
|
251
|
+
if (result?.ok === false) throw Object.assign(new Error(result.error || "Scoped cleanup could not be confirmed."), result);
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (!supplied && (!binding || typeof binding.threadId !== "string" || binding.workdir !== await realpath(workdir) || binding.configRoot !== configRoot)) {
|
|
257
|
+
throw new Error("This Codex conversation belongs to another working directory or credential home. Restore its original host configuration.");
|
|
258
|
+
}
|
|
259
|
+
let native;
|
|
260
|
+
let nativeProviderId;
|
|
261
|
+
let current;
|
|
262
|
+
let stopping;
|
|
263
|
+
let disposed = false;
|
|
264
|
+
let preparedThreadSettings;
|
|
265
|
+
let bindingWork = Promise.resolve();
|
|
266
|
+
const providerKey = "conversation";
|
|
267
|
+
const providerOwner = supplied?.providerOwner || createCodexAppServerProviderOwner({
|
|
268
|
+
onRecoveryEvent(_owner, event, error) {
|
|
269
|
+
if (event === "stopFailed") current?.completion.reject(error);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
if (!conversation?.runtime?.store) throw new TypeError("Codex requires its bound conversation store.");
|
|
273
|
+
const sessionId = conversation.sessionId;
|
|
274
|
+
const nativeStore = conversation.runtime.store;
|
|
275
|
+
const boundedWriter = name => (id, input) => {
|
|
276
|
+
const limit = name === "writeConversationThinkingMessage" ? 4 * 1024 * 1024 : maximumOutput;
|
|
277
|
+
if ((input.text?.length || 0) > limit) throw new Error("Codex output exceeded the configured limit.");
|
|
278
|
+
return nativeStore[name](id, input);
|
|
279
|
+
};
|
|
280
|
+
const runtime = supplied ? conversation.runtime : { ...conversation.runtime, store: { ...nativeStore,
|
|
281
|
+
writeConversationAssistantMessage: boundedWriter("writeConversationAssistantMessage"),
|
|
282
|
+
upsertConversationAssistantMessage: boundedWriter("upsertConversationAssistantMessage"),
|
|
283
|
+
writeConversationCommentaryMessage: boundedWriter("writeConversationCommentaryMessage"),
|
|
284
|
+
writeConversationThinkingMessage: boundedWriter("writeConversationThinkingMessage"),
|
|
285
|
+
async updateConversationStream(id, input) {
|
|
286
|
+
if ((input.delta?.length || input.text?.length || 0) > maximumOutput) throw new Error("Codex output exceeded the configured limit.");
|
|
287
|
+
const stream = await nativeStore.updateConversationStream(id, input);
|
|
288
|
+
if (stream?.messages.some(message => message.text.length > maximumOutput)) throw new Error("Codex output exceeded the configured limit.");
|
|
289
|
+
return stream;
|
|
290
|
+
},
|
|
291
|
+
writeConversationUserMessage(id, input) {
|
|
292
|
+
const pending = owner.pendingUserMessages.get(`${sessionId}\0${input.messageId}`);
|
|
293
|
+
return conversation.runtime.store.writeConversationUserMessage(id, {
|
|
294
|
+
...input, authoredRequest: pending?.authoredInput
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
} };
|
|
298
|
+
const owner = supplied?.runOwner || createCodexAppServerRunOwner({
|
|
299
|
+
createRuntime: async () => runtime,
|
|
300
|
+
createStore: async () => runtime.store,
|
|
301
|
+
acquireProvider: async () => native,
|
|
302
|
+
hasRuntime: () => Boolean(binding.threadId),
|
|
303
|
+
publish: (_id, event) => publishNative(event),
|
|
304
|
+
checkpoint: (_id, input) => conversation.checkpoint(input)
|
|
305
|
+
});
|
|
306
|
+
async function publishNative(event) {
|
|
307
|
+
if (supplied) binding.threadId = await conversation.identity.read();
|
|
308
|
+
await conversation.publish(event);
|
|
309
|
+
if (event.nativeGoal?.threadId === binding.threadId) {
|
|
310
|
+
await recordGoal(event.nativeGoal.goal);
|
|
311
|
+
}
|
|
312
|
+
const turn = event.payload?.agentSession?.turn;
|
|
313
|
+
if (!turn || !current?.dispatched) return;
|
|
314
|
+
if (turn.active && current.finished && current.finishIfCurrent) {
|
|
315
|
+
// The original command owner may start the next turn during Send.
|
|
316
|
+
// Its publication replaces only this adapter's settled waiter.
|
|
317
|
+
current.completion = Promise.withResolvers();
|
|
318
|
+
current.completion.promise.catch(() => {});
|
|
319
|
+
current.finished = false;
|
|
320
|
+
}
|
|
321
|
+
if (turn.id && turn.id !== current.turnId) {
|
|
322
|
+
const previous = current.turnId;
|
|
323
|
+
current.turnId = turn.id;
|
|
324
|
+
if (current.admitted) {
|
|
325
|
+
if (previous) await current.onNativeTurn({ turnId: turn.id, nativeOwner: true });
|
|
326
|
+
} else current.pendingNativeTurn = turn.id;
|
|
327
|
+
}
|
|
328
|
+
if (!turn.active && event.reason === "codex-app-server-turn-idle") {
|
|
329
|
+
current.finished = true;
|
|
330
|
+
if (["failed", "interrupted"].includes(turn.status) && !current.signal.aborted) {
|
|
331
|
+
current.completion.reject(Object.assign(new Error(turn.error || `Codex turn ${turn.status}.`), { status: turn.status }));
|
|
332
|
+
} else current.completion.resolve();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const savedTurn = codexAppServerTurnState({ agentRuns: binding.codexAppServerRun ? [binding.codexAppServerRun] : [] });
|
|
336
|
+
let observationError = binding.observationLoss && (!binding.observationLoss.stopped ||
|
|
337
|
+
savedTurn.active && savedTurn.status === "observation_lost")
|
|
338
|
+
? Object.assign(new Error(binding.observationLoss.message), { cleanupFailed: true }) : null;
|
|
339
|
+
if (observationError) await reportFailure(observationError);
|
|
340
|
+
function updateBinding(patch) {
|
|
341
|
+
const work = bindingWork.then(async () => {
|
|
342
|
+
const next = { ...binding, ...patch };
|
|
343
|
+
await writeBinding(next);
|
|
344
|
+
binding = next;
|
|
345
|
+
});
|
|
346
|
+
bindingWork = work.catch(() => {});
|
|
347
|
+
return work;
|
|
348
|
+
}
|
|
349
|
+
async function recordGoal(goal) {
|
|
350
|
+
if (supplied) binding.goal = goal;
|
|
351
|
+
else await updateBinding({ goal });
|
|
352
|
+
if (current?.admitted) await current.onEvent({ type: "goal", goal: goalValue(goal) });
|
|
353
|
+
else if (current) current.pendingGoal = goal;
|
|
354
|
+
else await conversation.publish({ payload: { goal: goalValue(goal) }, reason: "codex-goal" });
|
|
355
|
+
return goal;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let observationTarget;
|
|
359
|
+
const observation = supplied ? null : owner.createObservation(sessionId, {
|
|
360
|
+
target(error) {
|
|
361
|
+
if (observationTarget?.error !== error) observationTarget = { error, threadId: binding.threadId,
|
|
362
|
+
message: "Codex observation was lost. Work is stopped; use Resume or Send to continue.",
|
|
363
|
+
pendingMessage: "Codex observation was lost. A stop is not yet confirmed." };
|
|
364
|
+
return observationTarget;
|
|
365
|
+
},
|
|
366
|
+
retireProvider: () => providerOwner.closeProvider(providerKey)
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
async function recordObservationBarrier({ provider, error }) {
|
|
370
|
+
observationError = error;
|
|
371
|
+
// Keep admission unavailable in memory even if its durable write fails.
|
|
372
|
+
await reportFailure(error);
|
|
373
|
+
await updateBinding({ observationLoss: {
|
|
374
|
+
message: error.message, providerId: nativeProviderId,
|
|
375
|
+
runtime: provider.runtime || provider.runtimeStopOwner || binding.observationLoss?.runtime,
|
|
376
|
+
stopped: false
|
|
377
|
+
} });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function completeObservation({ provider, error, sharedStop }) {
|
|
381
|
+
await updateBinding({ observationLoss: { ...binding.observationLoss, stopped: true },
|
|
382
|
+
...(sharedStop ? { executionId: "" } : {}) });
|
|
383
|
+
await observation.complete({ provider, error, sharedStop });
|
|
384
|
+
observationError = null;
|
|
385
|
+
await reportFailure(error, { recovered: true });
|
|
386
|
+
current?.completion.reject(error);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function acquireProvider(facilities, providerId) {
|
|
390
|
+
nativeProviderId = providerId;
|
|
391
|
+
const providerOptions = {
|
|
392
|
+
...facilities.options, runtimeDir: facilities.parameters.runtimeDir, modelProviderId: providerId,
|
|
393
|
+
threadEnv: env,
|
|
394
|
+
readInstructions: () => preparedThreadSettings?.developerInstructions,
|
|
395
|
+
prepareThreadEnvironment: environment => preparedThreadSettings?.config.shell_environment_policy.set || environment,
|
|
396
|
+
prepareThreadResumeParams(threadId, params) {
|
|
397
|
+
if (threadId !== binding.threadId || !preparedThreadSettings) {
|
|
398
|
+
throw new Error("Codex requires current authorized settings before resuming this conversation.");
|
|
399
|
+
}
|
|
400
|
+
return { ...preparedThreadSettings, ...params,
|
|
401
|
+
config: { ...preparedThreadSettings.config, ...params.config } };
|
|
402
|
+
},
|
|
403
|
+
async beforeResumeThread(threadId) {
|
|
404
|
+
owner.assertThreadCanResume(await owner.readAgentRunForSession(runtime.store, sessionId), threadId);
|
|
405
|
+
},
|
|
406
|
+
requestTimeoutMs: limits.admissionTimeoutMs || 30_000
|
|
407
|
+
};
|
|
408
|
+
return providerOwner.withLifecycle(() => providerOwner.createProvider({
|
|
409
|
+
providerKey, providerOptions, preserveProcessExitProof: true,
|
|
410
|
+
resources: {
|
|
411
|
+
release(provider) {
|
|
412
|
+
if (native === provider) native = null;
|
|
413
|
+
const subscriptionPrefix = `${providerKey}:`;
|
|
414
|
+
for (const key of [...owner.eventSubscriptions.keys()]) {
|
|
415
|
+
if (key.startsWith(subscriptionPrefix)) owner.unsubscribeEventSubscription(key);
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
invalidation: {
|
|
419
|
+
async prepare(input) {
|
|
420
|
+
if (native !== input.provider) return false;
|
|
421
|
+
await recordObservationBarrier(input);
|
|
422
|
+
await observation.barrier(input);
|
|
423
|
+
},
|
|
424
|
+
async complete({ provider, error, preparationError, stopped }) {
|
|
425
|
+
try {
|
|
426
|
+
if (preparationError) throw preparationError;
|
|
427
|
+
if (stopped.status !== "fulfilled") throw stopped.reason;
|
|
428
|
+
await completeObservation({ provider, error, sharedStop: true });
|
|
429
|
+
} catch (failure) {
|
|
430
|
+
error.cleanupFailed = true;
|
|
431
|
+
error.cause = failure;
|
|
432
|
+
observationError = error;
|
|
433
|
+
await reportFailure(error);
|
|
434
|
+
current?.completion.reject(error);
|
|
435
|
+
throw failure;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
create: onObservationLost => new CodexAppServerAgentProvider({ ...providerOptions, onObservationLost }, facilities),
|
|
441
|
+
observation: {
|
|
442
|
+
async prepare(input) {
|
|
443
|
+
observationError = input.error;
|
|
444
|
+
await reportFailure(input.error);
|
|
445
|
+
const threads = await observation.prepare(input);
|
|
446
|
+
await recordObservationBarrier(input);
|
|
447
|
+
return threads;
|
|
448
|
+
},
|
|
449
|
+
async barrier(input) {
|
|
450
|
+
await observation.barrier(input);
|
|
451
|
+
await recordObservationBarrier(input);
|
|
452
|
+
},
|
|
453
|
+
complete: completeObservation
|
|
454
|
+
}
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function stop() {
|
|
459
|
+
if (stopping) return stopping;
|
|
460
|
+
stopping = (async () => {
|
|
461
|
+
if (!native && observationError) {
|
|
462
|
+
// Recover the exact saved owner through the production disk lock
|
|
463
|
+
// and metadata path. Cleanup must not start or resume a thread.
|
|
464
|
+
const saved = binding.observationLoss;
|
|
465
|
+
const facilities = await nativeHost({ providerId: saved.providerId,
|
|
466
|
+
accountIdentity: binding.connectionIdentities?.[saved.providerId] });
|
|
467
|
+
native = await acquireProvider(facilities, saved.providerId);
|
|
468
|
+
native.runtimeStopOwner = saved.runtime;
|
|
469
|
+
native.observationFailure = observationError;
|
|
470
|
+
}
|
|
471
|
+
if (native?.observationFailure) {
|
|
472
|
+
await providerOwner.recover(providerKey, native.observationFailure);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (native && binding.threadId) {
|
|
476
|
+
const provider = native;
|
|
477
|
+
try {
|
|
478
|
+
const { goal } = await provider.stopThreadForObservationLoss(binding.threadId, current?.turnId);
|
|
479
|
+
await recordGoal(goal || null);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
await provider.failObservation(error);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
})().catch(error => { error.cleanupFailed = true; throw error; }).finally(() => { stopping = null; });
|
|
485
|
+
return stopping;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function selection(configuration, context, signal) {
|
|
489
|
+
if (!configuration.integrationId) return { model: configuration.model, providerId: "openai" };
|
|
490
|
+
const connection = await connections.resolve({ context, integrationId: configuration.integrationId });
|
|
491
|
+
signal.throwIfAborted();
|
|
492
|
+
const provider = nativeAiProvider(connection.providerId);
|
|
493
|
+
const model = nativeAiModel(connection.model, connection.providerId);
|
|
494
|
+
if (!provider || !model) throw new Error("This exact provider/model is not supported by the Codex integration. Choose a supported model without renaming it.");
|
|
495
|
+
validateConnectionModel(configuration, connection);
|
|
496
|
+
if (configuration.effort && !model.variants.includes(configuration.effort)) throw new Error("The selected Codex model does not support this reasoning effort.");
|
|
497
|
+
const identity = hash([provider.id, provider.baseUrl, connection.apiKey]);
|
|
498
|
+
const previous = binding.connectionIdentities?.[provider.id];
|
|
499
|
+
if (previous && previous !== identity) throw new Error("This conversation belongs to another Codex provider account. Restore that connection or start a new conversation.");
|
|
500
|
+
if (!previous) await updateBinding({ connectionIdentities: { ...binding.connectionIdentities, [provider.id]: identity } });
|
|
501
|
+
return { model: model.id, providerId: provider.id, accountIdentity: identity, config: codexProviderConfiguration(provider, connection.apiKey) };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function connect(signal, selected) {
|
|
505
|
+
if (observationError) throw observationError;
|
|
506
|
+
if (native && nativeProviderId !== selected.providerId) {
|
|
507
|
+
await stop();
|
|
508
|
+
providerOwner.closeProvider(providerKey);
|
|
509
|
+
}
|
|
510
|
+
if (!native) {
|
|
511
|
+
const facilities = await nativeHost(selected);
|
|
512
|
+
signal.throwIfAborted();
|
|
513
|
+
const provider = await acquireProvider(facilities, selected.providerId);
|
|
514
|
+
// Account startup has other waiters. Cancellation releases this
|
|
515
|
+
// observer; the existing runtime owner still settles shared startup.
|
|
516
|
+
const connected = providerOwner.acquireRuntime({ providerKey, provider,
|
|
517
|
+
providerOptions: provider.options, operation: () => provider.connect() });
|
|
518
|
+
const cancelled = Promise.withResolvers();
|
|
519
|
+
const abort = () => cancelled.reject(signal.reason);
|
|
520
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
521
|
+
if (signal.aborted) abort();
|
|
522
|
+
try {
|
|
523
|
+
await Promise.race([connected, cancelled.promise]);
|
|
524
|
+
signal.throwIfAborted();
|
|
525
|
+
// Execution identity is a reference for recovery, not ownership of
|
|
526
|
+
// the account service by this one conversation.
|
|
527
|
+
await updateBinding({ executionId: provider.runtime.executionId });
|
|
528
|
+
native = provider;
|
|
529
|
+
nativeProviderId = selected.providerId;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
void connected.then(() => providerOwner.closeProvider(providerKey), () => providerOwner.closeProvider(providerKey));
|
|
532
|
+
throw error;
|
|
533
|
+
} finally {
|
|
534
|
+
signal.removeEventListener("abort", abort);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
signal.throwIfAborted();
|
|
538
|
+
if (selected.providerId !== "openai") return;
|
|
539
|
+
const identity = await accountIdentity(native.client, signal);
|
|
540
|
+
if (binding.accountIdentity && binding.accountIdentity !== identity) {
|
|
541
|
+
providerOwner.closeProvider(providerKey);
|
|
542
|
+
throw new Error("This conversation belongs to another Codex account. Restore that account or start a new conversation.");
|
|
543
|
+
}
|
|
544
|
+
if (!binding.accountIdentity) await updateBinding({ accountIdentity: identity });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function prepareConfiguration(configuration, context, signal, tools) {
|
|
548
|
+
const dynamicTools = (tools?.schemas || []).map(({ function: tool }) => ({
|
|
549
|
+
type: "function", name: tool.name, description: tool.description, inputSchema: tool.parameters
|
|
550
|
+
}));
|
|
551
|
+
const toolSchemaIdentity = hash(dynamicTools);
|
|
552
|
+
if (binding.threadId && (binding.toolSchemaIdentity || hash([])) !== toolSchemaIdentity) {
|
|
553
|
+
throw new Error("This Codex conversation was created with different application tool entry points. Start a new conversation to enable or remove application tools.");
|
|
554
|
+
}
|
|
555
|
+
const selected = await selection(configuration, context, signal);
|
|
556
|
+
await connect(signal, selected);
|
|
557
|
+
const [configResult, hookResult] = await Promise.all([
|
|
558
|
+
native.client.request("config/read", { cwd: binding.workdir, includeLayers: false }, { signal }),
|
|
559
|
+
native.client.request("hooks/list", { cwds: [binding.workdir] }, { signal })
|
|
560
|
+
]);
|
|
561
|
+
const toolConfiguration = codexToolFreeConfiguration({ configResult, hookResult, workdir: binding.workdir });
|
|
562
|
+
// Native control verification runs the host's shell even when model
|
|
563
|
+
// shell tools are disabled. Its login profile needs the standard tools;
|
|
564
|
+
// do not copy credentials or the rest of the host environment here.
|
|
565
|
+
toolConfiguration.shell_environment_policy.set.PATH = env.PATH || "/usr/bin:/bin";
|
|
566
|
+
// A goal can be set while an ordinary turn is already running. Codex
|
|
567
|
+
// must have its native scheduler enabled before that turn starts.
|
|
568
|
+
toolConfiguration.features.goals = true;
|
|
569
|
+
if (host.commandWrapper) {
|
|
570
|
+
const hookCommand = codexCommandHookCommand(host.commandWrapper);
|
|
571
|
+
const owned = hookResult.data[0].hooks.filter(hook => hook.source === "sessionFlags" &&
|
|
572
|
+
hook.eventName === "preToolUse" && hook.handlerType === "command" && hook.command === hookCommand);
|
|
573
|
+
if (owned.length !== 1 || typeof owned[0].currentHash !== "string" || !owned[0].currentHash) {
|
|
574
|
+
throw new Error("Codex did not install the host's required command wrapper.");
|
|
575
|
+
}
|
|
576
|
+
toolConfiguration.features.hooks = true;
|
|
577
|
+
toolConfiguration.hooks.PreToolUse = [{ matcher: "^Bash$", hooks: [{ type: "command", command: hookCommand, timeout: 30 }] }];
|
|
578
|
+
toolConfiguration.hooks.state[owned[0].key] = { enabled: true, trusted_hash: owned[0].currentHash };
|
|
579
|
+
}
|
|
580
|
+
if (nativeTools) {
|
|
581
|
+
Object.assign(toolConfiguration.features, { shell_tool: true, unified_exec: true, view_image: true });
|
|
582
|
+
toolConfiguration.include_environment_context = true;
|
|
583
|
+
toolConfiguration.include_permissions_instructions = true;
|
|
584
|
+
toolConfiguration.tools.update_plan.enabled = true;
|
|
585
|
+
toolConfiguration.shell_environment_policy.set = Object.fromEntries(Object.entries(env).filter(([name, value]) =>
|
|
586
|
+
typeof value === "string" && !["DBUS_SESSION_BUS_ADDRESS", "DBUS_STARTER_ADDRESS", "DBUS_STARTER_BUS_TYPE"].includes(name)));
|
|
587
|
+
toolConfiguration.web_search = selected.config?.web_search || "disabled";
|
|
588
|
+
}
|
|
589
|
+
const params = { developerInstructions: configuration.systemPrompt,
|
|
590
|
+
cwd: binding.workdir, modelProvider: selected.providerId, ...(selected.model ? { model: selected.model } : {}),
|
|
591
|
+
dynamicTools, ...(!nativeTools ? { environments: [], runtimeWorkspaceRoots: [], selectedCapabilityRoots: [] } : {}),
|
|
592
|
+
approvalPolicy: "never", sandbox: nativeTools ? "danger-full-access" : "read-only", allowProviderModelFallback: false,
|
|
593
|
+
config: { ...selected.config, ...toolConfiguration }
|
|
594
|
+
};
|
|
595
|
+
// Cold control recovery uses the provider's original settings facilities;
|
|
596
|
+
// it must not resume with ambient defaults or an old instruction prompt.
|
|
597
|
+
preparedThreadSettings = params;
|
|
598
|
+
return { selected, params, toolSchemaIdentity };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function prepare(configuration, context, signal, tools) {
|
|
602
|
+
const { selected, params, toolSchemaIdentity } = await prepareConfiguration(configuration, context, signal, tools);
|
|
603
|
+
const retainedThreadId = binding.threadId;
|
|
604
|
+
const prepared = await ensureCodexAppServerThread({
|
|
605
|
+
provider: native, workdir: binding.workdir,
|
|
606
|
+
observeThread: threadId => owner.subscribeEvents(sessionId, native, threadId, { providerKey }),
|
|
607
|
+
settings: async () => ({ threadSettings: params, threadStartSettings: params }),
|
|
608
|
+
identity: { read: () => retainedThreadId,
|
|
609
|
+
async write({ threadId }) { await updateBinding({ threadId, toolSchemaIdentity }); } }
|
|
610
|
+
});
|
|
611
|
+
if (!retainedThreadId && prepared.thread.raw?.historyMode !== "paginated") {
|
|
612
|
+
throw new Error("Codex did not create the required paginated history. Update Codex before starting this conversation.");
|
|
613
|
+
}
|
|
614
|
+
owner.subscribeEvents(sessionId, native, prepared.threadId, { providerKey });
|
|
615
|
+
return { ...selected, resumeOptions: params };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function deliveryInput(input, beforeDispatch, rejected) {
|
|
619
|
+
if (supplied) return { ...input.nativeMessage,
|
|
620
|
+
onPromptSending: beforeDispatch,
|
|
621
|
+
onPromptRejected: async () => { await input.nativeMessage.onPromptRejected?.(); await rejected?.(); } };
|
|
622
|
+
return { messageId: input.messageId, message: input.text,
|
|
623
|
+
displayMessage: input.authoredInput?.text ?? input.text,
|
|
624
|
+
displayAttachments: input.attachments, attachments: input.localFiles,
|
|
625
|
+
onPromptSending: value => beforeDispatch({ threadId: value.threadId }), onPromptRejected: rejected };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async function deliveredRow(input, nativeIdentity) {
|
|
629
|
+
const written = await owner.writeDeliveredUserMessage(runtime, sessionId,
|
|
630
|
+
input.authoredInput?.text ?? input.text, input.messageId, null, input.attachments,
|
|
631
|
+
nativeIdentity);
|
|
632
|
+
if (written) return written;
|
|
633
|
+
const saved = (await runtime.store.readConversationLog(sessionId)).find(turn =>
|
|
634
|
+
(turn.user || turn.system)?.messageId === input.messageId);
|
|
635
|
+
if (!saved) throw new Error("The accepted native receipt has no authored conversation message.");
|
|
636
|
+
return saved;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function withDelivery(input, operation) {
|
|
640
|
+
return owner.withMessageDelivery(sessionId, input.messageId, {
|
|
641
|
+
attachments: input.attachments, text: input.authoredInput?.text ?? input.text,
|
|
642
|
+
turnMetadata: input.nativeMessage?.turnMetadata || null, actorContext: input.nativeMessage?.actorContext || null,
|
|
643
|
+
authoredInput: input.authoredInput
|
|
644
|
+
}, operation);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function messagePreparation(configuration, context, signal, tools) {
|
|
648
|
+
if (supplied) return supplied.messagePreparation;
|
|
649
|
+
return {
|
|
650
|
+
async readContext() {
|
|
651
|
+
const session = await runtime.getSession(sessionId);
|
|
652
|
+
return { runtime, session, selection: {
|
|
653
|
+
runtime, session, threadId: () => binding.threadId,
|
|
654
|
+
async acquireProvider() {
|
|
655
|
+
const reused = Boolean(native?.isAvailable());
|
|
656
|
+
await connect(signal, await selection(configuration, context, signal));
|
|
657
|
+
return { provider: native, reused };
|
|
658
|
+
}
|
|
659
|
+
} };
|
|
660
|
+
},
|
|
661
|
+
threadPreparation() {
|
|
662
|
+
return {
|
|
663
|
+
get failureThreadId() { return binding.threadId; },
|
|
664
|
+
async provider() {
|
|
665
|
+
const { selected, params, toolSchemaIdentity } = await prepareConfiguration(configuration, context, signal, tools);
|
|
666
|
+
const retainedThreadId = binding.threadId;
|
|
667
|
+
return {
|
|
668
|
+
provider: native,
|
|
669
|
+
workdir: binding.workdir,
|
|
670
|
+
observerOptions: { providerKey },
|
|
671
|
+
requirePaginatedHistory: !retainedThreadId,
|
|
672
|
+
settings: { ...selected, resumeOptions: params },
|
|
673
|
+
preparation: {
|
|
674
|
+
settings: async () => ({ threadSettings: params, threadStartSettings: params }),
|
|
675
|
+
identity: { read: () => retainedThreadId,
|
|
676
|
+
async write({ threadId }) { await updateBinding({ threadId, toolSchemaIdentity }); } }
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
},
|
|
682
|
+
async prepareMessage(input, prepared, { starting }) {
|
|
683
|
+
if (!starting) return null;
|
|
684
|
+
const { settings } = prepared;
|
|
685
|
+
return { renderedPrompt: input.message,
|
|
686
|
+
get turnSettings() { return { cwd: binding.workdir,
|
|
687
|
+
...(settings.model ? { model: settings.model } : {}), effort: configuration.effort || null,
|
|
688
|
+
...(configuration.outputSchema ? { outputSchema: configuration.outputSchema } : {}) }; }
|
|
689
|
+
};
|
|
690
|
+
},
|
|
691
|
+
async finishMessage(input, prepared, outcome) {
|
|
692
|
+
if (Object.hasOwn(outcome, "error")) throw outcome.error;
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function acceptMessage(command, nativeIdentity, row, nativeResult) {
|
|
698
|
+
if (typeof nativeIdentity?.threadId !== "string" || !nativeIdentity.threadId.trim() ||
|
|
699
|
+
typeof nativeIdentity?.turnId !== "string" || !nativeIdentity.turnId.trim()) {
|
|
700
|
+
throw Object.assign(new Error("The native delivery has no exact turn identity. Inspect this message's receipt before continuing."), {
|
|
701
|
+
code: "conversation_delivery_uncertain", delivery: "uncertain"
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
const { turnId } = nativeIdentity;
|
|
705
|
+
const active = current;
|
|
706
|
+
if (!active) throw new Error("The native delivery lost its active conversation owner.");
|
|
707
|
+
active.inputId = command.input.messageId;
|
|
708
|
+
if (!active.turnId) active.turnId = turnId;
|
|
709
|
+
await command.accept({ nativeTurnId: turnId, conversationTurn: row || await deliveredRow(command.input, nativeIdentity),
|
|
710
|
+
...(supplied ? { nativeResult } : {}) });
|
|
711
|
+
active.admitted = true;
|
|
712
|
+
active.admission.resolve();
|
|
713
|
+
if (active.pendingGoal !== undefined) {
|
|
714
|
+
const goal = active.pendingGoal;
|
|
715
|
+
delete active.pendingGoal;
|
|
716
|
+
await active.onEvent({ type: "goal", goal: goalValue(goal) });
|
|
717
|
+
}
|
|
718
|
+
if (active.pendingNativeTurn) {
|
|
719
|
+
const next = active.pendingNativeTurn;
|
|
720
|
+
delete active.pendingNativeTurn;
|
|
721
|
+
await active.onNativeTurn({ turnId: next, nativeOwner: true });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function sendMessage(command, configuration, context, signal, tools) {
|
|
726
|
+
return withDelivery(command.input, async () => {
|
|
727
|
+
let attempted = false;
|
|
728
|
+
let rejected = false;
|
|
729
|
+
let accepted = false;
|
|
730
|
+
const input = deliveryInput(command.input, async value => {
|
|
731
|
+
await command.beforeDispatch(value);
|
|
732
|
+
signal.throwIfAborted();
|
|
733
|
+
attempted = true;
|
|
734
|
+
current.dispatched = true;
|
|
735
|
+
}, () => { rejected = true; });
|
|
736
|
+
try {
|
|
737
|
+
const delivered = await owner.dispatchMessage(sessionId, input, {
|
|
738
|
+
...(supplied ? { ...context, actorContext: input.actorContext || null } : { preparedInput: nativeInput(command.input) })
|
|
739
|
+
}, messagePreparation(configuration, context, signal, tools));
|
|
740
|
+
const { value, nativeIdentity } = delivered;
|
|
741
|
+
if (supplied ? value.delivered === true : value.delivered || nativeIdentity) {
|
|
742
|
+
await acceptMessage(command, nativeIdentity || { threadId: value.threadId, turnId: value.turnId }, value.conversationTurn, delivered);
|
|
743
|
+
accepted = true;
|
|
744
|
+
if (value.ok === false) throw new Error(value.error);
|
|
745
|
+
return delivered;
|
|
746
|
+
}
|
|
747
|
+
// The original main sender retains its pending evidence when the
|
|
748
|
+
// command returns delivered:false, even if a native ID is known.
|
|
749
|
+
if (supplied) return delivered;
|
|
750
|
+
throw Object.assign(new Error(value?.error || "Codex is not ready to receive this instruction."), value);
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (attempted && !accepted && !rejected && !Number.isInteger(error.code)) error.delivery = "uncertain";
|
|
753
|
+
throw error;
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function goalContext(configuration, context, signal, { prepareThread = false, tools } = {}) {
|
|
759
|
+
if (supplied) return owner.prepareGoalContext(sessionId, context, supplied.goalPreparation, { prepareThread });
|
|
760
|
+
let selected;
|
|
761
|
+
if (prepareThread && !binding.threadId) selected = await prepare(configuration, context, signal, tools);
|
|
762
|
+
else if (prepareThread) {
|
|
763
|
+
const prepared = await prepareConfiguration(configuration, context, signal, tools);
|
|
764
|
+
selected = { ...prepared.selected, resumeOptions: prepared.params };
|
|
765
|
+
} else {
|
|
766
|
+
selected = await selection(configuration, context, signal);
|
|
767
|
+
await connect(signal, selected);
|
|
768
|
+
}
|
|
769
|
+
return { runtime, session: await runtime.getSession(sessionId), provider: native,
|
|
770
|
+
threadId: binding.threadId, providerKey, resumeOptions: selected.resumeOptions };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function interrupt({ configuration, context, input = {}, representation } = {}) {
|
|
774
|
+
if (supplied) {
|
|
775
|
+
const admission = supplied.admission();
|
|
776
|
+
if (admission.ok === false) {
|
|
777
|
+
const value = codexAppServerFrozenTurnInterruptResponse({
|
|
778
|
+
threadId: input.threadId || input.codexSessionId, turnId: input.turnId || input.codexTurnId
|
|
779
|
+
});
|
|
780
|
+
return representation === "native" ? { value } : value;
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
const prepared = await supplied.controlPreparation(context, input);
|
|
784
|
+
if (prepared.ok === false) {
|
|
785
|
+
if (representation === "native") return { value: prepared };
|
|
786
|
+
throw Object.assign(new Error(prepared.error), prepared, { cleanupFailed: true });
|
|
787
|
+
}
|
|
788
|
+
const result = await owner.interruptTurn(sessionId, input, owner.controlContext(sessionId, prepared));
|
|
789
|
+
if (representation === "native") return result;
|
|
790
|
+
if (result.value?.ok === false) throw Object.assign(new Error(result.value.error), result.value, { cleanupFailed: true });
|
|
791
|
+
return result.value;
|
|
792
|
+
} finally { admission.release(); }
|
|
793
|
+
}
|
|
794
|
+
if (observationError) {
|
|
795
|
+
await stop();
|
|
796
|
+
return { ok: true, interrupted: true };
|
|
797
|
+
}
|
|
798
|
+
const result = await owner.interruptTurn(sessionId, {}, {
|
|
799
|
+
runtime, admissionError: () => null, threadId: () => binding.threadId,
|
|
800
|
+
observationOwner: () => native,
|
|
801
|
+
async acquireProvider() {
|
|
802
|
+
if (!native && configuration) {
|
|
803
|
+
const signal = AbortSignal.timeout(30_000);
|
|
804
|
+
await connect(signal, await selection(configuration, context, signal));
|
|
805
|
+
}
|
|
806
|
+
if (!native) throw new Error("Codex's retained runtime owner is unavailable.");
|
|
807
|
+
await native.ensureAvailable();
|
|
808
|
+
return native;
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
if (result.value?.ok === false) throw Object.assign(new Error(result.value.error || "Codex could not confirm that work stopped."), { cleanupFailed: true });
|
|
812
|
+
return result.value;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return Object.freeze({
|
|
816
|
+
publishNative,
|
|
817
|
+
async readGoal({ configuration, context }) {
|
|
818
|
+
if (supplied) {
|
|
819
|
+
const target = await supplied.readGoalContext(context);
|
|
820
|
+
const result = target.ok === false ? { status: "unavailable", goal: null }
|
|
821
|
+
: await providerOwner.readSessionGoal(conversation.namespace, target.threadId);
|
|
822
|
+
binding.threadId = target.threadId || "";
|
|
823
|
+
binding.goal = result.goal || null;
|
|
824
|
+
if (target.completeResult) await target.completeResult(result, {
|
|
825
|
+
segmentId: target.threadId ? `codex:${target.threadId}` : null, capabilities: goalCapabilities
|
|
826
|
+
});
|
|
827
|
+
return { goal: goalValue(result.goal), nativeResult: result };
|
|
828
|
+
}
|
|
829
|
+
if (!binding.threadId) return null;
|
|
830
|
+
const signal = AbortSignal.timeout(30_000);
|
|
831
|
+
await connect(signal, await selection(configuration, context, signal));
|
|
832
|
+
const { goal } = await native.readGoal(binding.threadId, { signal });
|
|
833
|
+
await recordGoal(goal || null);
|
|
834
|
+
return goalValue(goal);
|
|
835
|
+
},
|
|
836
|
+
async updateGoal({ configuration, context, input, tools, representation }) {
|
|
837
|
+
if (supplied) {
|
|
838
|
+
const invalid = owner.validateGoalInput(input);
|
|
839
|
+
if (invalid) return { goal: goalValue(binding.goal), nativeResult: invalid };
|
|
840
|
+
const admission = supplied.admission();
|
|
841
|
+
if (admission.ok === false) return { goal: goalValue(binding.goal), nativeResult: admission };
|
|
842
|
+
let nativeContext;
|
|
843
|
+
let result;
|
|
844
|
+
try {
|
|
845
|
+
nativeContext = await goalContext(configuration, context);
|
|
846
|
+
let command = input;
|
|
847
|
+
if (representation !== "native") {
|
|
848
|
+
binding.threadId = nativeContext.threadId;
|
|
849
|
+
const goal = binding.goal || null;
|
|
850
|
+
if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw new Error("The native goal changed. Read it again before changing it.");
|
|
851
|
+
command = { ...input, threadId: binding.threadId,
|
|
852
|
+
createdAt: goal?.createdAt, objective: input.action === "set" ? input.objective : goal?.objective };
|
|
853
|
+
}
|
|
854
|
+
result = await owner.updateGoal(sessionId, command, {
|
|
855
|
+
context: nativeContext, prepareConversation: () => goalContext(configuration, context, undefined, { prepareThread: true })
|
|
856
|
+
});
|
|
857
|
+
if (result.ok !== false) binding.goal = result.goal || null;
|
|
858
|
+
} finally { admission.release(); }
|
|
859
|
+
if (nativeContext.completeResult) await nativeContext.completeResult(result);
|
|
860
|
+
return { goal: goalValue(result.goal), nativeResult: result };
|
|
861
|
+
}
|
|
862
|
+
if (binding.goalRequest) throw Object.assign(new Error("A goal command from an earlier runtime has unresolved delivery. Inspect its saved state before changing the goal."), {
|
|
863
|
+
code: "conversation_delivery_uncertain"
|
|
864
|
+
});
|
|
865
|
+
const signal = AbortSignal.timeout(30_000);
|
|
866
|
+
const nativeContext = await goalContext(configuration, context, signal, {
|
|
867
|
+
prepareThread: Boolean(binding.threadId && ["set", "resume"].includes(input.action)), tools
|
|
868
|
+
});
|
|
869
|
+
const goal = binding.threadId ? (await native.readGoal(binding.threadId, { signal })).goal : null;
|
|
870
|
+
if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw new Error("The native goal changed. Read it again before changing it.");
|
|
871
|
+
const result = await owner.updateGoal(sessionId, { ...input, threadId: binding.threadId,
|
|
872
|
+
createdAt: goal?.createdAt, objective: input.action === "set" ? input.objective : goal?.objective }, {
|
|
873
|
+
context: nativeContext,
|
|
874
|
+
prepareConversation: () => goalContext(configuration, context, signal, { prepareThread: true, tools })
|
|
875
|
+
});
|
|
876
|
+
if (result.ok === false) throw new Error(result.error);
|
|
877
|
+
return goalValue(result.goal);
|
|
878
|
+
},
|
|
879
|
+
async inspectAdmission({ messageId, threadId, nativeTurnId, configuration, context, goal: command, representation }) {
|
|
880
|
+
if (supplied) {
|
|
881
|
+
const prepared = await supplied.inspectionPreparation({ messageId, threadId }, context);
|
|
882
|
+
if (prepared.ok === false) return representation === "native" ? prepared : { accepted: false, ...prepared };
|
|
883
|
+
const target = await owner.conversationContext(sessionId, {}, prepared);
|
|
884
|
+
if (target.ok === false) return representation === "native" ? target : { accepted: false, ...target };
|
|
885
|
+
const receipt = await inspectCodexAppServerMessageAdmission({ provider: target.provider, threadId, messageId });
|
|
886
|
+
return representation === "native" ? receipt : { ...receipt, accepted: receipt.admission === "accepted" };
|
|
887
|
+
}
|
|
888
|
+
if (command || binding.goalReceipt?.messageId === messageId || binding.goalRequest?.messageId === messageId) {
|
|
889
|
+
// Old goal-message records stay inspection-only. Native goal state
|
|
890
|
+
// cannot certify the authored delivery invented by that adapter.
|
|
891
|
+
return { accepted: false, recoveryLimitation: "This older goal-message record requires offline inspection. Its saved history and delivery evidence were preserved." };
|
|
892
|
+
}
|
|
893
|
+
if (!binding.threadId) return { accepted: false };
|
|
894
|
+
const signal = AbortSignal.timeout(30_000);
|
|
895
|
+
await connect(signal, await selection(configuration, context, signal));
|
|
896
|
+
let acceptedTurn;
|
|
897
|
+
let cursor;
|
|
898
|
+
do {
|
|
899
|
+
const page = await native.client.request("thread/turns/list", { threadId: binding.threadId,
|
|
900
|
+
limit: 100, sortDirection: "desc", itemsView: "full", ...(cursor ? { cursor } : {}) }, { signal });
|
|
901
|
+
acceptedTurn = page.data.find(turn => (!nativeTurnId || turn.id === nativeTurnId) &&
|
|
902
|
+
turn.items?.some(item => item.type === "userMessage" && item.clientId === messageId));
|
|
903
|
+
cursor = page.nextCursor;
|
|
904
|
+
} while (!acceptedTurn && cursor);
|
|
905
|
+
if (!acceptedTurn) return { accepted: false };
|
|
906
|
+
const stored = await runtime.getSession(sessionId);
|
|
907
|
+
const tracked = codexAppServerTurnState(stored);
|
|
908
|
+
const before = await runtime.store.readConversationLog(sessionId);
|
|
909
|
+
const request = JSON.parse(await runtime.store.readMetadataValue(sessionId, "assistant_delivery")).engines.codex.pending;
|
|
910
|
+
if (request?.messageId === messageId) await owner.writeDeliveredUserMessage(runtime, sessionId,
|
|
911
|
+
request.displayMessage, messageId, request.turnMetadata, request.displayAttachments,
|
|
912
|
+
{ threadId: binding.threadId, turnId: acceptedTurn?.id || "" });
|
|
913
|
+
let recoveryLimitation;
|
|
914
|
+
if (!stored.agentRuns.length) {
|
|
915
|
+
recoveryLimitation = "Delivery is confirmed, but this older binding has no original native run record. Stored history is preserved; output recovery cannot be proven.";
|
|
916
|
+
} else if (tracked.threadId !== binding.threadId || acceptedTurn && tracked.turnId !== acceptedTurn.id && tracked.outerTurnId !== messageId) {
|
|
917
|
+
recoveryLimitation = "Delivery is confirmed. The original native owner tracks a different turn, so this inspection preserves stored history without reconstructing unowned output.";
|
|
918
|
+
} else {
|
|
919
|
+
owner.subscribeEvents(sessionId, native, binding.threadId, { providerKey });
|
|
920
|
+
await owner.reconcileThreadStatus(sessionId, native, binding.threadId, {
|
|
921
|
+
source: "delivery_inspection", requireTrackedTurn: true, observeLatestTurn: true
|
|
922
|
+
});
|
|
923
|
+
if (tracked.turnId) {
|
|
924
|
+
const recovered = await owner.submitAssistantResult(sessionId, binding.threadId, tracked.turnId, { recoverFromProvider: true });
|
|
925
|
+
if (recovered.reason === "error") throw new Error(recovered.error);
|
|
926
|
+
}
|
|
927
|
+
await owner.notificationQueue.drain(sessionId);
|
|
928
|
+
}
|
|
929
|
+
const after = await runtime.store.readConversationLog(sessionId);
|
|
930
|
+
const row = after.find(turn => (turn.user || turn.system)?.messageId === messageId);
|
|
931
|
+
const latest = codexAppServerTurnState(await runtime.getSession(sessionId));
|
|
932
|
+
return { accepted: true, conversationTurn: row, recovered: JSON.stringify(before) !== JSON.stringify(after),
|
|
933
|
+
nativeTurnId: acceptedTurn?.id || latest.turnId, status: latest.active ? "interrupted" : latest.status,
|
|
934
|
+
...(recoveryLimitation ? { recoveryLimitation } : {}) };
|
|
935
|
+
},
|
|
936
|
+
steer(command) {
|
|
937
|
+
if (!current || current.finished && !current.finishIfCurrent) throw new Error("Codex has no active turn to steer.");
|
|
938
|
+
return current.steer(command);
|
|
939
|
+
},
|
|
940
|
+
async run({ configuration, context, input, tools, signal, beforeDispatch, accept, onEvent, onNativeTurn, finishIfCurrent }) {
|
|
941
|
+
if (disposed || current) throw new Error("This Codex conversation is closed or already working.");
|
|
942
|
+
const completion = Promise.withResolvers();
|
|
943
|
+
const admission = Promise.withResolvers();
|
|
944
|
+
completion.promise.catch(() => {});
|
|
945
|
+
admission.promise.catch(() => {});
|
|
946
|
+
if (input.goal) throw new Error("Codex goals are controls. Inspect any unresolved goal-message record before continuing.");
|
|
947
|
+
const active = current = { configuration, context, inputId: input.messageId,
|
|
948
|
+
onEvent, onNativeTurn, signal, completion, admission, inputReady: admission.promise, finishIfCurrent,
|
|
949
|
+
admitted: false, dispatched: false, turnId: "" };
|
|
950
|
+
let cancellation;
|
|
951
|
+
let problem;
|
|
952
|
+
let delivered;
|
|
953
|
+
let waitForTurn = false;
|
|
954
|
+
let executingProvider;
|
|
955
|
+
const toolCalls = new Set();
|
|
956
|
+
function abort() {
|
|
957
|
+
if (cancellation) return;
|
|
958
|
+
cancellation = interrupt();
|
|
959
|
+
cancellation.then(() => active.completion.reject(signal.reason), error => active.completion.reject(error));
|
|
960
|
+
}
|
|
961
|
+
async function submit(command, operation) {
|
|
962
|
+
await admission.promise;
|
|
963
|
+
const ready = Promise.withResolvers();
|
|
964
|
+
const previous = active.inputReady;
|
|
965
|
+
active.inputReady = ready.promise;
|
|
966
|
+
try { return await operation(command, configuration, context, signal, tools); }
|
|
967
|
+
finally { ready.resolve(); active.inputReady = previous; }
|
|
968
|
+
}
|
|
969
|
+
active.steer = command => submit(command, sendMessage);
|
|
970
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
971
|
+
try {
|
|
972
|
+
signal.throwIfAborted();
|
|
973
|
+
// Preparation retains the provider's authenticated account and the
|
|
974
|
+
// stable application-tool entry points. The original owner alone
|
|
975
|
+
// subscribes to notifications and handles native turn state/output.
|
|
976
|
+
if (!supplied) {
|
|
977
|
+
const selected = await selection(configuration, context, signal);
|
|
978
|
+
await connect(signal, selected);
|
|
979
|
+
executingProvider = native;
|
|
980
|
+
}
|
|
981
|
+
if (!supplied) native.setServerRequestHandler(async ({ method, params }) => {
|
|
982
|
+
if (method !== "item/tool/call" || !tools || params?.threadId !== binding.threadId || params.namespace) {
|
|
983
|
+
throw new Error("This conversation does not authorize that native tool request.");
|
|
984
|
+
}
|
|
985
|
+
const operation = (async () => {
|
|
986
|
+
await active.inputReady;
|
|
987
|
+
const ownership = Promise.withResolvers();
|
|
988
|
+
owner.notificationQueue.run({ sessionId, provider: executingProvider }, () => {
|
|
989
|
+
const failure = executingProvider.observationFailure;
|
|
990
|
+
if (failure) ownership.reject(failure.cause || failure);
|
|
991
|
+
else ownership.resolve(params.turnId === active.turnId);
|
|
992
|
+
});
|
|
993
|
+
if (!await ownership.promise) throw new Error("This application tool call belongs to another turn.");
|
|
994
|
+
const result = await tools.execute({ id: params.callId, name: params.tool, arguments: JSON.stringify(params.arguments) });
|
|
995
|
+
return { success: result.ok, contentItems: [{ type: "inputText", text: JSON.stringify(result) }] };
|
|
996
|
+
})();
|
|
997
|
+
toolCalls.add(operation);
|
|
998
|
+
try { return await operation; }
|
|
999
|
+
catch (error) { active.completion.reject(error); throw error; }
|
|
1000
|
+
finally { toolCalls.delete(operation); }
|
|
1001
|
+
});
|
|
1002
|
+
const command = { input, beforeDispatch, accept };
|
|
1003
|
+
delivered = await sendMessage(command, configuration, context, signal, tools);
|
|
1004
|
+
waitForTurn = !supplied || delivered.value?.delivered === true;
|
|
1005
|
+
if (!finishIfCurrent && waitForTurn) {
|
|
1006
|
+
await completion.promise;
|
|
1007
|
+
await owner.notificationQueue.drain(sessionId);
|
|
1008
|
+
}
|
|
1009
|
+
} catch (error) { problem = error; }
|
|
1010
|
+
if (finishIfCurrent) {
|
|
1011
|
+
try {
|
|
1012
|
+
for (;;) {
|
|
1013
|
+
const settling = active.completion;
|
|
1014
|
+
if (waitForTurn) {
|
|
1015
|
+
try { await settling.promise; }
|
|
1016
|
+
catch (error) { problem = error; }
|
|
1017
|
+
}
|
|
1018
|
+
if (await finishIfCurrent(() => active.completion === settling)) break;
|
|
1019
|
+
// The same original dispatch established another native turn.
|
|
1020
|
+
// Keep its completion and Stop owner until that turn settles.
|
|
1021
|
+
problem = undefined;
|
|
1022
|
+
waitForTurn = true;
|
|
1023
|
+
}
|
|
1024
|
+
if (!problem && waitForTurn) await owner.notificationQueue.drain(sessionId);
|
|
1025
|
+
} catch (error) { problem = error; }
|
|
1026
|
+
}
|
|
1027
|
+
signal.removeEventListener("abort", abort);
|
|
1028
|
+
admission.reject(problem || signal.reason);
|
|
1029
|
+
try {
|
|
1030
|
+
// Account invalidation already owns this stop attempt. Its failed
|
|
1031
|
+
// proof stays available for explicit cleanup through the same owner.
|
|
1032
|
+
if ((!supplied && problem || signal.aborted) && problem?.code !== "codex_runtime_invalidated") {
|
|
1033
|
+
await (cancellation || interrupt());
|
|
1034
|
+
}
|
|
1035
|
+
} catch (error) { problem = error; }
|
|
1036
|
+
try {
|
|
1037
|
+
await owner.notificationQueue.drain(sessionId);
|
|
1038
|
+
// The original queue starts observation recovery without awaiting
|
|
1039
|
+
// it. Join only this provider's existing attempt before settling.
|
|
1040
|
+
if (!supplied) await providerOwner.pendingRecovery(providerKey, executingProvider);
|
|
1041
|
+
} catch (error) { problem = error; }
|
|
1042
|
+
finally {
|
|
1043
|
+
await Promise.allSettled([...toolCalls]);
|
|
1044
|
+
if (!supplied) {
|
|
1045
|
+
native?.setServerRequestHandler(null);
|
|
1046
|
+
owner.clearSessionRecoveryTimers(sessionId);
|
|
1047
|
+
}
|
|
1048
|
+
current = null;
|
|
1049
|
+
}
|
|
1050
|
+
if (problem) throw problem;
|
|
1051
|
+
return delivered;
|
|
1052
|
+
},
|
|
1053
|
+
cancel: interrupt,
|
|
1054
|
+
async dispose(options) {
|
|
1055
|
+
if (supplied) {
|
|
1056
|
+
try {
|
|
1057
|
+
const result = await disposeNative({ sessionId, namespace: conversation.namespace, native: supplied, options });
|
|
1058
|
+
if (result?.ok === false) throw Object.assign(new Error(result.error), result);
|
|
1059
|
+
disposed = true;
|
|
1060
|
+
current?.completion.reject(Object.assign(new Error("This conversation handle is closed."), { status: "interrupted" }));
|
|
1061
|
+
return result;
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
error.cleanupFailed = true;
|
|
1064
|
+
current?.completion.reject(error);
|
|
1065
|
+
throw error;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
await stop();
|
|
1069
|
+
owner.clearSessionRecoveryTimers(sessionId);
|
|
1070
|
+
for (const key of owner.eventSubscriptions.keys()) owner.unsubscribeEventSubscription(key);
|
|
1071
|
+
await owner.notificationQueue.drain(sessionId);
|
|
1072
|
+
await providerOwner.stopCachedProvider(providerKey, { preserveProcessExitProof: true, requireStopped: true });
|
|
1073
|
+
disposed = true;
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|