@jskit-ai/assistant-core 0.1.239 → 0.1.241

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 (103) 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 +20 -16
  11. package/src/server/conversation/claudeProcess.js +2 -2
  12. package/src/server/conversation/claudeTurn.js +51 -40
  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/codexGoalCommands.js +1 -1
  21. package/src/server/conversation/codexHelperExecution.js +3 -13
  22. package/src/server/conversation/codexHelperThreadLedger.js +7 -13
  23. package/src/server/conversation/codexHelperThreadRecovery.js +2 -16
  24. package/src/server/conversation/codexHelperThreads.js +5 -6
  25. package/src/server/conversation/codexHistoryAdapter.js +32 -26
  26. package/src/server/conversation/codexMessageCommands.js +22 -22
  27. package/src/server/conversation/codexMessageDelivery.js +12 -7
  28. package/src/server/conversation/codexModelCatalog.js +97 -0
  29. package/src/server/conversation/codexNotificationObserver.js +24 -40
  30. package/src/server/conversation/codexNotificationQueue.js +2 -3
  31. package/src/server/conversation/codexOutput.js +20 -27
  32. package/src/server/conversation/codexProcess.js +123 -127
  33. package/src/server/conversation/codexProcessLeader.js +3 -3
  34. package/src/server/conversation/codexProvider.js +31 -614
  35. package/src/server/conversation/codexProviderOwner.js +2 -2
  36. package/src/server/conversation/codexProviderSelection.js +38 -40
  37. package/src/server/conversation/codexRenewalCommands.js +13 -10
  38. package/src/server/conversation/codexRenewalThreads.js +412 -0
  39. package/src/server/conversation/codexRunJournal.js +24 -42
  40. package/src/server/conversation/codexRuntimeLifecycle.js +19 -15
  41. package/src/server/conversation/codexSessionCleanup.js +11 -10
  42. package/src/server/conversation/codexSessionReconciliation.js +6 -6
  43. package/src/server/conversation/codexThreadReadiness.js +19 -33
  44. package/src/server/conversation/codexThreadStatus.js +3 -3
  45. package/src/server/conversation/codexTurn.js +124 -297
  46. package/src/server/conversation/codexTurnRecovery.js +49 -28
  47. package/src/server/conversation/codexTurnState.js +34 -13
  48. package/src/server/conversation/commandWrapper.js +3 -0
  49. package/src/server/conversation/deliveryTiming.js +14 -5
  50. package/src/server/conversation/index.js +1 -0
  51. package/src/server/conversation/localExecution.js +13 -17
  52. package/src/server/conversation/normalize.js +8 -3
  53. package/src/server/conversation/openCodeClient.js +9 -10
  54. package/src/server/conversation/openCodeProcess.js +9 -5
  55. package/src/server/conversation/openCodeRuntime.js +10 -11
  56. package/src/server/conversation/openCodeTurn.js +43 -29
  57. package/src/server/conversation/providers/claudeDriver.js +10 -16
  58. package/src/server/conversation/providers/codexDriver.js +27 -43
  59. package/src/server/conversation/providers/opencodeDriver.js +17 -42
  60. package/src/server/conversation/providers/scoped.js +48 -4
  61. package/src/server/conversation/runtime.js +50 -49
  62. package/src/server/conversation/runtimeStateUpgrade.js +34 -22
  63. package/src/server/conversation/transcript.js +10 -10
  64. package/src/server/hosts/codexConfiguration.js +1 -0
  65. package/src/server/hosts/codexProvider.js +8 -6
  66. package/src/server/index.js +2 -1
  67. package/src/server/lib/assistantToolLoop.js +1 -153
  68. package/src/server/lib/serviceToolCatalog.js +17 -42
  69. package/src/shared/assistantResource.js +4 -2
  70. package/src/shared/conversation/attachments.js +4 -0
  71. package/src/shared/conversation/longTextBlocks.js +54 -19
  72. package/src/shared/conversation/numberedQuestionSugar.js +17 -10
  73. package/src/shared/conversation/pagination.js +4 -6
  74. package/src/shared/conversation/patches.js +1 -4
  75. package/src/shared/conversation/streaming.js +3 -11
  76. package/src/shared/conversation/turns.js +11 -0
  77. package/src/shared/index.js +1 -0
  78. package/test/assistantBoundedToolLoop.test.js +2 -2
  79. package/test/assistantResource.test.js +4 -4
  80. package/test/assistantScroll.browser.test.js +29 -6
  81. package/test/assistantSettingsResource.test.js +2 -2
  82. package/test/claudeConversationSession.test.js +20 -3
  83. package/test/claudeTurn.test.js +27 -7
  84. package/test/codexClient.test.js +20 -0
  85. package/test/codexConversationSession.test.js +5 -3
  86. package/test/codexConversationsCheckpoint.test.js +51 -0
  87. package/test/codexModelCatalog.test.js +1 -1
  88. package/test/codexProviderHome.test.js +51 -0
  89. package/test/codexProviderSelection.test.js +71 -0
  90. package/test/codexTurn.test.js +222 -6
  91. package/test/conversationAgentRun.test.js +2 -1
  92. package/test/conversationAttachments.test.js +27 -2
  93. package/test/conversationDeliveryTiming.test.js +21 -0
  94. package/test/conversationNormalize.test.js +11 -0
  95. package/test/fixtures/nestedListQuestionMessage.md +37 -0
  96. package/test/frameworkActions.test.js +2 -2
  97. package/test/longTextBlocks.test.js +102 -0
  98. package/test/messageDelivery.test.js +7 -1
  99. package/test/openCodeClient.test.js +20 -0
  100. package/test/openCodeRuntime.test.js +1 -1
  101. package/test/openCodeTurn.test.js +90 -0
  102. package/test/opencodeConversationSession.test.js +1 -1
  103. package/test/serviceToolCatalog.test.js +27 -0
@@ -5,18 +5,30 @@ import { readClaudeJsonFrames } from "./claudeClient.js";
5
5
  import { createNativeHistoryExport, retireNativeConversation } from "./nativeHistoryExport.js";
6
6
 
7
7
  const UUID_PATTERN = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/iu;
8
+ const TRANSCRIPT_PATH_PATTERN = /\.jsonl(?:\.(?:superseded|orphaned)-.*)?$/u;
8
9
 
9
10
  function requireClaudeSessionId(id) {
10
11
  if (!UUID_PATTERN.test(id)) throw new TypeError("Invalid Claude conversation id.");
11
12
  return id;
12
13
  }
13
14
 
15
+ // Claude's project directory name for a canonical working directory.
16
+ function claudeProjectDirectoryName(canonical) {
17
+ return canonical.normalize("NFC").replace(/[^a-zA-Z0-9]/gu, "-");
18
+ }
19
+
20
+ // Text the person authored in a user frame: plain content or its text blocks.
21
+ function claudeUserText(content) {
22
+ return typeof content === "string" ? content : (Array.isArray(content) ? content : [])
23
+ .filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n");
24
+ }
25
+
14
26
  async function archivedClaudeProjectDirectory(workdir) {
15
27
  const canonical = await realpath(workdir).catch((error) => {
16
28
  if (error.code === "ENOENT") return workdir;
17
29
  throw error;
18
30
  });
19
- return canonical.normalize("NFC").replace(/[^a-zA-Z0-9]/gu, "-");
31
+ return claudeProjectDirectoryName(canonical);
20
32
  }
21
33
 
22
34
  function matchingClaudeProjectDirectories(names, directory) {
@@ -37,7 +49,7 @@ export async function listClaudeConversationStorage({ configRoot, binding }) {
37
49
  const entries = await readdir(path.join(projects, name), { withFileTypes: true });
38
50
  if (entries.length > 10000) throw new Error("Claude conversation inventory exceeds its limit.");
39
51
  for (const entry of entries) {
40
- const id = entry.name.replace(/\.jsonl(?:\.(?:superseded|orphaned)-.*)?$/u, "");
52
+ const id = entry.name.replace(TRANSCRIPT_PATH_PATTERN, "");
41
53
  if (UUID_PATTERN.test(id) && entry.isFile()) ids.add(id);
42
54
  }
43
55
  }
@@ -49,14 +61,13 @@ export async function listClaudeConversationStorage({ configRoot, binding }) {
49
61
  // lookup because its native and Node builds use different hash suffixes.
50
62
  async function claudeHistoryPath({ configRoot, workdir, conversationId }) {
51
63
  requireClaudeSessionId(conversationId);
52
- const canonical = (await realpath(workdir)).normalize("NFC");
53
- const directory = canonical.replace(/[^a-zA-Z0-9]/gu, "-");
64
+ const directory = claudeProjectDirectoryName(await realpath(workdir));
54
65
  const projects = path.join(configRoot, "projects");
55
- const candidates = directory.length <= 200 ? [directory] :
56
- (await readdir(projects).catch((error) => {
66
+ const candidates = directory.length <= 200 ? [directory] : matchingClaudeProjectDirectories(
67
+ await readdir(projects).catch((error) => {
57
68
  if (error.code === "ENOENT") return [];
58
69
  throw error;
59
- })).filter((name) => name.startsWith(`${directory.slice(0, 200)}-`));
70
+ }), directory);
60
71
  for (const name of candidates) {
61
72
  const candidate = path.join(projects, name, `${conversationId}.jsonl`);
62
73
  const info = await stat(candidate).catch((error) => {
@@ -283,10 +294,7 @@ async function readClaudeHistory(options) {
283
294
  userIds.push(frame.uuid);
284
295
  lastUserId = frame.uuid;
285
296
  if (options.includeUserMessages && !frame.parent_tool_use_id) {
286
- const content = frame.message.content;
287
- const text = typeof content === "string" ? content : content
288
- .filter(block => block.type === "text" && typeof block.text === "string").map(block => block.text).join("\n\n");
289
- inspected.userMessages.push({ id: frame.uuid, text });
297
+ inspected.userMessages.push({ id: frame.uuid, text: claudeUserText(frame.message.content) });
290
298
  }
291
299
  }
292
300
  if (options.includeFinalCarriers && !frame.parent_tool_use_id && !frame.isMeta) {
@@ -333,8 +341,6 @@ export async function deleteClaudeConversationHistory({ configRoot, workdir, con
333
341
 
334
342
  export { claudeHistoryPath, claudeMessageBlocks, readClaudeHistory, requireClaudeSessionId, archivedClaudeProjectDirectory, matchingClaudeProjectDirectories };
335
343
 
336
- const TRANSCRIPT_PATH_PATTERN = /\.jsonl(?:\.(?:superseded|orphaned)-.*)?$/u;
337
-
338
344
  export async function retireClaudeConversationHistory({ configRoot, binding, beforeDelete, requireIdle, signal }) {
339
345
  const id = requireClaudeSessionId(binding.conversationId);
340
346
  if (!path.isAbsolute(configRoot)) throw new TypeError("Claude retirement requires an absolute configuration root.");
@@ -411,9 +417,7 @@ export async function retireClaudeConversationHistory({ configRoot, binding, bef
411
417
  .filter((message) => message.role !== "thinking")
412
418
  .map((message) => ({ ...provenance, role: "assistant", text: message.text }));
413
419
  if (frame.type === "user") {
414
- const content = frame.message?.content;
415
- const user = typeof content === "string" ? content : (Array.isArray(content) ? content : [])
416
- .filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n");
420
+ const user = claudeUserText(frame.message?.content);
417
421
  if (user) text.push({ ...provenance, role: "user", text: user });
418
422
  }
419
423
  if (frame.attachment?.type === "goal_status" && typeof frame.attachment.condition === "string") {
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { jsonHash } from "./normalize.js";
2
2
  import { nativeAiProvider } from "../../shared/nativeProviders.js";
3
3
  import { Duplex } from "node:stream";
4
4
  import { stat } from "node:fs/promises";
@@ -83,7 +83,7 @@ async function readNativeClaudeCodeAuthStatus({ env, credentialHome, command, co
83
83
 
84
84
  /** Exact existing native connection pin, shared with stopped metadata conversion. */
85
85
  export function claudeConnectionIdentity(configRoot, providerId, baseUrl, apiKey) {
86
- return createHash("sha256").update(JSON.stringify([configRoot, providerId, baseUrl, apiKey])).digest("hex");
86
+ return jsonHash([configRoot, providerId, baseUrl, apiKey]);
87
87
  }
88
88
 
89
89
  /** Native provider routing; the application supplies only its authorized selection. */
@@ -5,13 +5,14 @@ import { setTimeout as delay } from "node:timers/promises";
5
5
  import { claudeMessageBlocks, deleteClaudeConversationHistory, readClaudeHistory, requireClaudeSessionId, retireClaudeConversationHistory } from "./claudeHistory.js";
6
6
  import { bindClaudeConversationAccount, claudeCodeArguments, createClaudeAccountQueries, createClaudeCodeProcess, stopClaudeCodeProcess } from "./claudeProcess.js";
7
7
  import { claudeApplicationToolResponse } from "./claudeTools.js";
8
+ import { normalizeNullableText } from "./normalize.js";
8
9
  import { createClaudeConversationAdapter } from "./providers/claude.js";
9
10
 
10
11
  function failure(message, code = "assistant_claude_turn_failed") {
11
12
  return Object.assign(new Error(message), { code });
12
13
  }
13
14
 
14
- const text = value => String(value ?? "").trim();
15
+ const text = normalizeNullableText;
15
16
  const now = () => new Date().toISOString();
16
17
 
17
18
  function hasPendingClaudeStartup(entry) {
@@ -166,6 +167,11 @@ export function createClaudeConversationOwner({
166
167
  let closing = false;
167
168
  const createProcess = process.create || createClaudeCodeProcess;
168
169
  const saveAccount = store.save;
170
+ // Two hosts share this owner. A bound store (one with releaseExecution) is a
171
+ // runtime binding: the native turn owns activity and execution release.
172
+ // Otherwise the host retains entries and their original checkpoint order.
173
+ const bound = Boolean(store.releaseExecution);
174
+ const isTurnActive = entry => bound ? entry.nativeTurn.read().active : Boolean(entry.turn?.active);
169
175
 
170
176
  function createAccountQueries({ accountIdentity, createProcess }) {
171
177
  return createClaudeAccountQueries({ accountIdentity,
@@ -249,13 +255,13 @@ export function createClaudeConversationOwner({
249
255
  }
250
256
  }
251
257
  if (entry.pausedGoal && entry.blockedGoal) throw createError("The saved Claude goal cannot be both paused and blocked.");
252
- entry.nativeTurn = createClaudeConversationTurn({ conversationId: id, entry,
258
+ entry.nativeTurn = createClaudeConversationTurn({ conversationId: id, entry, bound,
253
259
  onEvent: event => onEvent(entry, event),
254
260
  process: {
255
261
  stopExecution: process.stopExecution,
256
- ...(store.releaseExecution ? { releaseExecution: () => store.releaseExecution(entry) } : {}),
262
+ ...(bound ? { releaseExecution: () => store.releaseExecution(entry) } : {}),
257
263
  getProcess: () => entry.stopping ? null : entry.process,
258
- isActive: () => store.releaseExecution ? entry.nativeTurn.read().active : Boolean(entry.turn?.active),
264
+ isActive: () => isTurnActive(entry),
259
265
  startProcess: input => startProcess(entry, input),
260
266
  stopProcess: reason => entry.process || entry.executionId
261
267
  ? entry.nativeTurn.stopProcess(reason, { preparing: true }) : Promise.resolve()
@@ -271,7 +277,7 @@ export function createClaudeConversationOwner({
271
277
  try { await store.save(entry); }
272
278
  catch (error) { Object.assign(entry, previous); throw error; }
273
279
  },
274
- readState: () => ({ running: Boolean(entry.process), active: store.releaseExecution ? entry.nativeTurn.read().active : Boolean(entry.turn?.active) }),
280
+ readState: () => ({ running: Boolean(entry.process), active: isTurnActive(entry) }),
275
281
  interrupt: () => interrupt(entry), send: input => send(entry, input) });
276
282
  entries.set(key, entry);
277
283
  await store.save(entry);
@@ -312,7 +318,7 @@ export function createClaudeConversationOwner({
312
318
  async function ensureReady(entry) {
313
319
  if (entry.process) {
314
320
  // Status reads retain active instructions; apply changes at idle admission.
315
- if (store.releaseExecution ? entry.nativeTurn.read().active : entry.turn?.active) await bindAccount(entry);
321
+ if (isTurnActive(entry)) await bindAccount(entry);
316
322
  else await ensureProcess(entry);
317
323
  } else {
318
324
  await bindAccount(entry);
@@ -441,10 +447,10 @@ export function createClaudeConversationOwner({
441
447
  }
442
448
  await entry.nativeTurn.receive(frame);
443
449
  },
444
- ...(store.releaseExecution || options.applicationTools || entry.command?.tools ? {
450
+ ...(bound || options.applicationTools || entry.command?.tools ? {
445
451
  async onControlRequest(request, { signal }) {
446
452
  const active = entry.command;
447
- const suppliedTools = !store.releaseExecution && active?.tools;
453
+ const suppliedTools = !bound && active?.tools;
448
454
  const owner = active?.nativeOwner;
449
455
  const work = applicationCall.run({ entry, command: active, owner,
450
456
  toolUseId: request.message?.params?._meta?.["claudecode/toolUseId"] },
@@ -471,7 +477,7 @@ export function createClaudeConversationOwner({
471
477
  } : {}),
472
478
  onFailure: async error => {
473
479
  if (entry.localCommand?.processToken === processToken) entry.localCommand.reject(error);
474
- if (store.releaseExecution) {
480
+ if (bound) {
475
481
  entry.nativeTurn.invalidateInstructions();
476
482
  try { await entry.nativeTurn.stopProcess(error.message); }
477
483
  catch (cleanupError) { error = cleanupError; await context.reportFailure(cleanupError); }
@@ -492,7 +498,7 @@ export function createClaudeConversationOwner({
492
498
  await onEvent(entry, { type: "execution", context, executionId: entry.executionId });
493
499
  } catch (bindingError) { error.bindingError = bindingError; }
494
500
  }
495
- if (store.releaseExecution && error.stopProof?.scopeEmpty) {
501
+ if (bound && error.stopProof?.scopeEmpty) {
496
502
  await store.releaseExecution(entry);
497
503
  if (entry.startupCleanup?.executionId === error.executionId) entry.startupCleanup = null;
498
504
  }
@@ -893,6 +899,14 @@ export function createClaudeConversationOwner({
893
899
  return { status: "available", threadId: entry.id, goal: await entry.goals.read(history), goalCommandId: history.goalCommandId || null };
894
900
  }
895
901
 
902
+ // A native Set, Resume or Cancel supersedes a retained Pause or blocker.
903
+ async function releaseRetainedGoal(entry) {
904
+ if (!entry.pausedGoal && !entry.blockedGoal) return;
905
+ entry.pausedGoal = null;
906
+ entry.blockedGoal = null;
907
+ await store.save(entry);
908
+ }
909
+
896
910
  async function updateGoal(entry, input, operations) {
897
911
  await bindAccount(entry);
898
912
  const call = applicationCall.getStore();
@@ -928,7 +942,7 @@ export function createClaudeConversationOwner({
928
942
  for (;;) {
929
943
  const history = await readHistory(entry);
930
944
  if (history.goalCommandId === claudeNativeMessageId(messageId)) {
931
- if (entry.pausedGoal || entry.blockedGoal) { entry.pausedGoal = null; entry.blockedGoal = null; await store.save(entry); }
945
+ await releaseRetainedGoal(entry);
932
946
  result = { status: "available", threadId: entry.id, goal: await entry.goals.read(history), goalCommandId: history.goalCommandId };
933
947
  break;
934
948
  }
@@ -952,7 +966,7 @@ export function createClaudeConversationOwner({
952
966
  }
953
967
 
954
968
  async function cancel(control, { context, acquire, reportFailure, entry }) {
955
- if (store.releaseExecution) {
969
+ if (bound) {
956
970
  await entry.nativeTurn.stopProcess("Work stopped.");
957
971
  return;
958
972
  }
@@ -973,7 +987,7 @@ export function createClaudeConversationOwner({
973
987
  }
974
988
 
975
989
  async function dispatch(control, command, { acquire, entry: selectedEntry, dispatch: nativeDispatch }) {
976
- if (store.releaseExecution) {
990
+ if (bound) {
977
991
  await send(selectedEntry, command.input, { command: nativeDispatch });
978
992
  return;
979
993
  }
@@ -1001,14 +1015,13 @@ export function createClaudeConversationOwner({
1001
1015
  }
1002
1016
 
1003
1017
  function steer(control, command, options) {
1004
- if (!store.releaseExecution) return dispatch(control, command, options).then(delivered => ({ value: delivered.value }));
1018
+ if (!bound) return dispatch(control, command, options).then(delivered => ({ value: delivered.value }));
1005
1019
  if (!control.current?.steer || !control.current.entry.nativeTurn.read().active) throw new Error("Claude has no active turn to steer.");
1006
1020
  return control.current.steer(command);
1007
1021
  }
1008
1022
 
1009
1023
  async function run(control, command, { acquire, reportFailure, entry: selectedEntry, maximumOutput, maximumToolCalls = 32, timeoutMs } = {}) {
1010
1024
  if (control.disposed || control.current) throw new Error("This Claude conversation is closed or already working.");
1011
- const binding = Boolean(store.releaseExecution);
1012
1025
  const { configuration, context, input, tools, signal, beforeDispatch, accept, onMessage, onEvent, onNativeToolUse } = command;
1013
1026
  const completion = Promise.withResolvers();
1014
1027
  completion.promise.catch(() => {});
@@ -1039,7 +1052,7 @@ export function createClaudeConversationOwner({
1039
1052
  let delivered;
1040
1053
  function abort() {
1041
1054
  if (cancellation) return;
1042
- if (binding) {
1055
+ if (bound) {
1043
1056
  cancellation = entry.nativeTurn.cancel(entry.process?.client, () => entry.nativeTurn.stopProcess("Work stopped."));
1044
1057
  cancellation.then(() => completion.reject(signal.reason), error => completion.reject(error));
1045
1058
  } else {
@@ -1048,7 +1061,7 @@ export function createClaudeConversationOwner({
1048
1061
  }
1049
1062
  }
1050
1063
  try {
1051
- if (binding) {
1064
+ if (bound) {
1052
1065
  entry = selectedEntry;
1053
1066
  if (entry.disposed || entry.command) throw new Error("This Claude conversation is closed or already working.");
1054
1067
  active.entry = entry;
@@ -1062,7 +1075,7 @@ export function createClaudeConversationOwner({
1062
1075
  let dispatchFailure;
1063
1076
  try {
1064
1077
  delivered = await dispatch(control, command, { acquire, entry, dispatch: nativeDispatch });
1065
- if (binding) ready.resolve();
1078
+ if (bound) ready.resolve();
1066
1079
  } catch (error) { dispatchFailure = error; }
1067
1080
  for (;;) {
1068
1081
  const settling = active.delivery;
@@ -1078,15 +1091,15 @@ export function createClaudeConversationOwner({
1078
1091
  catch (error) { problem = error; }
1079
1092
  }
1080
1093
  problem = dispatchFailure || failure || problem;
1081
- if (!problem && settling?.completion && (binding ? result.status !== "completed" : !result.ok)) {
1082
- problem = Object.assign(new Error(result.error || (binding ? "Claude interrupted this answer." : `Claude turn ${result.status}.`)), { status: result.status });
1094
+ if (!problem && settling?.completion && (bound ? result.status !== "completed" : !result.ok)) {
1095
+ problem = Object.assign(new Error(result.error || (bound ? "Claude interrupted this answer." : `Claude turn ${result.status}.`)), { status: result.status });
1083
1096
  }
1084
1097
  break;
1085
1098
  }
1086
1099
  } catch (error) { problem = error; }
1087
1100
  ready.reject(problem || new Error("Claude work ended."));
1088
1101
  signal.removeEventListener("abort", abort);
1089
- if (binding) {
1102
+ if (bound) {
1090
1103
  try {
1091
1104
  if (problem || signal.aborted) await (cancellation || entry.nativeTurn.stopProcess(problem?.message || "Work stopped."));
1092
1105
  } catch (error) {
@@ -1118,19 +1131,19 @@ export function createClaudeConversationOwner({
1118
1131
  }
1119
1132
  if (control.current === active) control.current = null;
1120
1133
  if (problem) {
1121
- if (binding && nativeDispatch.attempted && !nativeDispatch.admitted && problem.delivery !== "not-sent") problem.delivery = "uncertain";
1134
+ if (bound && nativeDispatch.attempted && !nativeDispatch.admitted && problem.delivery !== "not-sent") problem.delivery = "uncertain";
1122
1135
  throw problem;
1123
1136
  }
1124
- if (!binding) return { value: delivered.value };
1137
+ if (!bound) return { value: delivered.value };
1125
1138
  }
1126
1139
 
1127
- return Object.freeze({ applicationToolsSupported: true, entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readFinalAssistantResult, readHistory, ensureReady, ensureProcess, readContextUsage, compactContext, stopForTerminal, prepareTerminal, send, startTurn,
1140
+ return Object.freeze({ applicationToolsSupported: true, entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readFinalAssistantResult, readHistory, ensureReady, ensureProcess, readContextUsage, compactContext, releaseRetainedGoal, stopForTerminal, prepareTerminal, send, startTurn,
1128
1141
  interrupt, read, inspectAdmission, hasActiveTemporaryConversation, hasActiveConversation, retireConversationHistory, reconcileSessions, restoreSessionEntries, closeSession, closeProject, invalidateRuntimes, drain, forget, close, deleteConversation, recover, stopSelected, wait, stopConversation,
1129
1142
  readGoal, updateGoal, renewal, run, steer, cancel });
1130
1143
  }
1131
1144
 
1132
1145
  /** Native turn handling moved from the production session provider; hosts persist its events. */
1133
- export function createClaudeConversationTurn({ conversationId, onEvent = async () => {}, process, entry } = {}) {
1146
+ export function createClaudeConversationTurn({ conversationId, onEvent = async () => {}, process, entry, bound = false } = {}) {
1134
1147
  if (!conversationId) throw new TypeError("A Claude conversation id is required.");
1135
1148
  const instructions = process ? createClaudeConversationAdapter(process) : null;
1136
1149
  const publish = entry ? publishEntryEvent : onEvent;
@@ -1241,31 +1254,30 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
1241
1254
  invalidateFinalAssistantResult();
1242
1255
  // Runtime bindings commit execution release before dropping the handle.
1243
1256
  // Retained host entries keep their original post-proof checkpoint order.
1244
- const binding = Boolean(process.releaseExecution);
1245
1257
  if (entry.stopPending) return entry.stopPending;
1246
1258
  const active = read().active;
1247
- if (!binding) entry.stopping = true;
1259
+ if (!bound) entry.stopping = true;
1248
1260
  const operation = async () => {
1249
1261
  const startup = entry.startupCleanup?.executionId === entry.executionId ? entry.startupCleanup : null;
1250
1262
  let proof;
1251
1263
  try {
1252
- if (!binding || entry.process || entry.executionId) {
1264
+ if (!bound || entry.process || entry.executionId) {
1253
1265
  proof = await stopClaudeCodeProcess({ process: entry.process || startup, executionId: entry.executionId,
1254
1266
  stopExecution: process.stopExecution });
1255
1267
  }
1256
1268
  } catch (error) {
1257
- if (binding || error.code !== "claude_stop_unconfirmed") throw error;
1269
+ if (bound || error.code !== "claude_stop_unconfirmed") throw error;
1258
1270
  entry.observationError = reason || "Claude process exit has not been confirmed. Try Stop again.";
1259
1271
  await onEvent({ type: "save" });
1260
1272
  await onEvent({ type: "stop-unconfirmed" });
1261
1273
  throw error;
1262
1274
  }
1263
- if (binding && (entry.process || entry.executionId)) await process.releaseExecution();
1275
+ if (bound && (entry.process || entry.executionId)) await process.releaseExecution();
1264
1276
  entry.process = null;
1265
1277
  entry.executionId = "";
1266
1278
  entry.exitProof = proof;
1267
- stop(binding ? reason : reason || "Claude stopped before acknowledging the prompt.");
1268
- if (binding) {
1279
+ stop(bound ? reason : reason || "Claude stopped before acknowledging the prompt.");
1280
+ if (bound) {
1269
1281
  if (entry.startupCleanup === startup) entry.startupCleanup = null;
1270
1282
  if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
1271
1283
  return;
@@ -1287,7 +1299,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
1287
1299
  if (entry.startupCleanup === startup) entry.startupCleanup = null;
1288
1300
  return entry.exitProof;
1289
1301
  };
1290
- entry.stopPending = operation().catch(error => { if (binding) error.cleanupFailed = true; throw error; })
1302
+ entry.stopPending = operation().catch(error => { if (bound) error.cleanupFailed = true; throw error; })
1291
1303
  .finally(() => { entry.stopPending = null; });
1292
1304
  return entry.stopPending;
1293
1305
  }
@@ -1295,9 +1307,8 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
1295
1307
  async function publishEntryEvent(event, deferred = false) {
1296
1308
  if (entry.stopping) return;
1297
1309
  if (event.type === "settled" && event.generation !== generation) return;
1298
- const binding = Boolean(process.releaseExecution);
1299
1310
  const command = entry.command;
1300
- if (event.type === "settled" && !binding && !deferred && command?.finishIfCurrent && command !== checkpointedCommand) {
1311
+ if (event.type === "settled" && !bound && !deferred && command?.finishIfCurrent && command !== checkpointedCommand) {
1301
1312
  const delivery = command.delivery;
1302
1313
  const completion = entry.completion;
1303
1314
  const owner = acceptedInput;
@@ -1328,17 +1339,17 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
1328
1339
  }
1329
1340
  if (event.type === "continued") {
1330
1341
  if (entry.completion === resolvedCompletion) entry.completion = Promise.withResolvers();
1331
- if (binding) setState("active");
1342
+ if (bound) setState("active");
1332
1343
  else await updateState("active");
1333
1344
  } else if (event.type === "admitted") {
1334
1345
  if (event.userEcho) entry.sent = true;
1335
- if (!binding) await onEvent({ type: "save" });
1346
+ if (!bound) await onEvent({ type: "save" });
1336
1347
  } else if (event.type === "phase") {
1337
1348
  entry.turn.phase = event.phase;
1338
- if (!binding) await updateState(entry.turn.state);
1349
+ if (!bound) await updateState(entry.turn.state);
1339
1350
  } else if (event.type === "settled") {
1340
1351
  const state = event.status === "interrupted" ? "interrupted" : event.status === "failed" ? "failed" : "completed";
1341
- if (binding) setState(state, event.error);
1352
+ if (bound) setState(state, event.error);
1342
1353
  else if (!await updateState(state, event.error, event.generation)) return;
1343
1354
  }
1344
1355
  const published = await onEvent(event);
@@ -1350,7 +1361,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
1350
1361
  if (event.type === "settled") {
1351
1362
  if (event.generation !== generation) return;
1352
1363
  if (event.source === "result") await onEvent({ type: "provider-update", change: "goal" });
1353
- resolveCompletion(binding ? event : readResult());
1364
+ resolveCompletion(bound ? event : readResult());
1354
1365
  }
1355
1366
  return published;
1356
1367
  }
@@ -0,0 +1,104 @@
1
+ import path from "node:path";
2
+ import { codexAuthOutputRequiresReconnect } from "./codexConfiguration.js";
3
+ import {
4
+ assertExistingDirectory, codexAppServerCommandBaseEnv, codexAppServerCredentialHome,
5
+ codexAppServerProcessCwd, ensureWritablePrivateDirectory
6
+ } from "./codexProcess.js";
7
+ import { normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
8
+ import { normalizeNullableText as normalizeAgentText } from "./normalize.js";
9
+
10
+ // Codex auth preflight: one bounded `codex debug models` run whose
11
+ // reconnect-required output marks the stored credentials invalid.
12
+ const CODEX_AUTH_PREFLIGHT_TIMEOUT_MS = 15000;
13
+ const CODEX_AUTH_PREFLIGHT_OUTPUT_TAIL_BYTES = 4096;
14
+
15
+ function tailAppend(text = "", chunk = "", maxBytes = CODEX_AUTH_PREFLIGHT_OUTPUT_TAIL_BYTES) {
16
+ const next = `${String(text || "")}${String(chunk || "")}`;
17
+ return next.length > maxBytes ? next.slice(-maxBytes) : next;
18
+ }
19
+
20
+ function codexAuthPreflightArgs() {
21
+ return [
22
+ "-c",
23
+ "check_for_update_on_startup=false",
24
+ "debug",
25
+ "models"
26
+ ];
27
+ }
28
+
29
+ async function runCodexAuthPreflight({
30
+ codexCommand = "codex",
31
+ env = process.env,
32
+ executionRoot = "",
33
+ runtimeDir = "",
34
+ terminalEnv = {},
35
+ timeoutMs = CODEX_AUTH_PREFLIGHT_TIMEOUT_MS,
36
+ toolHomeSource = "",
37
+ workdir = ""
38
+ } = {}, execution) {
39
+ const normalizedToolHomeSource = normalizeAgentText(toolHomeSource);
40
+ if (normalizedToolHomeSource) {
41
+ await assertExistingDirectory(normalizedToolHomeSource, "Codex credential home");
42
+ }
43
+ const normalizedRuntimeDir = normalizeAgentText(runtimeDir);
44
+ if (normalizedRuntimeDir) {
45
+ await ensureWritablePrivateDirectory(path.resolve(normalizedRuntimeDir));
46
+ }
47
+ const baseEnv = codexAppServerCommandBaseEnv({
48
+ env,
49
+ terminalEnv
50
+ });
51
+ const processCwd = codexAppServerProcessCwd({
52
+ executionRoot,
53
+ runtimeDir,
54
+ workdir
55
+ });
56
+ try {
57
+ const result = await execution.run({
58
+ args: codexAuthPreflightArgs(),
59
+ baseEnv,
60
+ command: codexCommand,
61
+ credentialHome: codexAppServerCredentialHome(normalizedToolHomeSource, baseEnv),
62
+ cwd: processCwd,
63
+ inheritProcessEnv: false,
64
+ mode: "capture",
65
+ timeout: normalizePositiveInteger(timeoutMs, { fallback: CODEX_AUTH_PREFLIGHT_TIMEOUT_MS })
66
+ });
67
+ return {
68
+ code: result.exitCode,
69
+ ok: result.ok === true,
70
+ output: tailAppend("", result.output || [
71
+ result.stderr,
72
+ result.stdout
73
+ ].filter(Boolean).join("\n")),
74
+ signal: result.signal,
75
+ timedOut: result.timedOut === true
76
+ };
77
+ } catch (error) {
78
+ return {
79
+ error,
80
+ ok: false,
81
+ output: normalizeAgentText(error?.message || error)
82
+ };
83
+ }
84
+ }
85
+
86
+ export async function assertCodexAuthPreflightReady(options = {}, {
87
+ reason = "codex-auth-preflight", execution, credentials
88
+ } = {}) {
89
+ const result = await runCodexAuthPreflight(options, execution);
90
+ if (result.ok && !codexAuthOutputRequiresReconnect(result.output)) {
91
+ return result;
92
+ }
93
+ const observed = normalizeAgentText(result.output || result.error?.message || "Codex auth preflight failed.");
94
+ if (codexAuthOutputRequiresReconnect(observed)) {
95
+ await credentials.markInvalid( {
96
+ reason
97
+ });
98
+ throw credentials.reconnectError({
99
+ modelProviderId: "",
100
+ observed
101
+ });
102
+ }
103
+ throw new Error(observed || "Codex auth preflight failed.");
104
+ }
@@ -1,10 +1,13 @@
1
1
  import { createConnection } from "node:net";
2
2
  import WebSocket from "ws";
3
- import { normalizeText as normalizeAgentText } from "./normalize.js";
3
+ import { normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import { normalizeNullableText as normalizeAgentText } from "./normalize.js";
4
5
  const CODEX_APP_SERVER_REQUEST_TIMEOUT_MS = 60_000;
5
- function normalizePositiveInteger(value, fallback) {
6
- const number = Number(value);
7
- return Number.isSafeInteger(number) && number > 0 ? number : fallback;
6
+
7
+ function messageTooLargeError() {
8
+ const error = new Error("Codex app-server message exceeded the configured transport limit.");
9
+ error.code = "assistant_codex_app_server_message_too_large";
10
+ return error;
8
11
  }
9
12
 
10
13
  function socketPathFromCodexAppServerEndpoint(endpoint = "") {
@@ -56,10 +59,8 @@ class CodexAppServerJsonRpcClient {
56
59
  WebSocketImpl = WebSocket
57
60
  } = {}) {
58
61
  this.endpoint = normalizeAgentText(endpoint);
59
- this.maxMessageBytes = Number.isSafeInteger(maxMessageBytes) && maxMessageBytes > 0
60
- ? maxMessageBytes
61
- : Number.POSITIVE_INFINITY;
62
- this.requestTimeoutMs = normalizePositiveInteger(requestTimeoutMs, CODEX_APP_SERVER_REQUEST_TIMEOUT_MS);
62
+ this.maxMessageBytes = normalizePositiveInteger(maxMessageBytes, { fallback: Number.POSITIVE_INFINITY });
63
+ this.requestTimeoutMs = normalizePositiveInteger(requestTimeoutMs, { fallback: CODEX_APP_SERVER_REQUEST_TIMEOUT_MS });
63
64
  this.WebSocketImpl = WebSocketImpl;
64
65
  this.onDisconnect = onDisconnect;
65
66
  this.nextRequestId = 1;
@@ -233,11 +234,7 @@ class CodexAppServerJsonRpcClient {
233
234
  throw new Error("Codex app-server connection is not open.");
234
235
  }
235
236
  const serialized = JSON.stringify(payload);
236
- if (Buffer.byteLength(serialized, "utf8") > this.maxMessageBytes) {
237
- const error = new Error("Codex app-server message exceeded the configured transport limit.");
238
- error.code = "assistant_codex_app_server_message_too_large";
239
- throw error;
240
- }
237
+ if (Buffer.byteLength(serialized, "utf8") > this.maxMessageBytes) throw messageTooLargeError();
241
238
  this.socket.send(serialized);
242
239
  }
243
240
 
@@ -246,9 +243,7 @@ class CodexAppServerJsonRpcClient {
246
243
  try {
247
244
  const text = socketMessageText(event, this.maxMessageBytes);
248
245
  if (text === null) {
249
- const error = new Error("Codex app-server message exceeded the configured transport limit.");
250
- error.code = "assistant_codex_app_server_message_too_large";
251
- this.disconnect(error);
246
+ this.disconnect(messageTooLargeError());
252
247
  return;
253
248
  }
254
249
  message = JSON.parse(text);