@jskit-ai/assistant-core 0.1.240 → 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 (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 +20 -16
  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 +27 -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
@@ -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
  }
@@ -960,7 +966,7 @@ export function createClaudeConversationOwner({
960
966
  }
961
967
 
962
968
  async function cancel(control, { context, acquire, reportFailure, entry }) {
963
- if (store.releaseExecution) {
969
+ if (bound) {
964
970
  await entry.nativeTurn.stopProcess("Work stopped.");
965
971
  return;
966
972
  }
@@ -981,7 +987,7 @@ export function createClaudeConversationOwner({
981
987
  }
982
988
 
983
989
  async function dispatch(control, command, { acquire, entry: selectedEntry, dispatch: nativeDispatch }) {
984
- if (store.releaseExecution) {
990
+ if (bound) {
985
991
  await send(selectedEntry, command.input, { command: nativeDispatch });
986
992
  return;
987
993
  }
@@ -1009,14 +1015,13 @@ export function createClaudeConversationOwner({
1009
1015
  }
1010
1016
 
1011
1017
  function steer(control, command, options) {
1012
- 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 }));
1013
1019
  if (!control.current?.steer || !control.current.entry.nativeTurn.read().active) throw new Error("Claude has no active turn to steer.");
1014
1020
  return control.current.steer(command);
1015
1021
  }
1016
1022
 
1017
1023
  async function run(control, command, { acquire, reportFailure, entry: selectedEntry, maximumOutput, maximumToolCalls = 32, timeoutMs } = {}) {
1018
1024
  if (control.disposed || control.current) throw new Error("This Claude conversation is closed or already working.");
1019
- const binding = Boolean(store.releaseExecution);
1020
1025
  const { configuration, context, input, tools, signal, beforeDispatch, accept, onMessage, onEvent, onNativeToolUse } = command;
1021
1026
  const completion = Promise.withResolvers();
1022
1027
  completion.promise.catch(() => {});
@@ -1047,7 +1052,7 @@ export function createClaudeConversationOwner({
1047
1052
  let delivered;
1048
1053
  function abort() {
1049
1054
  if (cancellation) return;
1050
- if (binding) {
1055
+ if (bound) {
1051
1056
  cancellation = entry.nativeTurn.cancel(entry.process?.client, () => entry.nativeTurn.stopProcess("Work stopped."));
1052
1057
  cancellation.then(() => completion.reject(signal.reason), error => completion.reject(error));
1053
1058
  } else {
@@ -1056,7 +1061,7 @@ export function createClaudeConversationOwner({
1056
1061
  }
1057
1062
  }
1058
1063
  try {
1059
- if (binding) {
1064
+ if (bound) {
1060
1065
  entry = selectedEntry;
1061
1066
  if (entry.disposed || entry.command) throw new Error("This Claude conversation is closed or already working.");
1062
1067
  active.entry = entry;
@@ -1070,7 +1075,7 @@ export function createClaudeConversationOwner({
1070
1075
  let dispatchFailure;
1071
1076
  try {
1072
1077
  delivered = await dispatch(control, command, { acquire, entry, dispatch: nativeDispatch });
1073
- if (binding) ready.resolve();
1078
+ if (bound) ready.resolve();
1074
1079
  } catch (error) { dispatchFailure = error; }
1075
1080
  for (;;) {
1076
1081
  const settling = active.delivery;
@@ -1086,15 +1091,15 @@ export function createClaudeConversationOwner({
1086
1091
  catch (error) { problem = error; }
1087
1092
  }
1088
1093
  problem = dispatchFailure || failure || problem;
1089
- if (!problem && settling?.completion && (binding ? result.status !== "completed" : !result.ok)) {
1090
- 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 });
1091
1096
  }
1092
1097
  break;
1093
1098
  }
1094
1099
  } catch (error) { problem = error; }
1095
1100
  ready.reject(problem || new Error("Claude work ended."));
1096
1101
  signal.removeEventListener("abort", abort);
1097
- if (binding) {
1102
+ if (bound) {
1098
1103
  try {
1099
1104
  if (problem || signal.aborted) await (cancellation || entry.nativeTurn.stopProcess(problem?.message || "Work stopped."));
1100
1105
  } catch (error) {
@@ -1126,10 +1131,10 @@ export function createClaudeConversationOwner({
1126
1131
  }
1127
1132
  if (control.current === active) control.current = null;
1128
1133
  if (problem) {
1129
- 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";
1130
1135
  throw problem;
1131
1136
  }
1132
- if (!binding) return { value: delivered.value };
1137
+ if (!bound) return { value: delivered.value };
1133
1138
  }
1134
1139
 
1135
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,
@@ -1138,7 +1143,7 @@ export function createClaudeConversationOwner({
1138
1143
  }
1139
1144
 
1140
1145
  /** Native turn handling moved from the production session provider; hosts persist its events. */
1141
- export function createClaudeConversationTurn({ conversationId, onEvent = async () => {}, process, entry } = {}) {
1146
+ export function createClaudeConversationTurn({ conversationId, onEvent = async () => {}, process, entry, bound = false } = {}) {
1142
1147
  if (!conversationId) throw new TypeError("A Claude conversation id is required.");
1143
1148
  const instructions = process ? createClaudeConversationAdapter(process) : null;
1144
1149
  const publish = entry ? publishEntryEvent : onEvent;
@@ -1249,31 +1254,30 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
1249
1254
  invalidateFinalAssistantResult();
1250
1255
  // Runtime bindings commit execution release before dropping the handle.
1251
1256
  // Retained host entries keep their original post-proof checkpoint order.
1252
- const binding = Boolean(process.releaseExecution);
1253
1257
  if (entry.stopPending) return entry.stopPending;
1254
1258
  const active = read().active;
1255
- if (!binding) entry.stopping = true;
1259
+ if (!bound) entry.stopping = true;
1256
1260
  const operation = async () => {
1257
1261
  const startup = entry.startupCleanup?.executionId === entry.executionId ? entry.startupCleanup : null;
1258
1262
  let proof;
1259
1263
  try {
1260
- if (!binding || entry.process || entry.executionId) {
1264
+ if (!bound || entry.process || entry.executionId) {
1261
1265
  proof = await stopClaudeCodeProcess({ process: entry.process || startup, executionId: entry.executionId,
1262
1266
  stopExecution: process.stopExecution });
1263
1267
  }
1264
1268
  } catch (error) {
1265
- if (binding || error.code !== "claude_stop_unconfirmed") throw error;
1269
+ if (bound || error.code !== "claude_stop_unconfirmed") throw error;
1266
1270
  entry.observationError = reason || "Claude process exit has not been confirmed. Try Stop again.";
1267
1271
  await onEvent({ type: "save" });
1268
1272
  await onEvent({ type: "stop-unconfirmed" });
1269
1273
  throw error;
1270
1274
  }
1271
- if (binding && (entry.process || entry.executionId)) await process.releaseExecution();
1275
+ if (bound && (entry.process || entry.executionId)) await process.releaseExecution();
1272
1276
  entry.process = null;
1273
1277
  entry.executionId = "";
1274
1278
  entry.exitProof = proof;
1275
- stop(binding ? reason : reason || "Claude stopped before acknowledging the prompt.");
1276
- if (binding) {
1279
+ stop(bound ? reason : reason || "Claude stopped before acknowledging the prompt.");
1280
+ if (bound) {
1277
1281
  if (entry.startupCleanup === startup) entry.startupCleanup = null;
1278
1282
  if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
1279
1283
  return;
@@ -1295,7 +1299,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
1295
1299
  if (entry.startupCleanup === startup) entry.startupCleanup = null;
1296
1300
  return entry.exitProof;
1297
1301
  };
1298
- 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; })
1299
1303
  .finally(() => { entry.stopPending = null; });
1300
1304
  return entry.stopPending;
1301
1305
  }
@@ -1303,9 +1307,8 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
1303
1307
  async function publishEntryEvent(event, deferred = false) {
1304
1308
  if (entry.stopping) return;
1305
1309
  if (event.type === "settled" && event.generation !== generation) return;
1306
- const binding = Boolean(process.releaseExecution);
1307
1310
  const command = entry.command;
1308
- if (event.type === "settled" && !binding && !deferred && command?.finishIfCurrent && command !== checkpointedCommand) {
1311
+ if (event.type === "settled" && !bound && !deferred && command?.finishIfCurrent && command !== checkpointedCommand) {
1309
1312
  const delivery = command.delivery;
1310
1313
  const completion = entry.completion;
1311
1314
  const owner = acceptedInput;
@@ -1336,17 +1339,17 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
1336
1339
  }
1337
1340
  if (event.type === "continued") {
1338
1341
  if (entry.completion === resolvedCompletion) entry.completion = Promise.withResolvers();
1339
- if (binding) setState("active");
1342
+ if (bound) setState("active");
1340
1343
  else await updateState("active");
1341
1344
  } else if (event.type === "admitted") {
1342
1345
  if (event.userEcho) entry.sent = true;
1343
- if (!binding) await onEvent({ type: "save" });
1346
+ if (!bound) await onEvent({ type: "save" });
1344
1347
  } else if (event.type === "phase") {
1345
1348
  entry.turn.phase = event.phase;
1346
- if (!binding) await updateState(entry.turn.state);
1349
+ if (!bound) await updateState(entry.turn.state);
1347
1350
  } else if (event.type === "settled") {
1348
1351
  const state = event.status === "interrupted" ? "interrupted" : event.status === "failed" ? "failed" : "completed";
1349
- if (binding) setState(state, event.error);
1352
+ if (bound) setState(state, event.error);
1350
1353
  else if (!await updateState(state, event.error, event.generation)) return;
1351
1354
  }
1352
1355
  const published = await onEvent(event);
@@ -1358,7 +1361,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
1358
1361
  if (event.type === "settled") {
1359
1362
  if (event.generation !== generation) return;
1360
1363
  if (event.source === "result") await onEvent({ type: "provider-update", change: "goal" });
1361
- resolveCompletion(binding ? event : readResult());
1364
+ resolveCompletion(bound ? event : readResult());
1362
1365
  }
1363
1366
  return published;
1364
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);