@jskit-ai/assistant-core 0.1.208 → 0.1.211
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +5 -5
- package/src/client/conversation/messageDelivery.js +7 -1
- package/src/server/conversation/codexHistoryAdapter.js +55 -2
- package/src/server/conversation/codexMessageCommands.js +2 -0
- package/src/server/conversation/codexMessageDelivery.js +7 -2
- package/src/server/conversation/codexNotificationObserver.js +15 -2
- package/src/server/conversation/codexProvider.js +64 -7
- package/src/server/conversation/codexTurn.js +16 -2
- package/src/server/conversation/codexTurnRecovery.js +104 -18
- package/src/server/conversation/continuity.js +3 -0
- package/src/server/conversation/openCodeRuntime.js +4 -0
- package/src/server/conversation/providers/codexDriver.js +106 -36
- package/src/server/conversation/providers/opencodeDriver.js +94 -37
- package/src/server/conversation/runtime.js +67 -7
- package/src/server/conversation/tools.js +5 -2
- package/src/server/conversation/transcript.js +2 -2
- package/src/server/hosts/openCodeProcess.js +1 -0
- package/test/claudeConversationSession.test.js +49 -0
- package/test/codexConversationSession.test.js +346 -0
- package/test/codexHistoryAdapter.test.js +103 -0
- package/test/codexTurn.test.js +542 -0
- package/test/conversationSession.test.js +19 -0
- package/test/conversationTools.test.js +298 -0
- package/test/messageDelivery.test.js +18 -0
- package/test/openCodeRuntime.test.js +29 -0
- package/test/opencodeConversationSession.test.js +354 -0
|
@@ -41,6 +41,9 @@ export function createCodexTurnRecovery({
|
|
|
41
41
|
captureContext,
|
|
42
42
|
runInContext,
|
|
43
43
|
activeReconcileMs,
|
|
44
|
+
finalizingGraceAfterHistoryRead,
|
|
45
|
+
failureDetailGraceMs,
|
|
46
|
+
notificationQueue,
|
|
44
47
|
hasRuntime,
|
|
45
48
|
recoverAdmission,
|
|
46
49
|
turnOutcomes,
|
|
@@ -78,6 +81,7 @@ export function createCodexTurnRecovery({
|
|
|
78
81
|
adoptSuccessorTurn: adoptCodexAppServerSuccessorTurn
|
|
79
82
|
} = settlement;
|
|
80
83
|
const codexAppServerPromptDeliveries = promptDeliveries;
|
|
84
|
+
const retainFinalizingTimer = finalizingGraceAfterHistoryRead || failureDetailGraceMs > 0;
|
|
81
85
|
|
|
82
86
|
function scheduleCodexAppServerActiveRecovery(sessionId = "", delayMs = activeReconcileMs) {
|
|
83
87
|
const normalizedSessionId = normalizeCodexRunText(sessionId);
|
|
@@ -100,7 +104,8 @@ export function createCodexTurnRecovery({
|
|
|
100
104
|
function scheduleCodexAppServerFinalizingRecovery(sessionId = "", threadId = "", turnId = "", {
|
|
101
105
|
completedAt = "",
|
|
102
106
|
status = "completed",
|
|
103
|
-
updatedAt = ""
|
|
107
|
+
updatedAt = "",
|
|
108
|
+
failureDetail = null
|
|
104
109
|
} = {}) {
|
|
105
110
|
const normalizedSessionId = normalizeCodexRunText(sessionId);
|
|
106
111
|
const normalizedThreadId = normalizeCodexRunText(threadId);
|
|
@@ -115,30 +120,62 @@ export function createCodexTurnRecovery({
|
|
|
115
120
|
normalizedTurnId
|
|
116
121
|
);
|
|
117
122
|
const projectContext = captureContext();
|
|
118
|
-
const delayMs =
|
|
119
|
-
|
|
120
|
-
state: "finalizing",
|
|
121
|
-
updatedAt
|
|
122
|
-
});
|
|
123
|
+
const delayMs = failureDetail
|
|
124
|
+
? Math.max(0, failureDetail.expiresAt - Date.now())
|
|
125
|
+
: codexAppServerFinalizingRemainingMs({ completedAt, state: "finalizing", updatedAt });
|
|
123
126
|
const timer = setTimeout(() => {
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
if (failureDetail) {
|
|
128
|
+
notificationQueue.run({ projectContext, provider: failureDetail.provider, sessionId: normalizedSessionId }, async () => {
|
|
129
|
+
if (codexAppServerFinalizingTimers.get(key) !== timer) return;
|
|
130
|
+
try {
|
|
131
|
+
return await stopCodexAppServerTurnWithProviderFailure(normalizedSessionId, normalizedThreadId, normalizedTurnId,
|
|
132
|
+
{ ...failureDetail, failureDetailReady: true });
|
|
133
|
+
} finally {
|
|
134
|
+
if (codexAppServerFinalizingTimers.get(key) === timer) {
|
|
135
|
+
clearCodexAppServerFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// The post-read policy keeps its first clock while expiry rereads history.
|
|
142
|
+
if (!retainFinalizingTimer) codexAppServerFinalizingTimers.delete(key);
|
|
143
|
+
const recovery = runInContext(
|
|
126
144
|
projectContext,
|
|
127
145
|
() => recoverCodexAppServerFinalizingTurn(
|
|
128
146
|
normalizedSessionId,
|
|
129
147
|
normalizedThreadId,
|
|
130
148
|
normalizedTurnId,
|
|
131
|
-
{ status }
|
|
149
|
+
{ status, timerRecovery: finalizingGraceAfterHistoryRead }
|
|
132
150
|
)
|
|
133
151
|
);
|
|
152
|
+
if (retainFinalizingTimer) {
|
|
153
|
+
timer.finalizingRecovery = recovery;
|
|
154
|
+
void recovery.then(
|
|
155
|
+
() => { if (timer.finalizingRecovery === recovery) delete timer.finalizingRecovery; },
|
|
156
|
+
error => {
|
|
157
|
+
if (timer.finalizingRecovery === recovery) delete timer.finalizingRecovery;
|
|
158
|
+
debugLog("appServerFinalizingRecovery.error", {
|
|
159
|
+
error: debugError(error), sessionId: normalizedSessionId,
|
|
160
|
+
threadId: normalizedThreadId, turnId: normalizedTurnId
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
void recovery;
|
|
166
|
+
}
|
|
134
167
|
}, delayMs);
|
|
168
|
+
if (failureDetailGraceMs > 0) timer.finalizingExpiresAt = Date.now() + delayMs;
|
|
169
|
+
if (failureDetail) timer.failureDetail = failureDetail;
|
|
170
|
+
else if (finalizingGraceAfterHistoryRead) timer.finalizingStartedAt = completedAt;
|
|
135
171
|
timer.unref?.();
|
|
136
172
|
codexAppServerFinalizingTimers.set(key, timer);
|
|
137
173
|
}
|
|
138
174
|
|
|
139
175
|
async function reconcileCodexAppServerActiveTurn(session = {}, {
|
|
140
176
|
provider: suppliedProvider = null,
|
|
141
|
-
runtime = null
|
|
177
|
+
runtime = null,
|
|
178
|
+
providerError = null
|
|
142
179
|
} = {}) {
|
|
143
180
|
const sessionId = normalizeCodexRunText(session.sessionId);
|
|
144
181
|
const trackedTurn = turnState(session);
|
|
@@ -307,8 +344,10 @@ export function createCodexTurnRecovery({
|
|
|
307
344
|
turnId: completedTurnId
|
|
308
345
|
});
|
|
309
346
|
if (codexAppServerTurnStatusIsProviderFailure(status)) {
|
|
347
|
+
const exactError = providerError?.threadId === currentTurn.threadId && providerError?.turnId === completedTurnId;
|
|
310
348
|
await stopCodexAppServerTurnWithProviderFailure(sessionId, currentTurn.threadId, completedTurnId, {
|
|
311
|
-
error: codexAppServerThreadError(thread),
|
|
349
|
+
error: exactError ? providerError.error : codexAppServerThreadError(thread),
|
|
350
|
+
failureDetailReady: exactError,
|
|
312
351
|
provider,
|
|
313
352
|
status
|
|
314
353
|
});
|
|
@@ -556,11 +595,14 @@ export function createCodexTurnRecovery({
|
|
|
556
595
|
}
|
|
557
596
|
|
|
558
597
|
async function recoverCodexAppServerFinalizingTurn(sessionId = "", threadId = "", turnId = "", {
|
|
559
|
-
status = "completed"
|
|
598
|
+
status = "completed", timerRecovery = false
|
|
560
599
|
} = {}) {
|
|
561
600
|
const normalizedSessionId = normalizeCodexRunText(sessionId);
|
|
562
601
|
const normalizedThreadId = normalizeCodexRunText(threadId);
|
|
563
602
|
const normalizedTurnId = normalizeCodexRunText(turnId);
|
|
603
|
+
const key = codexAppServerResultFinalizationKey(normalizedSessionId, normalizedThreadId, normalizedTurnId);
|
|
604
|
+
const priorTimer = retainFinalizingTimer ? codexAppServerFinalizingTimers.get(key) : null;
|
|
605
|
+
if (!timerRecovery && priorTimer?.finalizingRecovery) return priorTimer.finalizingRecovery;
|
|
564
606
|
const runtime = await createRuntime();
|
|
565
607
|
const session = await runtime.getSession(normalizedSessionId);
|
|
566
608
|
const turn = turnState(session);
|
|
@@ -588,11 +630,35 @@ export function createCodexTurnRecovery({
|
|
|
588
630
|
if (result?.processed) {
|
|
589
631
|
return result;
|
|
590
632
|
}
|
|
591
|
-
|
|
633
|
+
// A later failure owns the same first deadline, even when this successful
|
|
634
|
+
// history read was already in flight under the completion-based clock.
|
|
635
|
+
const currentTimer = codexAppServerFinalizingTimers.get(key);
|
|
636
|
+
if (currentTimer?.failureDetail) return result;
|
|
637
|
+
if (retainFinalizingTimer && priorTimer && !currentTimer) return result;
|
|
638
|
+
let graceTurn = turn;
|
|
639
|
+
if (finalizingGraceAfterHistoryRead) {
|
|
640
|
+
const readReturnedAt = new Date().toISOString();
|
|
641
|
+
const currentTurn = turnState(await runtime.getSession(normalizedSessionId));
|
|
642
|
+
const timer = codexAppServerFinalizingTimers.get(key);
|
|
643
|
+
if (timer?.failureDetail || (priorTimer && !timer)) return result;
|
|
644
|
+
if (currentTurn.state !== "finalizing" || currentTurn.threadId !== normalizedThreadId ||
|
|
645
|
+
currentTurn.turnId !== normalizedTurnId) {
|
|
646
|
+
clearCodexAppServerFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
|
|
647
|
+
return { ...result, reason: "stale_turn_state" };
|
|
648
|
+
}
|
|
649
|
+
if (codexAppServerTurnOwnsActiveGoal(currentTurn, normalizedThreadId)) {
|
|
650
|
+
clearCodexAppServerFinalizingTimer(normalizedSessionId, normalizedThreadId, normalizedTurnId);
|
|
651
|
+
return { ok: true, processed: false, reason: "goal_continuation_pending" };
|
|
652
|
+
}
|
|
653
|
+
if (!timerRecovery && timer?.finalizingRecovery) return timer.finalizingRecovery;
|
|
654
|
+
// This local clock never changes the native completion timestamp.
|
|
655
|
+
graceTurn = { ...currentTurn, completedAt: timer?.finalizingStartedAt || readReturnedAt };
|
|
656
|
+
}
|
|
657
|
+
if (!codexAppServerFinalizingExpired(graceTurn)) {
|
|
592
658
|
scheduleCodexAppServerFinalizingRecovery(normalizedSessionId, normalizedThreadId, normalizedTurnId, {
|
|
593
|
-
completedAt:
|
|
659
|
+
completedAt: graceTurn.completedAt,
|
|
594
660
|
status,
|
|
595
|
-
updatedAt:
|
|
661
|
+
updatedAt: graceTurn.updatedAt
|
|
596
662
|
});
|
|
597
663
|
return result;
|
|
598
664
|
}
|
|
@@ -611,7 +677,8 @@ export function createCodexTurnRecovery({
|
|
|
611
677
|
async function recoverCodexAppServerActiveTurn(sessionId = "", {
|
|
612
678
|
provider = null,
|
|
613
679
|
retryOnError = true,
|
|
614
|
-
runtime: suppliedRuntime = null
|
|
680
|
+
runtime: suppliedRuntime = null,
|
|
681
|
+
providerError = null
|
|
615
682
|
} = {}) {
|
|
616
683
|
const normalizedSessionId = normalizeCodexRunText(sessionId);
|
|
617
684
|
if (!normalizedSessionId) {
|
|
@@ -639,7 +706,8 @@ export function createCodexTurnRecovery({
|
|
|
639
706
|
}
|
|
640
707
|
const reconciledSession = await reconcileCodexAppServerActiveTurn(session, {
|
|
641
708
|
provider,
|
|
642
|
-
runtime
|
|
709
|
+
runtime,
|
|
710
|
+
providerError
|
|
643
711
|
});
|
|
644
712
|
const currentTurn = turnState(reconciledSession);
|
|
645
713
|
if (currentTurn.state === "starting" && currentTurn.threadId) {
|
|
@@ -831,8 +899,11 @@ export function createCodexTurnRecovery({
|
|
|
831
899
|
provider = null,
|
|
832
900
|
status = "failed",
|
|
833
901
|
usageLimitExceeded = false,
|
|
834
|
-
verifyInactive = true
|
|
902
|
+
verifyInactive = true,
|
|
903
|
+
deferFailureDetails = false,
|
|
904
|
+
failureDetailReady = false
|
|
835
905
|
} = {}) {
|
|
906
|
+
const observedAt = Date.now();
|
|
836
907
|
const normalizedSessionId = normalizeCodexRunText(sessionId);
|
|
837
908
|
const normalizedThreadId = normalizeCodexRunText(threadId);
|
|
838
909
|
const normalizedTurnId = normalizeCodexRunText(turnId);
|
|
@@ -867,6 +938,21 @@ export function createCodexTurnRecovery({
|
|
|
867
938
|
reason: "stale_turn_state"
|
|
868
939
|
};
|
|
869
940
|
}
|
|
941
|
+
const key = codexAppServerResultFinalizationKey(normalizedSessionId, normalizedThreadId, normalizedTurnId);
|
|
942
|
+
const pendingTimer = codexAppServerFinalizingTimers.get(key);
|
|
943
|
+
const pendingDetail = pendingTimer?.failureDetail;
|
|
944
|
+
if (failureDetailGraceMs > 0 && !failureDetailReady && (deferFailureDetails || pendingDetail) &&
|
|
945
|
+
![turnOutcomes.USER_CANCELLED, turnOutcomes.CONTROL_RECONFIGURATION].includes(resolvedOutcome)) {
|
|
946
|
+
if (!pendingDetail) {
|
|
947
|
+
scheduleCodexAppServerFinalizingRecovery(normalizedSessionId, normalizedThreadId, normalizedTurnId, {
|
|
948
|
+
failureDetail: { error, ok, outcome: resolvedOutcome, provider, status: normalizedStatus,
|
|
949
|
+
usageLimitExceeded, verifyInactive,
|
|
950
|
+
expiresAt: Math.min(observedAt + failureDetailGraceMs,
|
|
951
|
+
pendingTimer?.finalizingExpiresAt ?? Infinity) }
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
return { ok: true, processed: false, reason: "failure_detail_pending", status: normalizedStatus };
|
|
955
|
+
}
|
|
870
956
|
const recovered = await recoverCodexAppServerFinalResponseBeforeOutcome(
|
|
871
957
|
normalizedSessionId,
|
|
872
958
|
normalizedThreadId,
|
|
@@ -142,6 +142,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
|
|
|
142
142
|
await transcript.writeUserMessage({
|
|
143
143
|
text: pending.displayMessage, messageId: pending.messageId,
|
|
144
144
|
attachments: pending.displayAttachments,
|
|
145
|
+
...(pending.data !== undefined ? { data: pending.data } : {}),
|
|
145
146
|
turnMetadata: { ...pending.turnMetadata, ...turnMetadata }
|
|
146
147
|
});
|
|
147
148
|
await markDelivered(pending.seen);
|
|
@@ -198,6 +199,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
|
|
|
198
199
|
displayMessage: String(input.displayMessage || input.message || ""),
|
|
199
200
|
displayAttachments: input.displayAttachments || input.attachments || [],
|
|
200
201
|
attachmentIds: input.attachmentIds || [],
|
|
202
|
+
...(input.data !== undefined ? { data: structuredClone(input.data) } : {}),
|
|
201
203
|
seen: snapshot, attempted: false,
|
|
202
204
|
...(captureContext ? {
|
|
203
205
|
contextText: preamble.slice(0, preamble.length - String(input.message || "").length - "\n\nUser's message:\n".length),
|
|
@@ -213,6 +215,7 @@ export function createConversationChangeover({ state, transcript, agent, identit
|
|
|
213
215
|
const delivered = await agent.sendMessage(pending ? {
|
|
214
216
|
...input, message: pending.message, displayMessage: pending.displayMessage,
|
|
215
217
|
displayAttachments: pending.displayAttachments, attachmentIds: pending.attachmentIds,
|
|
218
|
+
data: pending.data,
|
|
216
219
|
...(captureContext ? { contextText: pending.contextText, contextAttachments: pending.contextAttachments } : {}),
|
|
217
220
|
onPromptSending: async ({ threadId, displayAttachments, turnMetadata }) => {
|
|
218
221
|
const replacement = value.replacement;
|
|
@@ -921,6 +921,7 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
921
921
|
if (message.summary === true) continue;
|
|
922
922
|
const assistantText = assistantMessageText(message);
|
|
923
923
|
const messageId = assistantText ? projection.messageId(message.id, "assistant") : "";
|
|
924
|
+
const outputId = assistantText ? projection.outputId?.(message.id, "assistant") : "";
|
|
924
925
|
const inFlight = streaming && index === rows.length - 1;
|
|
925
926
|
const reasoning = (message.content || []).filter((part) => part.type === "reasoning" && text(part.text))
|
|
926
927
|
.map((part) => ({
|
|
@@ -939,6 +940,7 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
939
940
|
// superseded narration never evaporates from the transcript.
|
|
940
941
|
const turn = await projection.store.writeConversationAssistantMessage(sessionId, {
|
|
941
942
|
messageId,
|
|
943
|
+
...(outputId ? { outputId } : {}),
|
|
942
944
|
text: assistantText
|
|
943
945
|
});
|
|
944
946
|
projection.store.completeConversationStreamMessage(sessionId, messageId);
|
|
@@ -949,6 +951,7 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
949
951
|
const conversationStream = projection.store.updateConversationStream(sessionId, {
|
|
950
952
|
turnId: inputMessageId,
|
|
951
953
|
messageId,
|
|
954
|
+
...(outputId ? { outputId } : {}),
|
|
952
955
|
text: assistantText
|
|
953
956
|
});
|
|
954
957
|
if (conversationStream) {
|
|
@@ -959,6 +962,7 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope } = {}) {
|
|
|
959
962
|
if (assistantText && !streaming) {
|
|
960
963
|
const turn = await projection.store.writeConversationAssistantMessage(sessionId, {
|
|
961
964
|
messageId,
|
|
965
|
+
...(outputId ? { outputId } : {}),
|
|
962
966
|
text: assistantText
|
|
963
967
|
});
|
|
964
968
|
projection.store.completeConversationStreamMessage(sessionId, messageId);
|
|
@@ -3,7 +3,8 @@ import { homedir, tmpdir } from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { open, realpath } from "node:fs/promises";
|
|
5
5
|
import { createCodexAppServerRuntime } from "../codexProcess.js";
|
|
6
|
-
import { CodexAppServerAgentProvider, codexLocalImageInput, ensureCodexAppServerThread,
|
|
6
|
+
import { CodexAppServerAgentProvider, codexLocalImageInput, ensureCodexAppServerThread, codexApplicationToolConfiguration,
|
|
7
|
+
assertCodexApplicationToolSchemaIdentity, inspectCodexAppServerMessageAdmission, retireCodexConversationHistory } from "../codexProvider.js";
|
|
7
8
|
import { createCodexAppServerProviderOwner } from "../codexProviderOwner.js";
|
|
8
9
|
import { codexAuthOutputRequiresReconnect, codexProviderConfiguration, codexToolFreeConfiguration } from "../codexConfiguration.js";
|
|
9
10
|
import { nativeAiProvider, nativeAiModel } from "../../../shared/nativeProviders.js";
|
|
@@ -46,6 +47,8 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
46
47
|
}
|
|
47
48
|
const nativeTools = host.nativeTools === true;
|
|
48
49
|
const maximumOutput = limits.maxOutputCharacters ?? 64_000;
|
|
50
|
+
const maximumFinalReply = limits.maxFinalReplyCharacters ?? maximumOutput;
|
|
51
|
+
if (!Number.isSafeInteger(maximumFinalReply) || maximumFinalReply < 1) throw new TypeError("Invalid Codex final reply limit.");
|
|
49
52
|
if (!Number.isSafeInteger(maximumOutput) || maximumOutput < 1) throw new TypeError("Invalid Codex output limit.");
|
|
50
53
|
|
|
51
54
|
async function accountRecord() {
|
|
@@ -276,7 +279,8 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
276
279
|
const sessionId = conversation.sessionId;
|
|
277
280
|
const nativeStore = conversation.runtime.store;
|
|
278
281
|
const boundedWriter = name => (id, input) => {
|
|
279
|
-
const limit = name === "writeConversationThinkingMessage" ? 4 * 1024 * 1024
|
|
282
|
+
const limit = name === "writeConversationThinkingMessage" ? 4 * 1024 * 1024
|
|
283
|
+
: name.includes("AssistantMessage") ? Math.min(maximumOutput, maximumFinalReply) : maximumOutput;
|
|
280
284
|
if ((input.text?.length || 0) > limit) throw new Error("Codex output exceeded the configured limit.");
|
|
281
285
|
return nativeStore[name](id, input);
|
|
282
286
|
};
|
|
@@ -299,12 +303,33 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
299
303
|
}
|
|
300
304
|
} };
|
|
301
305
|
const owner = supplied?.runOwner || createCodexAppServerRunOwner({
|
|
306
|
+
finalizingGraceMs: limits.codexFinalizingGraceMs,
|
|
307
|
+
finalizingGraceAfterHistoryRead: limits.codexFinalizingGraceAfterHistoryRead,
|
|
308
|
+
failureDetailGraceMs: limits.codexFailureDetailGraceMs,
|
|
302
309
|
createRuntime: async () => runtime,
|
|
303
310
|
createStore: async () => runtime.store,
|
|
304
311
|
acquireProvider: async () => native,
|
|
305
312
|
hasRuntime: () => Boolean(binding.threadId),
|
|
306
313
|
publish: (_id, event) => publishNative(event),
|
|
307
|
-
checkpoint: (_id, input) => conversation.checkpoint(input
|
|
314
|
+
checkpoint: (_id, input) => conversation.checkpoint({ ...input,
|
|
315
|
+
status: input.turnOutcome === "response_delivery_failure" ? "failed" : input.status }),
|
|
316
|
+
async onNotificationSignal(kind, { threadId, turnId, provider, error }) {
|
|
317
|
+
const active = current;
|
|
318
|
+
if (kind !== "provider_error" || !active?.dispatched || active.finished || provider !== native ||
|
|
319
|
+
threadId !== binding.threadId || !turnId || active.turnId && active.turnId !== turnId) return;
|
|
320
|
+
const tracked = codexAppServerTurnState(await runtime.getSession(sessionId));
|
|
321
|
+
if (tracked.threadId !== threadId || tracked.turnId !== turnId) return;
|
|
322
|
+
// The original detached waiter recovered only this admitted turn.
|
|
323
|
+
// Keep native activity truthful; run's existing cleanup owns Stop.
|
|
324
|
+
await owner.recoverActiveTurn(sessionId, { provider, retryOnError: false, providerError: { threadId, turnId, error } });
|
|
325
|
+
if (current !== active || active.finished || active.turnId !== turnId ||
|
|
326
|
+
provider !== native || threadId !== binding.threadId) return;
|
|
327
|
+
const result = await owner.stopTurnWithProviderFailure(sessionId, threadId, turnId, { provider, error, failureDetailReady: true });
|
|
328
|
+
if (current === active && active.turnId === turnId && provider === native &&
|
|
329
|
+
threadId === binding.threadId && result.reason === "provider_still_active") {
|
|
330
|
+
active.completion.reject(Object.assign(new Error(error), { status: "failed" }));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
308
333
|
});
|
|
309
334
|
async function publishNative(event) {
|
|
310
335
|
if (supplied) binding.threadId = await conversation.identity.read();
|
|
@@ -330,8 +355,9 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
330
355
|
}
|
|
331
356
|
if (!turn.active && event.reason === "codex-app-server-turn-idle") {
|
|
332
357
|
current.finished = true;
|
|
333
|
-
if (["failed", "interrupted"].includes(turn.status) && !current.signal.aborted) {
|
|
334
|
-
current.completion.reject(Object.assign(new Error(turn.error || `Codex turn ${turn.status}.`), {
|
|
358
|
+
if ((["failed", "interrupted"].includes(turn.status) || !supplied && turn.error) && !current.signal.aborted) {
|
|
359
|
+
current.completion.reject(Object.assign(new Error(turn.error || `Codex turn ${turn.status}.`), {
|
|
360
|
+
status: turn.status === "completed" ? "failed" : turn.status }));
|
|
335
361
|
} else current.completion.resolve();
|
|
336
362
|
}
|
|
337
363
|
}
|
|
@@ -548,13 +574,8 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
548
574
|
}
|
|
549
575
|
|
|
550
576
|
async function prepareConfiguration(configuration, context, signal, tools) {
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
}));
|
|
554
|
-
const toolSchemaIdentity = hash(dynamicTools);
|
|
555
|
-
if (binding.threadId && (binding.toolSchemaIdentity || hash([])) !== toolSchemaIdentity) {
|
|
556
|
-
throw new Error("This Codex conversation was created with different application tool entry points. Start a new conversation to enable or remove application tools.");
|
|
557
|
-
}
|
|
577
|
+
const { dynamicTools, toolSchemaIdentity } = codexApplicationToolConfiguration(tools);
|
|
578
|
+
assertCodexApplicationToolSchemaIdentity(binding.threadId, binding.toolSchemaIdentity, toolSchemaIdentity);
|
|
558
579
|
const selected = await selection(configuration, context, signal);
|
|
559
580
|
await connect(signal, selected);
|
|
560
581
|
const [configResult, hookResult] = await Promise.all([
|
|
@@ -648,7 +669,8 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
648
669
|
}
|
|
649
670
|
|
|
650
671
|
function messagePreparation(configuration, context, signal, tools) {
|
|
651
|
-
if (supplied) return supplied.messagePreparation
|
|
672
|
+
if (supplied) return tools ? { ...supplied.messagePreparation, applicationTools: tools,
|
|
673
|
+
providerReady: current.providerReady } : supplied.messagePreparation;
|
|
652
674
|
return {
|
|
653
675
|
async readContext() {
|
|
654
676
|
const session = await runtime.getSession(sessionId);
|
|
@@ -912,7 +934,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
912
934
|
const request = JSON.parse(await runtime.store.readMetadataValue(sessionId, "assistant_delivery")).engines.codex.pending;
|
|
913
935
|
if (request?.messageId === messageId) await owner.writeDeliveredUserMessage(runtime, sessionId,
|
|
914
936
|
request.displayMessage, messageId, request.turnMetadata, request.displayAttachments,
|
|
915
|
-
{ threadId: binding.threadId, turnId: acceptedTurn?.id || "" });
|
|
937
|
+
{ threadId: binding.threadId, turnId: acceptedTurn?.id || "" }, request.data);
|
|
916
938
|
let recoveryLimitation;
|
|
917
939
|
if (!stored.agentRuns.length) {
|
|
918
940
|
recoveryLimitation = "Delivery is confirmed, but this older binding has no original native run record. Stored history is preserved; output recovery cannot be proven.";
|
|
@@ -956,6 +978,72 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
956
978
|
let waitForTurn = false;
|
|
957
979
|
let executingProvider;
|
|
958
980
|
const toolCalls = new Set();
|
|
981
|
+
const toolRegistrations = new Map();
|
|
982
|
+
function toolHandler(provider, threadId, isRegistered = () => true) {
|
|
983
|
+
return async ({ method, params }) => {
|
|
984
|
+
if (method !== "item/tool/call" || !tools || params?.threadId !== (threadId || binding.threadId) || params.namespace ||
|
|
985
|
+
supplied && (disposed || current !== active || active.finished || signal.aborted || owner.runtimeLifecycle?.closing === true || !isRegistered())) {
|
|
986
|
+
throw new Error("This conversation does not authorize that native tool request.");
|
|
987
|
+
}
|
|
988
|
+
let owned = !supplied;
|
|
989
|
+
const operation = (async () => {
|
|
990
|
+
await active.inputReady;
|
|
991
|
+
if (supplied && owner.runtimeLifecycle?.closing === true) {
|
|
992
|
+
throw new Error("This conversation does not authorize that native tool request.");
|
|
993
|
+
}
|
|
994
|
+
const ownership = Promise.withResolvers();
|
|
995
|
+
owner.notificationQueue.run({ sessionId, provider }, () => {
|
|
996
|
+
if (supplied && (params.turnId !== active.turnId || disposed || current !== active || active.finished ||
|
|
997
|
+
signal.aborted || !isRegistered())) {
|
|
998
|
+
ownership.resolve(false);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
owned = true;
|
|
1002
|
+
const failure = provider.observationFailure;
|
|
1003
|
+
if (failure) ownership.reject(failure.cause || failure);
|
|
1004
|
+
else ownership.resolve(params.turnId === active.turnId);
|
|
1005
|
+
});
|
|
1006
|
+
if (!await ownership.promise || supplied && (disposed || current !== active || active.finished || signal.aborted || !isRegistered())) {
|
|
1007
|
+
owned = false;
|
|
1008
|
+
throw new Error("This application tool call belongs to another turn.");
|
|
1009
|
+
}
|
|
1010
|
+
const result = await tools.execute({ id: params.callId, name: params.tool, arguments: JSON.stringify(params.arguments) });
|
|
1011
|
+
return { success: result.ok, contentItems: [{ type: "inputText", text: JSON.stringify(result) }] };
|
|
1012
|
+
})();
|
|
1013
|
+
toolCalls.add(operation);
|
|
1014
|
+
try { return await operation; }
|
|
1015
|
+
catch (error) {
|
|
1016
|
+
if (!supplied || owned && current === active && params.turnId === active.turnId && isRegistered()) {
|
|
1017
|
+
if (supplied) active.toolFailure = error;
|
|
1018
|
+
active.completion.reject(error);
|
|
1019
|
+
}
|
|
1020
|
+
throw error;
|
|
1021
|
+
}
|
|
1022
|
+
finally { toolCalls.delete(operation); }
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
active.providerReady = async ({ provider, threadId }) => {
|
|
1026
|
+
signal.throwIfAborted();
|
|
1027
|
+
if (!threadId) return;
|
|
1028
|
+
if (typeof conversation.identity?.readToolSchemaIdentity !== "function") {
|
|
1029
|
+
throw new TypeError("Bound Codex application tools require the original schema identity reader and writer.");
|
|
1030
|
+
}
|
|
1031
|
+
if (await conversation.identity.read() !== threadId) throw new Error("The native application tool thread changed before dispatch.");
|
|
1032
|
+
assertCodexApplicationToolSchemaIdentity(threadId, await conversation.identity.readToolSchemaIdentity(),
|
|
1033
|
+
codexApplicationToolConfiguration(tools).toolSchemaIdentity);
|
|
1034
|
+
signal.throwIfAborted();
|
|
1035
|
+
if (disposed || current !== active) throw new Error("This conversation handle is closed.");
|
|
1036
|
+
for (const [previous, owned] of toolRegistrations) {
|
|
1037
|
+
if (previous === provider && owned.threadId === threadId && owned.client === provider.client && owned.registration.isCurrent()) return;
|
|
1038
|
+
owned.registration.release();
|
|
1039
|
+
toolRegistrations.delete(previous);
|
|
1040
|
+
}
|
|
1041
|
+
const client = provider.client;
|
|
1042
|
+
let registration;
|
|
1043
|
+
registration = provider.registerThreadRequestHandler(threadId, toolHandler(provider, threadId,
|
|
1044
|
+
() => registration?.isCurrent() === true && provider.client === client));
|
|
1045
|
+
toolRegistrations.set(provider, { threadId, client, registration });
|
|
1046
|
+
};
|
|
959
1047
|
function abort() {
|
|
960
1048
|
if (cancellation) return;
|
|
961
1049
|
cancellation = interrupt();
|
|
@@ -981,27 +1069,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
981
1069
|
await connect(signal, selected);
|
|
982
1070
|
executingProvider = native;
|
|
983
1071
|
}
|
|
984
|
-
if (!supplied) native.setServerRequestHandler(
|
|
985
|
-
if (method !== "item/tool/call" || !tools || params?.threadId !== binding.threadId || params.namespace) {
|
|
986
|
-
throw new Error("This conversation does not authorize that native tool request.");
|
|
987
|
-
}
|
|
988
|
-
const operation = (async () => {
|
|
989
|
-
await active.inputReady;
|
|
990
|
-
const ownership = Promise.withResolvers();
|
|
991
|
-
owner.notificationQueue.run({ sessionId, provider: executingProvider }, () => {
|
|
992
|
-
const failure = executingProvider.observationFailure;
|
|
993
|
-
if (failure) ownership.reject(failure.cause || failure);
|
|
994
|
-
else ownership.resolve(params.turnId === active.turnId);
|
|
995
|
-
});
|
|
996
|
-
if (!await ownership.promise) throw new Error("This application tool call belongs to another turn.");
|
|
997
|
-
const result = await tools.execute({ id: params.callId, name: params.tool, arguments: JSON.stringify(params.arguments) });
|
|
998
|
-
return { success: result.ok, contentItems: [{ type: "inputText", text: JSON.stringify(result) }] };
|
|
999
|
-
})();
|
|
1000
|
-
toolCalls.add(operation);
|
|
1001
|
-
try { return await operation; }
|
|
1002
|
-
catch (error) { active.completion.reject(error); throw error; }
|
|
1003
|
-
finally { toolCalls.delete(operation); }
|
|
1004
|
-
});
|
|
1072
|
+
if (!supplied) native.setServerRequestHandler(toolHandler(executingProvider));
|
|
1005
1073
|
const command = { input, beforeDispatch, accept };
|
|
1006
1074
|
delivered = await sendMessage(command, configuration, context, signal, tools);
|
|
1007
1075
|
waitForTurn = !supplied || delivered.value?.delivered === true;
|
|
@@ -1032,7 +1100,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
1032
1100
|
try {
|
|
1033
1101
|
// Account invalidation already owns this stop attempt. Its failed
|
|
1034
1102
|
// proof stays available for explicit cleanup through the same owner.
|
|
1035
|
-
if ((!supplied && problem || signal.aborted) && problem?.code !== "codex_runtime_invalidated") {
|
|
1103
|
+
if ((!supplied && problem || supplied && active.toolFailure || signal.aborted) && problem?.code !== "codex_runtime_invalidated") {
|
|
1036
1104
|
await (cancellation || interrupt());
|
|
1037
1105
|
}
|
|
1038
1106
|
} catch (error) { problem = error; }
|
|
@@ -1043,6 +1111,8 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
|
|
|
1043
1111
|
if (!supplied) await providerOwner.pendingRecovery(providerKey, executingProvider);
|
|
1044
1112
|
} catch (error) { problem = error; }
|
|
1045
1113
|
finally {
|
|
1114
|
+
for (const owned of toolRegistrations.values()) owned.registration.release();
|
|
1115
|
+
toolRegistrations.clear();
|
|
1046
1116
|
await Promise.allSettled([...toolCalls]);
|
|
1047
1117
|
if (!supplied) {
|
|
1048
1118
|
native?.setServerRequestHandler(null);
|