@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.
@@ -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 = codexAppServerFinalizingRemainingMs({
119
- completedAt,
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
- codexAppServerFinalizingTimers.delete(key);
125
- void runInContext(
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
- if (!codexAppServerFinalizingExpired(turn)) {
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: turn.completedAt,
659
+ completedAt: graceTurn.completedAt,
594
660
  status,
595
- updatedAt: turn.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, inspectCodexAppServerMessageAdmission, retireCodexConversationHistory } from "../codexProvider.js";
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 : maximumOutput;
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}.`), { status: 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 dynamicTools = (tools?.schemas || []).map(({ function: tool }) => ({
552
- type: "function", name: tool.name, description: tool.description, inputSchema: tool.parameters
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(async ({ method, params }) => {
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);