@jskit-ai/assistant-core 0.1.200 → 0.1.202
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,1082 @@
|
|
|
1
|
+
import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, normalizeCodexRunText, codexAppServerErrorMessage, codexAppServerSteerFailure, codexAppServerInterruptFailure, codexAppServerConversationTurnIsActive } from "./codexTurnState.js";
|
|
2
|
+
import { CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS, createCodexAppServerDetachedTurnWatcher } from "./codexDetachedTurn.js";
|
|
3
|
+
import { assertCodexAppServerHelperAccountIdentity, readCodexAppServerAccountIdentity, sendPreparedCodexAppServerHelperTurn } from "./codexHelperExecution.js";
|
|
4
|
+
import { codexAppServerRequestIsInvalid, codexAppServerThreadHasReadableHistory, codexAppServerThreadIsMissing, codexLocalImageInput, sendCodexAppServerPrompt } from "./codexProvider.js";
|
|
5
|
+
import { codexAppServerProjectHookTrustConfig, readCodexToolFreeConfiguration } from "./codexConfiguration.js";
|
|
6
|
+
import { codexAppServerAssistantItemText, codexAppServerContentText, classifyCodexAppServerEvent, codexAppServerNotificationThreadId, codexAppServerNotificationTurnId, codexAppServerNotificationTurnStatus, codexAppServerErrorText, codexAppServerThreadRawValue, codexAppServerProviderThreadAssistantSegments, codexAppServerProviderTurnId, codexAppServerProviderTurnStatus, codexAppServerProviderTurnItems, codexAppServerProviderTurnClientIds, codexAppServerThreadTurnId, codexAppServerThreadError } from "./codexEvents.js";
|
|
7
|
+
|
|
8
|
+
// Internal command facet of the original run owner; the same native conversation
|
|
9
|
+
// and turn-start collections retain their original lifetime and public references.
|
|
10
|
+
export function createCodexConversationCommands({
|
|
11
|
+
checkpoint,
|
|
12
|
+
codexAppServerConversationMessageId,
|
|
13
|
+
codexAppServerStreamMessage,
|
|
14
|
+
conversationPreparation,
|
|
15
|
+
ensureCodexAppServerProviderForActiveTurn,
|
|
16
|
+
errorPrefix,
|
|
17
|
+
helperLifecycle,
|
|
18
|
+
helperOwnershipError,
|
|
19
|
+
interruptFailedCode,
|
|
20
|
+
namespace,
|
|
21
|
+
providerSessions,
|
|
22
|
+
steerFailedCode
|
|
23
|
+
}) {
|
|
24
|
+
const codexAppServerConversations = new Map();
|
|
25
|
+
const codexAppServerConversationTurnStarts = new Map();
|
|
26
|
+
|
|
27
|
+
function codexAppServerConversation(sessionId = "", conversationId = "") {
|
|
28
|
+
return codexAppServerConversations.get(
|
|
29
|
+
namespace(sessionId)
|
|
30
|
+
)?.get(conversationId) || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasActiveConversation(sessionId) {
|
|
34
|
+
const conversations = codexAppServerConversations.get(
|
|
35
|
+
namespace(sessionId)
|
|
36
|
+
);
|
|
37
|
+
return Boolean(conversations && [...conversations.values()].some((conversation) => (
|
|
38
|
+
codexAppServerConversationTurnIsActive(conversation.status) ||
|
|
39
|
+
conversation.goal?.status === "active" && !conversation.provider.observationFailure
|
|
40
|
+
)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function withCodexAppServerConversationTurnStart(sessionId, input, operation) {
|
|
44
|
+
const messageId = normalizeCodexRunText(input.messageId);
|
|
45
|
+
const deliveryKey = messageId
|
|
46
|
+
? `${namespace(sessionId)}\0${normalizeCodexRunText(input.conversationId)}\0${messageId}`
|
|
47
|
+
: "";
|
|
48
|
+
const existing = deliveryKey ? codexAppServerConversationTurnStarts.get(deliveryKey) : null;
|
|
49
|
+
if (existing) {
|
|
50
|
+
return existing;
|
|
51
|
+
}
|
|
52
|
+
const start = operation();
|
|
53
|
+
if (deliveryKey) {
|
|
54
|
+
codexAppServerConversationTurnStarts.set(deliveryKey, start);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return await start;
|
|
58
|
+
} finally {
|
|
59
|
+
if (deliveryKey && codexAppServerConversationTurnStarts.get(deliveryKey) === start) {
|
|
60
|
+
codexAppServerConversationTurnStarts.delete(deliveryKey);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function observeCodexConversation(sessionId, conversationId, provider) {
|
|
66
|
+
const key = namespace(sessionId);
|
|
67
|
+
const conversations = codexAppServerConversations.get(key) || new Map();
|
|
68
|
+
let state = conversations.get(conversationId);
|
|
69
|
+
if (state?.provider === provider && state.unsubscribe) return state;
|
|
70
|
+
state?.unsubscribe?.();
|
|
71
|
+
state = { conversationId, provider, persistent: true, status: "ready", runId: "", error: "" };
|
|
72
|
+
state.unsubscribe = provider.subscribe((notification) => {
|
|
73
|
+
if (codexAppServerNotificationThreadId(notification) !== conversationId) return;
|
|
74
|
+
const turnId = codexAppServerNotificationTurnId(notification);
|
|
75
|
+
if (turnId) state.runId = turnId;
|
|
76
|
+
const status = codexAppServerNotificationTurnStatus(notification);
|
|
77
|
+
if (status) state.status = status;
|
|
78
|
+
const classification = classifyCodexAppServerEvent(notification);
|
|
79
|
+
if (notification.method === "item/agentMessage/delta") {
|
|
80
|
+
state.onEvent?.({ type: "text", text: notification.params.delta,
|
|
81
|
+
messageId: notification.params.itemId, threadId: conversationId, turnId,
|
|
82
|
+
message: codexAppServerStreamMessage(classification) });
|
|
83
|
+
} else if (["assistant_started", "live_progress", "final_assistant_result"].includes(classification.kind)) {
|
|
84
|
+
state.onEvent?.({ type: "message", threadId: conversationId, turnId,
|
|
85
|
+
message: { ...codexAppServerStreamMessage(classification),
|
|
86
|
+
...(classification.kind !== "assistant_started" ? { text: classification.text } : {}) } });
|
|
87
|
+
}
|
|
88
|
+
if (notification.method === "thread/goal/updated") state.goal = notification.params.goal;
|
|
89
|
+
if (notification.method === "thread/goal/cleared") state.goal = null;
|
|
90
|
+
if (notification.method === "turn/completed") {
|
|
91
|
+
state.onEvent = null;
|
|
92
|
+
const completedTurnId = state.runId;
|
|
93
|
+
const completedStatus = state.status;
|
|
94
|
+
void checkpoint(sessionId, {
|
|
95
|
+
outerTurnId: `codex:${conversationId}:${completedTurnId}`,
|
|
96
|
+
threadId: conversationId,
|
|
97
|
+
turnId: completedTurnId,
|
|
98
|
+
status: completedStatus
|
|
99
|
+
}).catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
conversations.set(conversationId, state);
|
|
103
|
+
codexAppServerConversations.set(key, conversations);
|
|
104
|
+
return state;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function readPersistentCodexConversation(sessionId, input, context) {
|
|
108
|
+
const conversationId = normalizeCodexRunText(input.conversationId);
|
|
109
|
+
const previous = codexAppServerConversation(sessionId, conversationId);
|
|
110
|
+
const state = observeCodexConversation(sessionId, conversationId, context.provider);
|
|
111
|
+
const thread = await context.provider.readThreadStatus(conversationId);
|
|
112
|
+
if (codexAppServerThreadRawValue(thread).historyMode !== "paginated") {
|
|
113
|
+
throw Object.assign(new Error("This conversation uses an unsupported Codex history format. Start a fresh conversation."), {
|
|
114
|
+
code: `${errorPrefix}codex_history_unsupported`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const turns = [];
|
|
118
|
+
let cursor;
|
|
119
|
+
do {
|
|
120
|
+
const page = await context.provider.listThreadTurns(conversationId, {
|
|
121
|
+
limit: 100, itemsView: "full", sortDirection: "asc", ...(cursor ? { cursor } : {})
|
|
122
|
+
});
|
|
123
|
+
turns.push(...page.data);
|
|
124
|
+
cursor = page.nextCursor;
|
|
125
|
+
} while (cursor);
|
|
126
|
+
const latest = turns.at(-1);
|
|
127
|
+
const runId = codexAppServerProviderTurnId(latest || {});
|
|
128
|
+
let status = codexAppServerThreadStatus(thread);
|
|
129
|
+
if (!codexAppServerConversationTurnIsActive(status) && latest) status = codexAppServerProviderTurnStatus(latest);
|
|
130
|
+
let goal = (await context.provider.readGoal(conversationId)).goal;
|
|
131
|
+
// A new backend observer cannot assume it saw the work preceding connection.
|
|
132
|
+
// Stop it once; browser reloads reuse the existing backend observer.
|
|
133
|
+
if (previous?.provider !== context.provider && (codexAppServerConversationTurnIsActive(status) || goal?.status === "active")) {
|
|
134
|
+
Object.assign(state, { runId, status: "inProgress", goal });
|
|
135
|
+
try {
|
|
136
|
+
await context.provider.stopThreadForObservationLoss(conversationId, "");
|
|
137
|
+
} catch (error) {
|
|
138
|
+
await context.provider.failObservation(error);
|
|
139
|
+
if (codexAppServerConversationTurnIsActive(state.status)) throw error;
|
|
140
|
+
}
|
|
141
|
+
status = "interrupted";
|
|
142
|
+
goal = (await context.provider.readGoal(conversationId)).goal;
|
|
143
|
+
}
|
|
144
|
+
if (goal?.status === "active") status = "inProgress";
|
|
145
|
+
Object.assign(state, { runId, status, goal });
|
|
146
|
+
const messages = [];
|
|
147
|
+
for (const turn of turns) {
|
|
148
|
+
const turnId = codexAppServerProviderTurnId(turn);
|
|
149
|
+
const complete = !codexAppServerConversationTurnIsActive(codexAppServerProviderTurnStatus(turn));
|
|
150
|
+
const items = codexAppServerProviderTurnItems(turn);
|
|
151
|
+
const finalIds = new Set(codexAppServerProviderThreadAssistantSegments({ turns: [turn] }, turnId).map(({ itemId }) => itemId));
|
|
152
|
+
for (const [index, item] of items.entries()) {
|
|
153
|
+
let role;
|
|
154
|
+
if (item.type === "reasoning") role = "thinking";
|
|
155
|
+
else if (item.type === "agentMessage") {
|
|
156
|
+
const progress = item.phase === "commentary" || (!item.phase && !finalIds.has(item.id) && (complete || index < items.length - 1));
|
|
157
|
+
role = progress ? "commentary" : "assistant";
|
|
158
|
+
}
|
|
159
|
+
else continue;
|
|
160
|
+
const text = role === "thinking"
|
|
161
|
+
? normalizeCodexRunText(codexAppServerContentText(item.summary)) || codexAppServerContentText(item.content)
|
|
162
|
+
: codexAppServerAssistantItemText(item);
|
|
163
|
+
if (text) messages.push({
|
|
164
|
+
id: codexAppServerConversationMessageId(conversationId, turnId, role, item.id || text),
|
|
165
|
+
...(item.id && role !== "thinking" ? { outputId: codexAppServerConversationMessageId(conversationId, turnId, "assistant-item", item.id) } : {}),
|
|
166
|
+
role,
|
|
167
|
+
text,
|
|
168
|
+
complete
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const latestText = latest ? codexAppServerProviderThreadAssistantSegments({ turns: [latest] }, runId)
|
|
173
|
+
.map(({ text }) => text).join("\n\n") : "";
|
|
174
|
+
return {
|
|
175
|
+
conversationId,
|
|
176
|
+
ok: true,
|
|
177
|
+
status,
|
|
178
|
+
runId,
|
|
179
|
+
messages,
|
|
180
|
+
goal,
|
|
181
|
+
admitted: Boolean(input.messageId && turns.some((turn) => codexAppServerProviderTurnClientIds(turn).includes(input.messageId))),
|
|
182
|
+
rawText: latestText.trim(),
|
|
183
|
+
error: state.error || codexAppServerThreadError(thread)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function codexAppServerEphemeralConversationSnapshot(state = {}) {
|
|
188
|
+
return {
|
|
189
|
+
conversationId: normalizeCodexRunText(state.conversationId),
|
|
190
|
+
error: normalizeCodexRunText(state.error),
|
|
191
|
+
message: normalizeCodexRunText(state.message),
|
|
192
|
+
ok: true,
|
|
193
|
+
outcome: state.outcome || null,
|
|
194
|
+
progressUpdates: Array.isArray(state.progressUpdates)
|
|
195
|
+
? state.progressUpdates.map((update = {}) => ({
|
|
196
|
+
id: normalizeCodexRunText(update.id),
|
|
197
|
+
text: normalizeCodexRunText(update.text)
|
|
198
|
+
})).filter((update) => update.id && update.text)
|
|
199
|
+
: [],
|
|
200
|
+
rawText: normalizeCodexRunText(state.rawText),
|
|
201
|
+
messageId: normalizeCodexRunText(state.messageId),
|
|
202
|
+
runId: normalizeCodexRunText(state.runId),
|
|
203
|
+
status: normalizeCodexRunText(state.status) || "ready",
|
|
204
|
+
turnMetadata: state.turnMetadata || null
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function codexAppServerConversationResult(operation) {
|
|
209
|
+
try {
|
|
210
|
+
return await operation();
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return conversationPreparation.failure(error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function prepareCodexAppServerConversationThread(provider, preparation, helperIsolation) {
|
|
217
|
+
if (preparation.inspection) {
|
|
218
|
+
const enforcement = await helperIsolation.inspect(provider, preparation.inspection);
|
|
219
|
+
return preparation.prepared(enforcement);
|
|
220
|
+
}
|
|
221
|
+
const config = preparation.projectHooks
|
|
222
|
+
? await codexAppServerProjectHookTrustConfig(provider, preparation.workdir)
|
|
223
|
+
: null;
|
|
224
|
+
return preparation.settings(config);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function acquireCodexAppServerConversationContext(sessionId, prepared, input = {}) {
|
|
228
|
+
if (prepared.result) return prepared.result;
|
|
229
|
+
const { context } = prepared;
|
|
230
|
+
if (context.assistantScope) {
|
|
231
|
+
const providerOptions = prepared.providerOptions;
|
|
232
|
+
const provider = await providerSessions.owner.ensureSession(
|
|
233
|
+
providerSessions.context(normalizeCodexRunText(sessionId), providerOptions)
|
|
234
|
+
);
|
|
235
|
+
const result = prepared.project(provider);
|
|
236
|
+
result.isolationConfig = await readCodexToolFreeConfiguration(
|
|
237
|
+
provider, context.workdir, conversationPreparation.isolation.assertCompatibility
|
|
238
|
+
);
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
let helperRestore = null;
|
|
242
|
+
if (prepared.helperTurn) {
|
|
243
|
+
const restoration = await prepared.helperRestoration;
|
|
244
|
+
helperRestore = helperLifecycle.assertRestored(
|
|
245
|
+
await helperLifecycle.restoreThreads(restoration.projectRuntimeRoot, restoration.context)
|
|
246
|
+
);
|
|
247
|
+
} else {
|
|
248
|
+
const threadId = normalizeCodexRunText(input.threadId || input.codexSessionId || input.conversationId);
|
|
249
|
+
if (threadId) {
|
|
250
|
+
await helperLifecycle.assertOrdinaryThreadAvailable(prepared.projectRuntimeRoot, threadId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const managedIdentity = prepared.managedIdentity;
|
|
254
|
+
const restoredAdmissionError = conversationPreparation.admissionError(sessionId);
|
|
255
|
+
if (restoredAdmissionError) throw restoredAdmissionError;
|
|
256
|
+
const activeProvider = prepared.helperTurn
|
|
257
|
+
? null
|
|
258
|
+
: await ensureCodexAppServerProviderForActiveTurn(context.session, managedIdentity);
|
|
259
|
+
const activeProviderAdmissionError = conversationPreparation.admissionError(sessionId);
|
|
260
|
+
if (activeProviderAdmissionError) throw activeProviderAdmissionError;
|
|
261
|
+
const providerOptions = activeProvider ? null : await prepared.providerOptions;
|
|
262
|
+
const providerOptionsAdmissionError = conversationPreparation.admissionError(sessionId);
|
|
263
|
+
if (providerOptionsAdmissionError) throw providerOptionsAdmissionError;
|
|
264
|
+
const provider = activeProvider?.provider || await providerSessions.owner.ensureSession(
|
|
265
|
+
providerSessions.context(normalizeCodexRunText(sessionId), providerOptions)
|
|
266
|
+
);
|
|
267
|
+
const providerAdmissionError = conversationPreparation.admissionError(sessionId);
|
|
268
|
+
if (providerAdmissionError) throw providerAdmissionError;
|
|
269
|
+
return prepared.project(provider, activeProvider?.providerOptions || providerOptions, helperRestore);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function codexAppServerConversationContext(sessionId, input, options) {
|
|
273
|
+
return acquireCodexAppServerConversationContext(sessionId,
|
|
274
|
+
await conversationPreparation.context(sessionId, input, options), input);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function codexAppServerEphemeralScopeContext(sessionId, input, scope) {
|
|
278
|
+
return acquireCodexAppServerConversationContext(sessionId,
|
|
279
|
+
await conversationPreparation.scope(sessionId, input, scope), input);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function readPreparedPersistentCodexConversation(sessionId, input, context) {
|
|
283
|
+
const result = await readPersistentCodexConversation(sessionId, input, context);
|
|
284
|
+
return { ...result, ...conversationPreparation.response(result.rawText) };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function createCodexAppServerConversation(sessionId, input = {}, options = {}) {
|
|
288
|
+
return codexAppServerConversationResult(async () => {
|
|
289
|
+
const context = await codexAppServerConversationContext(sessionId, input, options);
|
|
290
|
+
if (context.ok === false) return context;
|
|
291
|
+
const execution = conversationPreparation.execution(sessionId, input, options, context, null, "create");
|
|
292
|
+
return createPreparedCodexAppServerConversation(sessionId, input, {
|
|
293
|
+
provider: context.provider,
|
|
294
|
+
executionProfile: execution.executionProfile,
|
|
295
|
+
helperIsolation: conversationPreparation.isolation,
|
|
296
|
+
threadPreparation: () => execution.threadPreparation
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function startCodexAppServerConversationTurn(sessionId, input = {}, options = {}) {
|
|
302
|
+
const messageId = normalizeCodexRunText(input.messageId);
|
|
303
|
+
return withCodexAppServerConversationTurnStart(sessionId, input, () => codexAppServerConversationResult(async () => {
|
|
304
|
+
const conversationId = normalizeCodexRunText(input.conversationId);
|
|
305
|
+
const prompt = normalizeCodexRunText(input.message || input.prompt);
|
|
306
|
+
if (!conversationId || !prompt) {
|
|
307
|
+
return {
|
|
308
|
+
code: `${errorPrefix}agent_conversation_turn_input_required`,
|
|
309
|
+
error: "Assistant conversation turns require a conversation and message.",
|
|
310
|
+
ok: false
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const context = await codexAppServerConversationContext(sessionId, input, options);
|
|
314
|
+
if (context.ok === false) return context;
|
|
315
|
+
const conversationState = codexAppServerConversation(sessionId, conversationId);
|
|
316
|
+
const execution = conversationPreparation.execution(sessionId, input, options, context, conversationState, "start", { conversationId, prompt });
|
|
317
|
+
if (input.ephemeral === true && !conversationState) {
|
|
318
|
+
return {
|
|
319
|
+
...conversationPreparation.expired(conversationId, input),
|
|
320
|
+
code: `${errorPrefix}temporary_conversation_expired`,
|
|
321
|
+
ok: false,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
// Do not evaluate native preparation until the original duplicate/active
|
|
325
|
+
// checks and Helper isolation capability checks have run.
|
|
326
|
+
return startPreparedCodexAppServerConversationTurn(sessionId, input, {
|
|
327
|
+
executionProfile: execution.executionProfile,
|
|
328
|
+
helperIsolation: conversationPreparation.isolation,
|
|
329
|
+
prepareTurn: execution.prepareTurn,
|
|
330
|
+
authorized: execution.authorized,
|
|
331
|
+
readOnly: execution.readOnly,
|
|
332
|
+
actorMetadata: execution.actorMetadata,
|
|
333
|
+
onEvent: execution.onEvent,
|
|
334
|
+
projectResult: execution.projectResult,
|
|
335
|
+
conversationId,
|
|
336
|
+
messageId,
|
|
337
|
+
prompt,
|
|
338
|
+
provider: context.provider,
|
|
339
|
+
conversationState,
|
|
340
|
+
threadPreparation: () => execution.threadPreparation
|
|
341
|
+
});
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function readCodexAppServerConversation(sessionId, input = {}, options = {}) {
|
|
346
|
+
return codexAppServerConversationResult(async () => {
|
|
347
|
+
const conversationId = normalizeCodexRunText(input.conversationId);
|
|
348
|
+
if (!conversationId) {
|
|
349
|
+
return {
|
|
350
|
+
code: `${errorPrefix}agent_conversation_id_required`,
|
|
351
|
+
error: "Assistant conversation id is required.",
|
|
352
|
+
ok: false
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const conversationState = codexAppServerConversation(sessionId, conversationId);
|
|
356
|
+
if (conversationState && !input.persistent) {
|
|
357
|
+
return codexAppServerEphemeralConversationSnapshot(conversationState);
|
|
358
|
+
}
|
|
359
|
+
if (input.ephemeral === true) {
|
|
360
|
+
return conversationPreparation.expired(conversationId, input);
|
|
361
|
+
}
|
|
362
|
+
if (input.persistent && conversationState?.persistent && conversationState.provider.isAvailable?.() &&
|
|
363
|
+
!conversationState.provider.observationFailure && !conversationPreparation.admissionError(sessionId)) {
|
|
364
|
+
// Poll the already observed native thread. Execution environment setup
|
|
365
|
+
// belongs to Send and reconnection, not every history read.
|
|
366
|
+
return readPreparedPersistentCodexConversation(sessionId, input, { provider: conversationState.provider });
|
|
367
|
+
}
|
|
368
|
+
const context = await codexAppServerConversationContext(sessionId, input, options);
|
|
369
|
+
if (context.ok === false) {
|
|
370
|
+
return context;
|
|
371
|
+
}
|
|
372
|
+
if (input.persistent) return readPreparedPersistentCodexConversation(sessionId, input, context);
|
|
373
|
+
const thread = await context.provider.readThread(conversationId);
|
|
374
|
+
const runId = normalizeCodexRunText(input.runId) || codexAppServerThreadTurnId(thread);
|
|
375
|
+
const status = codexAppServerThreadStatus(thread);
|
|
376
|
+
const text = runId
|
|
377
|
+
? codexAppServerProviderThreadAssistantSegments(thread, runId)
|
|
378
|
+
.map((segment) => segment.text)
|
|
379
|
+
.join("\n\n")
|
|
380
|
+
: "";
|
|
381
|
+
return {
|
|
382
|
+
conversationId,
|
|
383
|
+
error: codexAppServerThreadError(thread),
|
|
384
|
+
ok: true,
|
|
385
|
+
runId,
|
|
386
|
+
status,
|
|
387
|
+
...conversationPreparation.response(text)
|
|
388
|
+
};
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function waitForCodexAppServerConversationTurn(sessionId, input = {}, {
|
|
393
|
+
assistantScope = null,
|
|
394
|
+
onEvent = null
|
|
395
|
+
} = {}) {
|
|
396
|
+
return codexAppServerConversationResult(async () => {
|
|
397
|
+
const conversationId = normalizeCodexRunText(input.conversationId);
|
|
398
|
+
const runId = normalizeCodexRunText(input.runId);
|
|
399
|
+
if (!conversationId || !runId) {
|
|
400
|
+
return {
|
|
401
|
+
code: `${errorPrefix}agent_conversation_run_required`,
|
|
402
|
+
error: "Waiting for an assistant conversation requires conversation and run ids.",
|
|
403
|
+
ok: false
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const scoped = assistantScope && codexAppServerConversation(sessionId, conversationId);
|
|
407
|
+
if (scoped?.executionProfile) {
|
|
408
|
+
if (scoped.runId !== runId || !scoped.completion) throw new Error("The scoped helper turn is unavailable.");
|
|
409
|
+
const result = await scoped.completion;
|
|
410
|
+
return { ...result, conversationId, ok: true, runId };
|
|
411
|
+
}
|
|
412
|
+
const context = await codexAppServerConversationContext(sessionId, input, {
|
|
413
|
+
assistantScope
|
|
414
|
+
});
|
|
415
|
+
if (context.ok === false) {
|
|
416
|
+
return context;
|
|
417
|
+
}
|
|
418
|
+
const result = await waitForPreparedCodexAppServerConversationTurn(sessionId, input, {
|
|
419
|
+
provider: context.provider,
|
|
420
|
+
onEvent
|
|
421
|
+
});
|
|
422
|
+
return { ...result, ...conversationPreparation.response(result.rawText) };
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function stopCodexAppServerConversation(sessionId, input = {}, options = {}) {
|
|
427
|
+
const conversationId = normalizeCodexRunText(input.conversationId);
|
|
428
|
+
const conversationState = codexAppServerConversation(sessionId, conversationId);
|
|
429
|
+
if (conversationState?.provider.observationFailure) {
|
|
430
|
+
await conversationState.provider.failObservation(conversationState.provider.observationFailure);
|
|
431
|
+
if (codexAppServerConversationTurnIsActive(conversationState.status)) {
|
|
432
|
+
throw new Error("Codex's stop is not yet confirmed.");
|
|
433
|
+
}
|
|
434
|
+
return { conversationId, ok: true, runId: conversationState.runId, status: "interrupted" };
|
|
435
|
+
}
|
|
436
|
+
if (input.ephemeral === true && !conversationState) {
|
|
437
|
+
return {
|
|
438
|
+
conversationExpired: true,
|
|
439
|
+
conversationId,
|
|
440
|
+
ok: true,
|
|
441
|
+
runId: normalizeCodexRunText(input.runId),
|
|
442
|
+
status: "interrupted"
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (input.persistent) {
|
|
446
|
+
const context = await codexAppServerConversationContext(sessionId, input, options);
|
|
447
|
+
if (context.ok === false) return context;
|
|
448
|
+
return stopPersistentCodexConversation({
|
|
449
|
+
conversationId,
|
|
450
|
+
conversationState,
|
|
451
|
+
provider: context.provider
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
const result = await interruptDetachedCodexAppServerConversationTurn(sessionId, {
|
|
455
|
+
agentSettings: input.agentSettings,
|
|
456
|
+
threadId: input.conversationId,
|
|
457
|
+
turnId: input.runId
|
|
458
|
+
}, options, conversationState);
|
|
459
|
+
return {
|
|
460
|
+
...result,
|
|
461
|
+
conversationId,
|
|
462
|
+
runId: normalizeCodexRunText(input.runId)
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function deleteScopedCodexAppServerConversation(sessionId, input = {}, options = {}) {
|
|
467
|
+
const conversationId = normalizeCodexRunText(input.conversationId);
|
|
468
|
+
const sessionKey = namespace(sessionId);
|
|
469
|
+
const conversations = codexAppServerConversations.get(sessionKey);
|
|
470
|
+
const conversationExpired = input.ephemeral === true && !conversations?.has(conversationId) &&
|
|
471
|
+
!(options.assistantScope && input.executionProfile);
|
|
472
|
+
let result;
|
|
473
|
+
if (conversationExpired) {
|
|
474
|
+
result = {
|
|
475
|
+
conversationExpired: true
|
|
476
|
+
};
|
|
477
|
+
} else {
|
|
478
|
+
result = await deleteDetachedCodexAppServerConversationThread(sessionId, {
|
|
479
|
+
agentSettings: input.agentSettings,
|
|
480
|
+
threadId: input.conversationId
|
|
481
|
+
}, options, { conversationId, sessionKey, conversations });
|
|
482
|
+
if (result.ok === false) {
|
|
483
|
+
return { ...result, conversationId };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
let providerExit = null;
|
|
487
|
+
if (options.assistantScope) {
|
|
488
|
+
const context = await codexAppServerEphemeralScopeContext(
|
|
489
|
+
sessionId,
|
|
490
|
+
input,
|
|
491
|
+
options.assistantScope
|
|
492
|
+
);
|
|
493
|
+
if (context.ok === false) {
|
|
494
|
+
return context;
|
|
495
|
+
}
|
|
496
|
+
const connection = providerSessions.context(normalizeCodexRunText(sessionId), context.providerOptions);
|
|
497
|
+
providerExit = await providerSessions.owner.stopProvider(connection.providerKey, context.providerOptions, {
|
|
498
|
+
preserveProcessExitProof: false,
|
|
499
|
+
requireStopped: true,
|
|
500
|
+
get runtimeHost() { return connection.runtimeHost; }
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
...result,
|
|
505
|
+
conversationId,
|
|
506
|
+
ok: conversationExpired ? providerExit?.ok !== false : result?.ok !== false,
|
|
507
|
+
...(providerExit ? { providerExit } : {})
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function closeCodexAppServerConversations(sessionId) {
|
|
512
|
+
const key = namespace(sessionId);
|
|
513
|
+
for (const state of codexAppServerConversations.get(key)?.values() || []) {
|
|
514
|
+
if (state.persistent) {
|
|
515
|
+
await stopCodexAppServerConversation(sessionId, { conversationId: state.conversationId, persistent: true });
|
|
516
|
+
state.unsubscribe?.();
|
|
517
|
+
} else {
|
|
518
|
+
await deleteScopedCodexAppServerConversation(sessionId, { conversationId: state.conversationId }).catch(() => null);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
codexAppServerConversations.delete(key);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function codexAppServerConversationControl(sessionId, input, {
|
|
525
|
+
assistantScope = null,
|
|
526
|
+
runtime = null,
|
|
527
|
+
session = null
|
|
528
|
+
} = {}, operation, dispatch) {
|
|
529
|
+
return codexAppServerConversationResult(async () => {
|
|
530
|
+
const prepared = conversationPreparation.control(sessionId, input, operation);
|
|
531
|
+
if (prepared.result) return prepared.result;
|
|
532
|
+
const { admission, threadId, turnId } = prepared;
|
|
533
|
+
try {
|
|
534
|
+
let context = null;
|
|
535
|
+
try {
|
|
536
|
+
context = await codexAppServerConversationContext(sessionId, input, { assistantScope, runtime, session });
|
|
537
|
+
} catch (error) {
|
|
538
|
+
const unavailable = prepared.unavailable();
|
|
539
|
+
if (unavailable) return unavailable;
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
if (context.ok === false) return context;
|
|
543
|
+
const unavailable = prepared.unavailable();
|
|
544
|
+
if (unavailable) return unavailable;
|
|
545
|
+
const policyResult = helperLifecycle?.inspectControl(operation, input, {
|
|
546
|
+
sessionId, threadId, turnId,
|
|
547
|
+
get retiredThreadIds() { return context.helperRestore?.retiredThreadIds; },
|
|
548
|
+
get projectRuntimeRoot() { return context.runtime?.stateRoot; },
|
|
549
|
+
get provider() { return context.provider; },
|
|
550
|
+
get workdir() { return context.workdir; }
|
|
551
|
+
});
|
|
552
|
+
// The original Helper-retirement return releases admission before its
|
|
553
|
+
// promise settles; only the native dispatch below is awaited here.
|
|
554
|
+
if (policyResult !== undefined) return policyResult;
|
|
555
|
+
if (operation === "interrupt") {
|
|
556
|
+
const unavailable = prepared.unavailable();
|
|
557
|
+
if (unavailable) return unavailable;
|
|
558
|
+
} else if (typeof context.provider.deleteThread !== "function") {
|
|
559
|
+
return {
|
|
560
|
+
code: `${errorPrefix}codex_detached_thread_delete_unavailable`,
|
|
561
|
+
error: "Codex app-server thread deletion is not available.",
|
|
562
|
+
ok: false,
|
|
563
|
+
statusCode: 409,
|
|
564
|
+
threadId
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return await dispatch({ provider: context.provider, threadId, turnId });
|
|
568
|
+
} finally {
|
|
569
|
+
admission.release();
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function interruptDetachedCodexAppServerConversationTurn(sessionId, input = {}, options = {}, conversationState = null) {
|
|
575
|
+
return interruptCodexAppServerConversation(conversationState, dispatch =>
|
|
576
|
+
codexAppServerConversationControl(sessionId, input, options, "interrupt", dispatch));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function deleteDetachedCodexAppServerConversationThread(sessionId, input = {}, options = {}, conversation = null) {
|
|
580
|
+
return deleteCodexAppServerConversation(conversation, dispatch =>
|
|
581
|
+
codexAppServerConversationControl(sessionId, input, options, "delete", dispatch));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function createPreparedCodexAppServerConversation(sessionId, input, {
|
|
585
|
+
provider,
|
|
586
|
+
executionProfile,
|
|
587
|
+
helperIsolation,
|
|
588
|
+
threadPreparation
|
|
589
|
+
} = {}) {
|
|
590
|
+
const prepareThread = () => prepareCodexAppServerConversationThread(provider, threadPreparation(), helperIsolation);
|
|
591
|
+
const thread = executionProfile
|
|
592
|
+
? (await helperIsolation.start(provider, prepareThread)).thread
|
|
593
|
+
: await provider.startThread({
|
|
594
|
+
...await prepareThread(),
|
|
595
|
+
...(input.ephemeral === true ? { ephemeral: true } : {})
|
|
596
|
+
});
|
|
597
|
+
const conversationId = normalizeCodexRunText(thread.id || thread.response?.thread?.id);
|
|
598
|
+
if (!conversationId) {
|
|
599
|
+
throw new Error("Codex app-server did not return a conversation id.");
|
|
600
|
+
}
|
|
601
|
+
if (input.persistent === true) observeCodexConversation(sessionId, conversationId, provider);
|
|
602
|
+
if (input.ephemeral === true) {
|
|
603
|
+
const sessionKey = namespace(sessionId);
|
|
604
|
+
const conversations = codexAppServerConversations.get(sessionKey) || new Map();
|
|
605
|
+
conversations.set(conversationId, {
|
|
606
|
+
conversationId,
|
|
607
|
+
executionProfile,
|
|
608
|
+
provider,
|
|
609
|
+
error: "",
|
|
610
|
+
message: "",
|
|
611
|
+
messageId: "",
|
|
612
|
+
nextProgressSequence: 0,
|
|
613
|
+
outcome: null,
|
|
614
|
+
progressUpdates: [],
|
|
615
|
+
rawText: "",
|
|
616
|
+
runId: "",
|
|
617
|
+
status: "ready",
|
|
618
|
+
turnMetadata: null,
|
|
619
|
+
watcher: null
|
|
620
|
+
});
|
|
621
|
+
codexAppServerConversations.set(sessionKey, conversations);
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
conversationId,
|
|
625
|
+
ok: true,
|
|
626
|
+
status: "ready"
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function steerCodexAppServerConversationTurn(input, {
|
|
631
|
+
conversationId,
|
|
632
|
+
conversationState,
|
|
633
|
+
messageId,
|
|
634
|
+
prompt,
|
|
635
|
+
provider
|
|
636
|
+
} = {}) {
|
|
637
|
+
const images = codexLocalImageInput(input.attachments || []);
|
|
638
|
+
const result = await provider.steerTurn(
|
|
639
|
+
conversationId,
|
|
640
|
+
conversationState.runId,
|
|
641
|
+
images.length ? [prompt, ...images] : prompt,
|
|
642
|
+
{ clientUserMessageId: messageId }
|
|
643
|
+
);
|
|
644
|
+
const failure = codexAppServerSteerFailure(result, steerFailedCode);
|
|
645
|
+
if (failure) return { ...failure, ok: false };
|
|
646
|
+
conversationState.messageId = messageId;
|
|
647
|
+
return {
|
|
648
|
+
conversationId,
|
|
649
|
+
messageId,
|
|
650
|
+
ok: true,
|
|
651
|
+
runId: conversationState.runId,
|
|
652
|
+
status: "inProgress",
|
|
653
|
+
deliveryMode: "steer"
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function acquireCodexAppServerDetachedThread(provider, requestedThreadId, threadSettings, input = {}) {
|
|
658
|
+
let thread = null;
|
|
659
|
+
if (requestedThreadId) {
|
|
660
|
+
thread = await provider.resumeThread(requestedThreadId, threadSettings);
|
|
661
|
+
}
|
|
662
|
+
if (!thread) {
|
|
663
|
+
thread = await provider.startThread({
|
|
664
|
+
...threadSettings,
|
|
665
|
+
...(input.ephemeral === true ? { ephemeral: true } : {})
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return thread;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function codexAppServerDetachedThreadId(thread, requestedThreadId) {
|
|
672
|
+
const threadId = normalizeCodexRunText(thread.id || thread.response?.thread?.id || requestedThreadId);
|
|
673
|
+
if (!threadId) {
|
|
674
|
+
throw new Error("Codex app-server did not return a detached chat thread id.");
|
|
675
|
+
}
|
|
676
|
+
return threadId;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async function dispatchCodexAppServerDetachedTurn({ provider, prompt, threadId }, authorized) {
|
|
680
|
+
const delivery = await sendCodexAppServerPrompt({ provider, prompt, threadId }, authorized);
|
|
681
|
+
const turnId = normalizeCodexRunText(delivery.turn?.id);
|
|
682
|
+
const status = normalizeCodexRunText(delivery.turn?.status || delivery.turn?.raw?.status);
|
|
683
|
+
return { delivery, status, turnId };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function runDetachedCodexAppServerTurn({
|
|
687
|
+
provider,
|
|
688
|
+
threadId,
|
|
689
|
+
timeoutMs,
|
|
690
|
+
onEvent: emitDetachedEvent,
|
|
691
|
+
onFailure
|
|
692
|
+
} = {}, operation) {
|
|
693
|
+
const watcher = createCodexAppServerDetachedTurnWatcher(provider, threadId, {
|
|
694
|
+
onEvent: (classification) => {
|
|
695
|
+
emitDetachedEvent({
|
|
696
|
+
classification,
|
|
697
|
+
threadId,
|
|
698
|
+
turnId: classification.turnId,
|
|
699
|
+
type: "notification"
|
|
700
|
+
});
|
|
701
|
+
},
|
|
702
|
+
timeoutMs
|
|
703
|
+
});
|
|
704
|
+
const waitForResult = watcher.wait();
|
|
705
|
+
void waitForResult.catch(() => null);
|
|
706
|
+
const throwWatcherFailure = async (fallbackError, status = "", {
|
|
707
|
+
waitForDetail = false
|
|
708
|
+
} = {}) => {
|
|
709
|
+
if (waitForDetail) {
|
|
710
|
+
watcher.failAfterDetailGrace(fallbackError);
|
|
711
|
+
} else {
|
|
712
|
+
watcher.failNow(fallbackError);
|
|
713
|
+
}
|
|
714
|
+
const error = await waitForResult.then(
|
|
715
|
+
() => fallbackError,
|
|
716
|
+
(watcherError) => watcherError
|
|
717
|
+
);
|
|
718
|
+
throw await onFailure(error, status);
|
|
719
|
+
};
|
|
720
|
+
const { delivery, status, turnId } = await operation(throwWatcherFailure);
|
|
721
|
+
watcher.setTurnId(turnId);
|
|
722
|
+
emitDetachedEvent({
|
|
723
|
+
status,
|
|
724
|
+
threadId,
|
|
725
|
+
turnId,
|
|
726
|
+
type: "turn"
|
|
727
|
+
});
|
|
728
|
+
if (codexAppServerTurnStatusIsProviderFailure(status)) {
|
|
729
|
+
const providerError = codexAppServerErrorText(
|
|
730
|
+
delivery.turn?.raw?.error ||
|
|
731
|
+
delivery.turn?.response?.turn?.error ||
|
|
732
|
+
delivery.turn?.error
|
|
733
|
+
);
|
|
734
|
+
const error = new Error(providerError || `Codex app-server turn ${status}.`);
|
|
735
|
+
await throwWatcherFailure(error, status, {
|
|
736
|
+
waitForDetail: true
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
|
|
740
|
+
await watcher.completeNow(status);
|
|
741
|
+
}
|
|
742
|
+
let result = null;
|
|
743
|
+
try {
|
|
744
|
+
result = await waitForResult;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
throw await onFailure(error, status);
|
|
747
|
+
}
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function startPreparedCodexAppServerConversationTurn(sessionId, input, {
|
|
752
|
+
conversationId,
|
|
753
|
+
messageId,
|
|
754
|
+
prompt,
|
|
755
|
+
provider,
|
|
756
|
+
conversationState,
|
|
757
|
+
executionProfile,
|
|
758
|
+
actorMetadata,
|
|
759
|
+
onEvent,
|
|
760
|
+
projectResult,
|
|
761
|
+
helperIsolation,
|
|
762
|
+
threadPreparation,
|
|
763
|
+
prepareTurn,
|
|
764
|
+
authorized,
|
|
765
|
+
readOnly
|
|
766
|
+
} = {}) {
|
|
767
|
+
if (conversationState && messageId && conversationState.messageId === messageId) {
|
|
768
|
+
return codexAppServerEphemeralConversationSnapshot(conversationState);
|
|
769
|
+
}
|
|
770
|
+
if (conversationState && codexAppServerConversationTurnIsActive(conversationState.status)) {
|
|
771
|
+
if (input.steer === true) {
|
|
772
|
+
return steerCodexAppServerConversationTurn(input, {
|
|
773
|
+
conversationId,
|
|
774
|
+
conversationState,
|
|
775
|
+
messageId,
|
|
776
|
+
prompt,
|
|
777
|
+
provider
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
code: `${errorPrefix}temporary_conversation_turn_active`,
|
|
782
|
+
error: "Temporary AI is already working on this conversation.",
|
|
783
|
+
ok: false
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
if (!conversationState || input.persistent === true) {
|
|
787
|
+
const prepareThread = () => prepareCodexAppServerConversationThread(provider, threadPreparation(), helperIsolation);
|
|
788
|
+
if (executionProfile) await helperIsolation.resume(provider, conversationId, prepareThread);
|
|
789
|
+
else await provider.resumeThread(conversationId, await prepareThread());
|
|
790
|
+
}
|
|
791
|
+
let admittedAccountIdentitySignature = "";
|
|
792
|
+
if (executionProfile) {
|
|
793
|
+
admittedAccountIdentitySignature = await readCodexAppServerAccountIdentity(provider, helperOwnershipError);
|
|
794
|
+
}
|
|
795
|
+
let watcher = null;
|
|
796
|
+
let waitForResult = null;
|
|
797
|
+
if (conversationState && !input.persistent) {
|
|
798
|
+
Object.assign(conversationState, {
|
|
799
|
+
error: "",
|
|
800
|
+
message: "",
|
|
801
|
+
messageId,
|
|
802
|
+
outcome: null,
|
|
803
|
+
progressUpdates: [],
|
|
804
|
+
rawText: "",
|
|
805
|
+
runId: "",
|
|
806
|
+
status: "starting",
|
|
807
|
+
turnMetadata: actorMetadata
|
|
808
|
+
});
|
|
809
|
+
watcher = createCodexAppServerDetachedTurnWatcher(provider, conversationId, {
|
|
810
|
+
includeThreadHistory: false,
|
|
811
|
+
timeoutMs: executionProfile?.limits.timeoutMs || 0,
|
|
812
|
+
onEvent(classification = {}) {
|
|
813
|
+
const current = codexAppServerConversation(sessionId, conversationId);
|
|
814
|
+
if (!current || (classification.turnId && current.runId && classification.turnId !== current.runId)) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
onEvent?.(classification, current);
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
waitForResult = watcher.wait();
|
|
821
|
+
void waitForResult.catch(() => null);
|
|
822
|
+
conversationState.watcher = watcher;
|
|
823
|
+
}
|
|
824
|
+
let delivery = null;
|
|
825
|
+
if (input.persistent && conversationState) conversationState.onEvent = onEvent;
|
|
826
|
+
try {
|
|
827
|
+
await input.onPromptSending?.({ threadId: conversationId });
|
|
828
|
+
if (executionProfile) {
|
|
829
|
+
const prepared = prepareTurn();
|
|
830
|
+
delivery = await sendPreparedCodexAppServerHelperTurn(provider, prepared, helperIsolation);
|
|
831
|
+
} else {
|
|
832
|
+
delivery = await sendCodexAppServerPrompt({
|
|
833
|
+
clientUserMessageId: input.messageId,
|
|
834
|
+
outputSchema: input.outputSchema,
|
|
835
|
+
prompt,
|
|
836
|
+
attachments: input.attachments,
|
|
837
|
+
provider,
|
|
838
|
+
readOnly,
|
|
839
|
+
threadId: conversationId
|
|
840
|
+
}, authorized);
|
|
841
|
+
}
|
|
842
|
+
} catch (error) {
|
|
843
|
+
if (input.persistent && conversationState) conversationState.onEvent = null;
|
|
844
|
+
watcher?.failNow(error);
|
|
845
|
+
await waitForResult?.catch(() => null);
|
|
846
|
+
if (conversationState) {
|
|
847
|
+
Object.assign(conversationState, {
|
|
848
|
+
error: codexAppServerErrorMessage(error, "Temporary AI message could not be sent."),
|
|
849
|
+
status: "failed",
|
|
850
|
+
watcher: null
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
const runId = normalizeCodexRunText(delivery.turn?.id);
|
|
856
|
+
const status = normalizeCodexRunText(delivery.turn?.status || delivery.turn?.raw?.status);
|
|
857
|
+
if (!runId) {
|
|
858
|
+
watcher?.failNow(new Error("Codex app-server accepted a conversation turn without returning its id."));
|
|
859
|
+
await waitForResult?.catch(() => null);
|
|
860
|
+
throw new Error("Codex app-server accepted a conversation turn without returning its id.");
|
|
861
|
+
}
|
|
862
|
+
if (conversationState && !input.persistent) {
|
|
863
|
+
let nativeCompleted = false;
|
|
864
|
+
Object.assign(conversationState, {
|
|
865
|
+
runId,
|
|
866
|
+
status: !executionProfile && codexAppServerTurnStatusIsSuccessfulComplete(status) ? status : "inProgress"
|
|
867
|
+
});
|
|
868
|
+
watcher.setTurnId(runId);
|
|
869
|
+
if (codexAppServerTurnStatusIsProviderFailure(status)) {
|
|
870
|
+
watcher.failNow(new Error(`Codex app-server turn ${status}.`));
|
|
871
|
+
} else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
|
|
872
|
+
await watcher.completeNow(status);
|
|
873
|
+
}
|
|
874
|
+
conversationState.completion = waitForResult.then(async (result = {}) => {
|
|
875
|
+
const current = codexAppServerConversation(sessionId, conversationId);
|
|
876
|
+
if (!current || current.runId !== runId) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
nativeCompleted = true;
|
|
880
|
+
if (!executionProfile) current.status = result.status || "completed";
|
|
881
|
+
if (executionProfile) {
|
|
882
|
+
const completingRunId = current.runId;
|
|
883
|
+
await assertCodexAppServerHelperAccountIdentity(provider, admittedAccountIdentitySignature, helperOwnershipError);
|
|
884
|
+
if (codexAppServerConversation(sessionId, conversationId) !== current ||
|
|
885
|
+
current.runId !== completingRunId || !codexAppServerConversationTurnIsActive(current.status)) {
|
|
886
|
+
throw new Error("The scoped helper turn is unavailable.");
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const response = projectResult(result);
|
|
890
|
+
Object.assign(current, {
|
|
891
|
+
error: "",
|
|
892
|
+
...response,
|
|
893
|
+
status: result.status || "completed",
|
|
894
|
+
watcher: null
|
|
895
|
+
});
|
|
896
|
+
return codexAppServerEphemeralConversationSnapshot(current);
|
|
897
|
+
}).catch(async (error) => {
|
|
898
|
+
const current = codexAppServerConversation(sessionId, conversationId);
|
|
899
|
+
if (!current || current.runId !== runId || current.status === "interrupted") {
|
|
900
|
+
throw error;
|
|
901
|
+
}
|
|
902
|
+
if (executionProfile && !nativeCompleted && codexAppServerConversationTurnIsActive(current.status)) {
|
|
903
|
+
try {
|
|
904
|
+
const interrupted = await provider.interruptTurn(conversationId, runId);
|
|
905
|
+
const failure = codexAppServerInterruptFailure(interrupted, interruptFailedCode);
|
|
906
|
+
if (failure) throw new Error(failure.error);
|
|
907
|
+
} catch {
|
|
908
|
+
current.error = "The helper deadline ended, but Stop could not be confirmed. Retry Stop before reusing this helper.";
|
|
909
|
+
current.watcher = null;
|
|
910
|
+
throw new Error(current.error);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
Object.assign(current, {
|
|
914
|
+
error: codexAppServerErrorMessage(error, "Temporary AI turn failed."),
|
|
915
|
+
status: "failed",
|
|
916
|
+
watcher: null
|
|
917
|
+
});
|
|
918
|
+
throw error;
|
|
919
|
+
});
|
|
920
|
+
void conversationState.completion.catch(() => null);
|
|
921
|
+
}
|
|
922
|
+
if (input.persistent && conversationState) Object.assign(conversationState, { runId, status, messageId });
|
|
923
|
+
return { conversationId, messageId, ok: true, runId, status };
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function interruptCodexAppServerConversation(conversationState, operation) {
|
|
927
|
+
const result = await operation(async ({ provider, threadId, turnId }) => {
|
|
928
|
+
const result = await provider.interruptTurn(threadId, turnId);
|
|
929
|
+
const interruptFailure = codexAppServerInterruptFailure(result, interruptFailedCode);
|
|
930
|
+
if (interruptFailure) {
|
|
931
|
+
return {
|
|
932
|
+
...interruptFailure,
|
|
933
|
+
result,
|
|
934
|
+
threadId,
|
|
935
|
+
turnId
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
ok: true,
|
|
940
|
+
result,
|
|
941
|
+
status: "interrupted",
|
|
942
|
+
threadId,
|
|
943
|
+
turnId
|
|
944
|
+
};
|
|
945
|
+
});
|
|
946
|
+
if (result.ok !== false && conversationState) {
|
|
947
|
+
conversationState.status = "interrupted";
|
|
948
|
+
conversationState.watcher?.failNow(new Error("Temporary AI turn was stopped."));
|
|
949
|
+
conversationState.watcher = null;
|
|
950
|
+
}
|
|
951
|
+
return result;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function deleteCodexAppServerConversation(conversation, operation) {
|
|
955
|
+
const result = await operation(async ({ provider, threadId }) => {
|
|
956
|
+
try {
|
|
957
|
+
const result = await provider.deleteThread(threadId);
|
|
958
|
+
return {
|
|
959
|
+
ok: true,
|
|
960
|
+
result,
|
|
961
|
+
status: "deleted",
|
|
962
|
+
threadId
|
|
963
|
+
};
|
|
964
|
+
} catch (error) {
|
|
965
|
+
if (
|
|
966
|
+
codexAppServerRequestIsInvalid(error, "thread/delete") &&
|
|
967
|
+
!await codexAppServerThreadHasReadableHistory(provider, threadId)
|
|
968
|
+
) {
|
|
969
|
+
return {
|
|
970
|
+
ok: true,
|
|
971
|
+
status: "notFound",
|
|
972
|
+
threadId
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
throw error;
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
if (result.ok !== false && conversation) {
|
|
979
|
+
const { conversationId, sessionKey, conversations } = conversation;
|
|
980
|
+
conversations?.get(conversationId)?.watcher?.failNow(new Error("Temporary AI conversation was closed."));
|
|
981
|
+
conversations?.get(conversationId)?.unsubscribe?.();
|
|
982
|
+
conversations?.delete(conversationId);
|
|
983
|
+
if (conversations?.size === 0) {
|
|
984
|
+
codexAppServerConversations.delete(sessionKey);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return result;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function stopPersistentCodexConversation({
|
|
991
|
+
conversationId,
|
|
992
|
+
conversationState,
|
|
993
|
+
provider
|
|
994
|
+
} = {}) {
|
|
995
|
+
try {
|
|
996
|
+
await provider.readThreadStatus(conversationId);
|
|
997
|
+
await provider.stopThreadForObservationLoss(conversationId, "");
|
|
998
|
+
} catch (error) {
|
|
999
|
+
if (!codexAppServerThreadIsMissing(error, conversationId) || error.message.toLowerCase().startsWith("thread not loaded:")) throw error;
|
|
1000
|
+
}
|
|
1001
|
+
if (conversationState) {
|
|
1002
|
+
conversationState.status = "interrupted";
|
|
1003
|
+
if (conversationState.goal?.status === "active") conversationState.goal = { ...conversationState.goal, status: "paused" };
|
|
1004
|
+
conversationState.watcher?.failNow(new Error("Temporary AI turn was stopped."));
|
|
1005
|
+
conversationState.watcher = null;
|
|
1006
|
+
}
|
|
1007
|
+
return { ok: true, conversationId, status: "interrupted" };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
async function waitForPreparedCodexAppServerConversationTurn(sessionId, input, {
|
|
1011
|
+
provider,
|
|
1012
|
+
onEvent = null
|
|
1013
|
+
} = {}) {
|
|
1014
|
+
const conversationId = normalizeCodexRunText(input.conversationId);
|
|
1015
|
+
const runId = normalizeCodexRunText(input.runId);
|
|
1016
|
+
const watcher = createCodexAppServerDetachedTurnWatcher(provider, conversationId, {
|
|
1017
|
+
onEvent,
|
|
1018
|
+
timeoutMs: Number(input.timeoutMs || 0) > 0
|
|
1019
|
+
? Number(input.timeoutMs)
|
|
1020
|
+
: input.persistent === true ? 0 : CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS
|
|
1021
|
+
});
|
|
1022
|
+
const conversationState = codexAppServerConversation(sessionId, conversationId);
|
|
1023
|
+
if (input.persistent && conversationState) conversationState.watcher = watcher;
|
|
1024
|
+
const waitForResult = watcher.wait();
|
|
1025
|
+
void waitForResult.catch(() => {});
|
|
1026
|
+
watcher.setTurnId(runId);
|
|
1027
|
+
try {
|
|
1028
|
+
const current = await provider.readThread(conversationId);
|
|
1029
|
+
const status = codexAppServerThreadStatus(current);
|
|
1030
|
+
if (codexAppServerTurnStatusIsProviderFailure(status)) {
|
|
1031
|
+
watcher.failNow(new Error(
|
|
1032
|
+
codexAppServerThreadError(current) || `Codex app-server turn ${status}.`
|
|
1033
|
+
));
|
|
1034
|
+
} else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
|
|
1035
|
+
await watcher.completeNow(status);
|
|
1036
|
+
}
|
|
1037
|
+
const result = await waitForResult;
|
|
1038
|
+
return {
|
|
1039
|
+
conversationId,
|
|
1040
|
+
ok: true,
|
|
1041
|
+
runId,
|
|
1042
|
+
status: result.status || status || "completed",
|
|
1043
|
+
rawText: result.text
|
|
1044
|
+
};
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
watcher.failNow(error);
|
|
1047
|
+
throw error;
|
|
1048
|
+
} finally {
|
|
1049
|
+
if (conversationState?.watcher === watcher) conversationState.watcher = null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return {
|
|
1054
|
+
codexAppServerConversations,
|
|
1055
|
+
codexAppServerConversationTurnStarts,
|
|
1056
|
+
codexAppServerConversation,
|
|
1057
|
+
hasActiveConversation,
|
|
1058
|
+
withCodexAppServerConversationTurnStart,
|
|
1059
|
+
observeCodexConversation,
|
|
1060
|
+
readPersistentCodexConversation,
|
|
1061
|
+
codexAppServerEphemeralConversationSnapshot,
|
|
1062
|
+
prepareCodexAppServerConversationThread,
|
|
1063
|
+
codexAppServerConversationContext,
|
|
1064
|
+
codexAppServerEphemeralScopeContext,
|
|
1065
|
+
createCodexAppServerConversation,
|
|
1066
|
+
startCodexAppServerConversationTurn,
|
|
1067
|
+
readCodexAppServerConversation,
|
|
1068
|
+
waitForCodexAppServerConversationTurn,
|
|
1069
|
+
stopCodexAppServerConversation,
|
|
1070
|
+
deleteScopedCodexAppServerConversation,
|
|
1071
|
+
closeCodexAppServerConversations,
|
|
1072
|
+
interruptDetachedCodexAppServerConversationTurn,
|
|
1073
|
+
deleteDetachedCodexAppServerConversationThread,
|
|
1074
|
+
steerCodexAppServerConversationTurn,
|
|
1075
|
+
acquireCodexAppServerDetachedThread,
|
|
1076
|
+
codexAppServerDetachedThreadId,
|
|
1077
|
+
dispatchCodexAppServerDetachedTurn,
|
|
1078
|
+
runDetachedCodexAppServerTurn,
|
|
1079
|
+
interruptCodexAppServerConversation,
|
|
1080
|
+
stopPersistentCodexConversation
|
|
1081
|
+
};
|
|
1082
|
+
}
|