@jskit-ai/assistant-core 0.1.240 → 0.1.242

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.
Files changed (101) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +5 -5
  3. package/src/client/conversation/AssistantTranscript.vue +26 -29
  4. package/src/client/conversation/LongTextPreviewBlocks.vue +11 -0
  5. package/src/client/conversation/messageDelivery.js +8 -2
  6. package/src/client/conversation/useAssistantAttachments.js +1 -1
  7. package/src/server/conversation/agentRun.js +12 -198
  8. package/src/server/conversation/attachments.js +6 -5
  9. package/src/server/conversation/boundedToolLoop.js +160 -0
  10. package/src/server/conversation/claudeHistory.js +37 -23
  11. package/src/server/conversation/claudeProcess.js +2 -2
  12. package/src/server/conversation/claudeTurn.js +41 -38
  13. package/src/server/conversation/codexAuthPreflight.js +104 -0
  14. package/src/server/conversation/codexClient.js +11 -16
  15. package/src/server/conversation/codexConfiguration.js +112 -98
  16. package/src/server/conversation/codexConversationStore.js +189 -0
  17. package/src/server/conversation/codexConversations.js +47 -44
  18. package/src/server/conversation/codexDetachedTurn.js +12 -11
  19. package/src/server/conversation/codexEvents.js +95 -99
  20. package/src/server/conversation/codexHelperExecution.js +3 -13
  21. package/src/server/conversation/codexHelperThreadLedger.js +7 -13
  22. package/src/server/conversation/codexHelperThreadRecovery.js +2 -16
  23. package/src/server/conversation/codexHelperThreads.js +5 -6
  24. package/src/server/conversation/codexHistoryAdapter.js +32 -26
  25. package/src/server/conversation/codexMessageCommands.js +22 -22
  26. package/src/server/conversation/codexMessageDelivery.js +12 -7
  27. package/src/server/conversation/codexModelCatalog.js +97 -0
  28. package/src/server/conversation/codexNotificationObserver.js +24 -40
  29. package/src/server/conversation/codexNotificationQueue.js +2 -3
  30. package/src/server/conversation/codexOutput.js +20 -27
  31. package/src/server/conversation/codexProcess.js +123 -127
  32. package/src/server/conversation/codexProcessLeader.js +3 -3
  33. package/src/server/conversation/codexProvider.js +31 -614
  34. package/src/server/conversation/codexProviderOwner.js +2 -2
  35. package/src/server/conversation/codexProviderSelection.js +38 -40
  36. package/src/server/conversation/codexRenewalCommands.js +13 -10
  37. package/src/server/conversation/codexRenewalThreads.js +412 -0
  38. package/src/server/conversation/codexRunJournal.js +24 -42
  39. package/src/server/conversation/codexRuntimeLifecycle.js +19 -15
  40. package/src/server/conversation/codexSessionCleanup.js +11 -10
  41. package/src/server/conversation/codexSessionReconciliation.js +6 -6
  42. package/src/server/conversation/codexThreadReadiness.js +19 -33
  43. package/src/server/conversation/codexThreadStatus.js +3 -3
  44. package/src/server/conversation/codexTurn.js +124 -297
  45. package/src/server/conversation/codexTurnRecovery.js +49 -28
  46. package/src/server/conversation/codexTurnState.js +34 -13
  47. package/src/server/conversation/commandWrapper.js +3 -0
  48. package/src/server/conversation/deliveryTiming.js +14 -5
  49. package/src/server/conversation/index.js +1 -0
  50. package/src/server/conversation/localExecution.js +13 -17
  51. package/src/server/conversation/normalize.js +8 -3
  52. package/src/server/conversation/openCodeClient.js +9 -10
  53. package/src/server/conversation/openCodeProcess.js +9 -5
  54. package/src/server/conversation/openCodeRuntime.js +10 -11
  55. package/src/server/conversation/openCodeTurn.js +43 -29
  56. package/src/server/conversation/providers/claudeDriver.js +8 -16
  57. package/src/server/conversation/providers/codexDriver.js +25 -41
  58. package/src/server/conversation/providers/opencodeDriver.js +17 -42
  59. package/src/server/conversation/providers/scoped.js +48 -4
  60. package/src/server/conversation/runtime.js +50 -49
  61. package/src/server/conversation/runtimeStateUpgrade.js +34 -22
  62. package/src/server/conversation/transcript.js +10 -10
  63. package/src/server/hosts/codexConfiguration.js +1 -0
  64. package/src/server/hosts/codexProvider.js +8 -6
  65. package/src/server/index.js +2 -1
  66. package/src/server/lib/assistantToolLoop.js +1 -153
  67. package/src/server/lib/serviceToolCatalog.js +17 -42
  68. package/src/shared/assistantResource.js +4 -2
  69. package/src/shared/conversation/attachments.js +4 -0
  70. package/src/shared/conversation/longTextBlocks.js +54 -19
  71. package/src/shared/conversation/numberedQuestionSugar.js +17 -10
  72. package/src/shared/conversation/pagination.js +4 -6
  73. package/src/shared/conversation/patches.js +1 -4
  74. package/src/shared/conversation/streaming.js +3 -11
  75. package/src/shared/conversation/turns.js +11 -0
  76. package/src/shared/index.js +1 -0
  77. package/test/assistantBoundedToolLoop.test.js +2 -2
  78. package/test/assistantResource.test.js +4 -4
  79. package/test/assistantSettingsResource.test.js +2 -2
  80. package/test/claudeConversationSession.test.js +11 -1
  81. package/test/claudeTurn.test.js +35 -7
  82. package/test/codexClient.test.js +20 -0
  83. package/test/codexConversationSession.test.js +2 -2
  84. package/test/codexConversationsCheckpoint.test.js +51 -0
  85. package/test/codexModelCatalog.test.js +1 -1
  86. package/test/codexProviderHome.test.js +51 -0
  87. package/test/codexProviderSelection.test.js +71 -0
  88. package/test/codexTurn.test.js +222 -6
  89. package/test/conversationAgentRun.test.js +2 -1
  90. package/test/conversationAttachments.test.js +27 -2
  91. package/test/conversationDeliveryTiming.test.js +21 -0
  92. package/test/conversationNormalize.test.js +11 -0
  93. package/test/fixtures/nestedListQuestionMessage.md +37 -0
  94. package/test/frameworkActions.test.js +2 -2
  95. package/test/longTextBlocks.test.js +102 -0
  96. package/test/messageDelivery.test.js +7 -1
  97. package/test/openCodeClient.test.js +20 -0
  98. package/test/openCodeRuntime.test.js +1 -1
  99. package/test/openCodeTurn.test.js +90 -0
  100. package/test/opencodeConversationSession.test.js +1 -1
  101. package/test/serviceToolCatalog.test.js +27 -0
@@ -1,9 +1,9 @@
1
- import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, normalizeCodexRunText, codexAppServerErrorMessage, codexAppServerSteerFailure, codexAppServerInterruptFailure, codexAppServerConversationTurnIsActive } from "./codexTurnState.js";
1
+ import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, normalizeCodexRunText, codexAppServerErrorMessage, codexAppServerSteerFailure, codexAppServerInterruptFailure, codexAppServerConversationTurnIsActive, codexAppServerNativeOuterTurnId } from "./codexTurnState.js";
2
2
  import { CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS, createCodexAppServerDetachedTurnWatcher } from "./codexDetachedTurn.js";
3
3
  import { assertCodexAppServerHelperAccountIdentity, readCodexAppServerAccountIdentity, sendPreparedCodexAppServerHelperTurn } from "./codexHelperExecution.js";
4
- import { codexAppServerRequestIsInvalid, codexAppServerThreadHasReadableHistory, codexAppServerThreadIsMissing, codexLocalImageInput, sendCodexAppServerPrompt } from "./codexProvider.js";
4
+ import { codexAppServerDeliveredTurn, codexAppServerRequestIsInvalid, codexAppServerThreadHasReadableHistory, codexAppServerThreadIsMissing, codexLocalImageInput, sendCodexAppServerPrompt } from "./codexProvider.js";
5
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";
6
+ import { codexAppServerAssistantItemText, codexAppServerContentText, classifyCodexAppServerEvent, codexAppServerNotificationThreadId, codexAppServerNotificationTurnId, codexAppServerNotificationTurnStatus, codexAppServerThreadRawValue, codexAppServerProviderThreadAssistantSegments, codexAppServerProviderTurnId, codexAppServerProviderTurnStatus, codexAppServerProviderTurnItems, codexAppServerProviderTurnClientIds, codexAppServerThreadTurnId, codexAppServerThreadError } from "./codexEvents.js";
7
7
 
8
8
  // Internal command facet of the original run owner; the same native conversation
9
9
  // and turn-start collections retain their original lifetime and public references.
@@ -19,7 +19,10 @@ export function createCodexConversationCommands({
19
19
  interruptFailedCode,
20
20
  namespace,
21
21
  providerSessions,
22
- steerFailedCode
22
+ sessionConnection,
23
+ ensureSessionProvider,
24
+ steerFailedCode,
25
+ turnOutcomes
23
26
  }) {
24
27
  const codexAppServerConversations = new Map();
25
28
  const codexAppServerConversationTurnStarts = new Map();
@@ -68,7 +71,7 @@ export function createCodexConversationCommands({
68
71
  let state = conversations.get(conversationId);
69
72
  if (state?.provider === provider && state.unsubscribe) return state;
70
73
  state?.unsubscribe?.();
71
- state = { conversationId, provider, persistent: true, status: "ready", runId: "", error: "" };
74
+ state = { conversationId, provider, persistent: true, status: "ready", runId: "", error: "", turnOutcome: "" };
72
75
  state.unsubscribe = provider.subscribe((notification) => {
73
76
  if (codexAppServerNotificationThreadId(notification) !== conversationId) return;
74
77
  const turnId = codexAppServerNotificationTurnId(notification);
@@ -91,11 +94,15 @@ export function createCodexConversationCommands({
91
94
  state.onEvent = null;
92
95
  const completedTurnId = state.runId;
93
96
  const completedStatus = state.status;
97
+ // A turn the person stopped reaches the checkpoint as cancelled, as Main turns do.
98
+ const completedOutcome = state.turnOutcome;
99
+ state.turnOutcome = "";
94
100
  void checkpoint(sessionId, {
95
- outerTurnId: `codex:${conversationId}:${completedTurnId}`,
101
+ outerTurnId: codexAppServerNativeOuterTurnId(conversationId, completedTurnId),
96
102
  threadId: conversationId,
97
103
  turnId: completedTurnId,
98
- status: completedStatus
104
+ status: completedStatus,
105
+ turnOutcome: completedOutcome
99
106
  }).catch(() => {});
100
107
  }
101
108
  });
@@ -229,9 +236,7 @@ export function createCodexConversationCommands({
229
236
  const { context } = prepared;
230
237
  if (context.assistantScope) {
231
238
  const providerOptions = prepared.providerOptions;
232
- const provider = await providerSessions.owner.ensureSession(
233
- providerSessions.context(normalizeCodexRunText(sessionId), providerOptions)
234
- );
239
+ const provider = await ensureSessionProvider(sessionId, providerOptions);
235
240
  const result = prepared.project(provider);
236
241
  result.isolationConfig = await readCodexToolFreeConfiguration(
237
242
  provider, context.workdir, conversationPreparation.isolation.assertCompatibility
@@ -251,21 +256,19 @@ export function createCodexConversationCommands({
251
256
  }
252
257
  }
253
258
  const managedIdentity = prepared.managedIdentity;
254
- const restoredAdmissionError = conversationPreparation.admissionError(sessionId);
255
- if (restoredAdmissionError) throw restoredAdmissionError;
259
+ const assertAdmitted = () => {
260
+ const error = conversationPreparation.admissionError(sessionId);
261
+ if (error) throw error;
262
+ };
263
+ assertAdmitted();
256
264
  const activeProvider = prepared.helperTurn
257
265
  ? null
258
266
  : await ensureCodexAppServerProviderForActiveTurn(context.session, managedIdentity);
259
- const activeProviderAdmissionError = conversationPreparation.admissionError(sessionId);
260
- if (activeProviderAdmissionError) throw activeProviderAdmissionError;
267
+ assertAdmitted();
261
268
  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
+ assertAdmitted();
270
+ const provider = activeProvider?.provider || await ensureSessionProvider(sessionId, providerOptions);
271
+ assertAdmitted();
269
272
  return prepared.project(provider, activeProvider?.providerOptions || providerOptions, helperRestore);
270
273
  }
271
274
 
@@ -369,11 +372,6 @@ export function createCodexConversationCommands({
369
372
  if (helperTurn) {
370
373
  try {
371
374
  await assertCodexAppServerHelperAccountIdentity(provider, input.expectedAccountIdentitySignature, helperOwnershipError);
372
- } catch (error) {
373
- await helper.discard();
374
- throw error;
375
- }
376
- try {
377
375
  execution.validateOutput(result.text);
378
376
  } catch (error) {
379
377
  await helper.discard();
@@ -516,7 +514,9 @@ export function createCodexConversationCommands({
516
514
  });
517
515
  }
518
516
 
519
- async function stopCodexAppServerConversation(sessionId, input = {}, options = {}) {
517
+ async function stopCodexAppServerConversation(sessionId, input = {}, options = {}, {
518
+ turnOutcome = turnOutcomes.USER_CANCELLED
519
+ } = {}) {
520
520
  const conversationId = normalizeCodexRunText(input.conversationId);
521
521
  const conversationState = codexAppServerConversation(sessionId, conversationId);
522
522
  if (conversationState?.provider.observationFailure) {
@@ -541,7 +541,8 @@ export function createCodexConversationCommands({
541
541
  return stopPersistentCodexConversation({
542
542
  conversationId,
543
543
  conversationState,
544
- provider: context.provider
544
+ provider: context.provider,
545
+ turnOutcome
545
546
  });
546
547
  }
547
548
  const result = await interruptDetachedCodexAppServerConversationTurn(sessionId, {
@@ -586,7 +587,7 @@ export function createCodexConversationCommands({
586
587
  if (context.ok === false) {
587
588
  return context;
588
589
  }
589
- const connection = providerSessions.context(normalizeCodexRunText(sessionId), context.providerOptions);
590
+ const connection = sessionConnection(sessionId, context.providerOptions);
590
591
  providerExit = await providerSessions.owner.stopProvider(connection.providerKey, context.providerOptions, {
591
592
  preserveProcessExitProof: false,
592
593
  requireStopped: true,
@@ -605,7 +606,9 @@ export function createCodexConversationCommands({
605
606
  const key = namespace(sessionId);
606
607
  for (const state of codexAppServerConversations.get(key)?.values() || []) {
607
608
  if (state.persistent) {
608
- await stopCodexAppServerConversation(sessionId, { conversationId: state.conversationId, persistent: true });
609
+ // Closing is not the person stopping the turn; its status alone decides the outcome.
610
+ await stopCodexAppServerConversation(sessionId, { conversationId: state.conversationId, persistent: true },
611
+ {}, { turnOutcome: "" });
609
612
  state.unsubscribe?.();
610
613
  } else {
611
614
  await deleteScopedCodexAppServerConversation(sessionId, { conversationId: state.conversationId }).catch(() => null);
@@ -771,9 +774,7 @@ export function createCodexConversationCommands({
771
774
 
772
775
  async function dispatchCodexAppServerDetachedTurn({ provider, prompt, threadId }, authorized) {
773
776
  const delivery = await sendCodexAppServerPrompt({ provider, prompt, threadId }, authorized);
774
- const turnId = normalizeCodexRunText(delivery.turn?.id);
775
- const status = normalizeCodexRunText(delivery.turn?.status || delivery.turn?.raw?.status);
776
- return { delivery, status, turnId };
777
+ return { delivery, ...codexAppServerDeliveredTurn(delivery) };
777
778
  }
778
779
 
779
780
  async function runDetachedCodexAppServerTurn({
@@ -819,12 +820,7 @@ export function createCodexConversationCommands({
819
820
  type: "turn"
820
821
  });
821
822
  if (codexAppServerTurnStatusIsProviderFailure(status)) {
822
- const providerError = codexAppServerErrorText(
823
- delivery.turn?.raw?.error ||
824
- delivery.turn?.response?.turn?.error ||
825
- delivery.turn?.error
826
- );
827
- const error = new Error(providerError || `Codex app-server turn ${status}.`);
823
+ const error = new Error(codexAppServerDeliveredTurn(delivery).error || `Codex app-server turn ${status}.`);
828
824
  await throwWatcherFailure(error, status, {
829
825
  waitForDetail: true
830
826
  });
@@ -915,7 +911,7 @@ export function createCodexConversationCommands({
915
911
  conversationState.watcher = watcher;
916
912
  }
917
913
  let delivery = null;
918
- if (input.persistent && conversationState) conversationState.onEvent = onEvent;
914
+ if (input.persistent && conversationState) Object.assign(conversationState, { onEvent, turnOutcome: "" });
919
915
  try {
920
916
  await input.onPromptSending?.({ threadId: conversationId });
921
917
  if (executionProfile) {
@@ -945,8 +941,7 @@ export function createCodexConversationCommands({
945
941
  }
946
942
  throw error;
947
943
  }
948
- const runId = normalizeCodexRunText(delivery.turn?.id);
949
- const status = normalizeCodexRunText(delivery.turn?.status || delivery.turn?.raw?.status);
944
+ const { turnId: runId, status } = codexAppServerDeliveredTurn(delivery);
950
945
  if (!runId) {
951
946
  watcher?.failNow(new Error("Codex app-server accepted a conversation turn without returning its id."));
952
947
  await waitForResult?.catch(() => null);
@@ -1083,13 +1078,21 @@ export function createCodexConversationCommands({
1083
1078
  async function stopPersistentCodexConversation({
1084
1079
  conversationId,
1085
1080
  conversationState,
1086
- provider
1081
+ provider,
1082
+ turnOutcome = ""
1087
1083
  } = {}) {
1084
+ // Set before stopping: the provider can report turn/completed while it stops.
1085
+ if (conversationState && codexAppServerConversationTurnIsActive(conversationState.status)) {
1086
+ conversationState.turnOutcome = turnOutcome;
1087
+ }
1088
1088
  try {
1089
1089
  await provider.readThreadStatus(conversationId);
1090
1090
  await provider.stopThreadForObservationLoss(conversationId, "");
1091
1091
  } catch (error) {
1092
- if (!codexAppServerThreadIsMissing(error, conversationId) || error.message.toLowerCase().startsWith("thread not loaded:")) throw error;
1092
+ if (!codexAppServerThreadIsMissing(error, conversationId) || error.message.toLowerCase().startsWith("thread not loaded:")) {
1093
+ if (conversationState) conversationState.turnOutcome = "";
1094
+ throw error;
1095
+ }
1093
1096
  }
1094
1097
  if (conversationState) {
1095
1098
  conversationState.status = "interrupted";
@@ -1,4 +1,5 @@
1
- import { normalizeText, isPlainObject } from "./normalize.js";
1
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { normalizeNullableText } from "./normalize.js";
2
3
  import { classifyCodexAppServerEvent, codexAppServerNotificationParams, codexAppServerNotificationThreadId, codexAppServerNotificationTurnId, codexAppServerNotificationTurnStatus, codexAppServerNotificationError, codexAppServerStatusFromValue, codexAppServerProviderThreadAssistantSegments, codexAppServerProviderTurnId, codexAppServerProviderTurnStatus, codexAppServerProviderTurnForOperation, codexAppServerProviderTurnText, codexAppServerProviderTurnError } from "./codexEvents.js";
3
4
  import { codexAppServerTurnStatusIsProviderFailure, codexAppServerTurnStatusIsSuccessfulComplete } from "./codexTurnState.js";
4
5
 
@@ -6,12 +7,12 @@ const CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS = 180_000;
6
7
  const CODEX_APP_SERVER_DETACHED_FAILURE_DETAIL_GRACE_MS = 500;
7
8
 
8
9
  function codexAppServerTurnTokenUsage(notification = {}) {
9
- if (normalizeText(notification.method) !== "thread/tokenUsage/updated") {
10
+ if (normalizeNullableText(notification.method) !== "thread/tokenUsage/updated") {
10
11
  return null;
11
12
  }
12
13
  const params = codexAppServerNotificationParams(notification);
13
- const tokenUsage = isPlainObject(params.tokenUsage) ? params.tokenUsage : {};
14
- const turnUsage = isPlainObject(tokenUsage.last) ? tokenUsage.last : tokenUsage;
14
+ const tokenUsage = isRecord(params.tokenUsage) ? params.tokenUsage : {};
15
+ const turnUsage = isRecord(tokenUsage.last) ? tokenUsage.last : tokenUsage;
15
16
  const snapshot = {};
16
17
  for (const field of [
17
18
  "cachedInputTokens",
@@ -35,7 +36,7 @@ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "",
35
36
  onEvent = null,
36
37
  timeoutMs = CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS
37
38
  } = {}) {
38
- const normalizedThreadId = normalizeText(threadId);
39
+ const normalizedThreadId = normalizeNullableText(threadId);
39
40
  let targetTurnId = "";
40
41
  let finalText = "";
41
42
  let usage = null;
@@ -106,7 +107,7 @@ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "",
106
107
  const rawStatus = thread.raw?.status || thread.response?.thread?.status;
107
108
  return {
108
109
  status: codexAppServerStatusFromValue(rawStatus),
109
- statusType: normalizeText(typeof rawStatus === "string" ? rawStatus : rawStatus?.type),
110
+ statusType: normalizeNullableText(typeof rawStatus === "string" ? rawStatus : rawStatus?.type),
110
111
  text: codexAppServerProviderThreadAssistantSegments(thread, targetTurnId)
111
112
  .map((segment) => segment.text)
112
113
  .join("\n\n")
@@ -126,7 +127,7 @@ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "",
126
127
  async function finishFromCompletion(status = "completed") {
127
128
  try {
128
129
  if (!targetTurnId) {
129
- pendingCompletionStatus = normalizeText(status) || "completed";
130
+ pendingCompletionStatus = normalizeNullableText(status) || "completed";
130
131
  return;
131
132
  }
132
133
  const authoritative = await resultFromThread().catch(() => ({
@@ -136,7 +137,7 @@ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "",
136
137
  }));
137
138
  finalText = authoritative.text || finalText;
138
139
  if (!finalText && !allowEmptyResponse) {
139
- pendingCompletionStatus = normalizeText(status) || "completed";
140
+ pendingCompletionStatus = normalizeNullableText(status) || "completed";
140
141
  const systemError = authoritative.statusType === "systemError" || authoritative.status === "failed";
141
142
  failAfterDetailGrace(new Error(systemError
142
143
  ? "Codex app-server thread entered a system error before producing an assistant response."
@@ -175,7 +176,7 @@ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "",
175
176
  failAfterDetailGrace(error);
176
177
  },
177
178
  setTurnId(turnId = "") {
178
- targetTurnId = normalizeText(turnId);
179
+ targetTurnId = normalizeNullableText(turnId);
179
180
  if (pendingFailure) {
180
181
  failAfterDetailGrace(pendingFailure);
181
182
  return;
@@ -240,7 +241,7 @@ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "",
240
241
  void finishFromCompletion(status);
241
242
  }
242
243
  }
243
- const method = normalizeText(notification.method);
244
+ const method = normalizeNullableText(notification.method);
244
245
  if (method !== "turn/completed" && method !== "thread/status/changed") {
245
246
  return;
246
247
  }
@@ -279,7 +280,7 @@ async function waitForCodexAppServerTurn(provider, threadId = "", turn = null, {
279
280
  timeoutMs = CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS,
280
281
  createError = codexAppServerTurnError
281
282
  } = {}) {
282
- const normalizedThreadId = normalizeText(threadId || "");
283
+ const normalizedThreadId = normalizeNullableText(threadId || "");
283
284
  const turnId = codexAppServerProviderTurnId(turn || {});
284
285
  const status = codexAppServerProviderTurnStatus(turn || {});
285
286
  if (!normalizedThreadId || !turnId) {