@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
@@ -10,11 +10,11 @@ import {
10
10
  codexAppServerRunInputSource
11
11
  } from "./codexTurnState.js";
12
12
  import crypto from "node:crypto";
13
- import { isPlainObject } from "./normalize.js";
13
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
14
14
  import {
15
+ codexAppServerAgentMessageText,
15
16
  codexAppServerAssistantItemText,
16
17
  classifyCodexAppServerEvent,
17
- codexAppServerContentText,
18
18
  codexAppServerNotificationEvent,
19
19
  codexAppServerNotificationEventType,
20
20
  codexAppServerNotificationEventPayload,
@@ -24,7 +24,7 @@ import {
24
24
  codexAppServerProviderThreadAssistantSegments
25
25
  } from "./codexEvents.js";
26
26
 
27
- // Original native output, reasoning persistence and exact-result identity.
27
+ // Native output, reasoning persistence and exact-result identity.
28
28
  // One instance belongs to each retained run owner; its state is shared by reference.
29
29
  export function createCodexAppServerOutput({
30
30
  namespace,
@@ -73,7 +73,7 @@ export function createCodexAppServerOutput({
73
73
  function codexAppServerLiveProgressCandidate(notification = {}) {
74
74
  const method = normalizeCodexRunText(notification.method);
75
75
  const event = codexAppServerNotificationEvent(notification);
76
- if (isPlainObject(event)) {
76
+ if (isRecord(event)) {
77
77
  const eventType = codexAppServerNotificationEventType(notification, event);
78
78
  const payload = codexAppServerNotificationEventPayload(notification, event);
79
79
  const payloadType = normalizeCodexRunText(payload.type);
@@ -83,11 +83,7 @@ export function createCodexAppServerOutput({
83
83
  explicit: Boolean(phase),
84
84
  phase,
85
85
  source: "event",
86
- text: normalizeCodexRunText(
87
- codexAppServerContentText(payload.message) ||
88
- codexAppServerContentText(payload.text) ||
89
- codexAppServerContentText(payload.content)
90
- )
86
+ text: codexAppServerAgentMessageText(payload)
91
87
  };
92
88
  }
93
89
  }
@@ -557,15 +553,14 @@ export function createCodexAppServerOutput({
557
553
  }
558
554
 
559
555
  function codexAppServerStreamMessage(classification) {
556
+ const messageId = codexAppServerConversationMessageId(
557
+ classification.threadId, classification.turnId, "assistant-item", classification.itemId
558
+ );
560
559
  return {
561
560
  nativeIdentity: { threadId: classification.threadId, turnId: classification.turnId },
562
561
  turnId: `${classification.threadId}:${classification.turnId}`,
563
- outputId: codexAppServerConversationMessageId(
564
- classification.threadId, classification.turnId, "assistant-item", classification.itemId
565
- ),
566
- messageId: codexAppServerConversationMessageId(
567
- classification.threadId, classification.turnId, "assistant-item", classification.itemId
568
- ),
562
+ outputId: messageId,
563
+ messageId,
569
564
  role: classification.role,
570
565
  delta: classification.delta
571
566
  };
@@ -601,11 +596,9 @@ export function createCodexAppServerOutput({
601
596
  const store = await createStore(normalizedSessionId);
602
597
  const run = await readCodexAppServerAgentRunForSession(store, normalizedSessionId);
603
598
  const turn = turnStateFromAgentRun(run || {});
604
- if (!codexAppServerTurnCanReceiveProviderActivity(
605
- turn,
606
- normalizedThreadId,
607
- codexAppServerNotificationTurnId(notification)
608
- )) {
599
+ const notificationTurnId = codexAppServerNotificationTurnId(notification);
600
+ const ownerTurnId = notificationTurnId || turn.turnId;
601
+ if (!codexAppServerTurnCanReceiveProviderActivity(turn, normalizedThreadId, notificationTurnId)) {
609
602
  return null;
610
603
  }
611
604
  const key = codexAppServerLiveProgressKey(normalizedSessionId, normalizedThreadId, notification);
@@ -615,11 +608,11 @@ export function createCodexAppServerOutput({
615
608
  const fingerprintKey = codexAppServerLiveProgressFingerprintKey(
616
609
  normalizedSessionId,
617
610
  normalizedThreadId,
618
- codexAppServerNotificationTurnId(notification) || turn.turnId,
611
+ ownerTurnId,
619
612
  text
620
613
  );
621
614
  const streamMessageId = codexAppServerConversationMessageId(
622
- normalizedThreadId, codexAppServerNotificationTurnId(notification),
615
+ normalizedThreadId, notificationTurnId,
623
616
  "assistant-item", codexAppServerNotificationItemId(notification)
624
617
  );
625
618
  if (
@@ -645,12 +638,12 @@ export function createCodexAppServerOutput({
645
638
  ...(streamMessageId ? { outputId: streamMessageId } : {}),
646
639
  messageId: codexAppServerConversationMessageId(
647
640
  normalizedThreadId,
648
- codexAppServerNotificationTurnId(notification) || turn.turnId,
641
+ ownerTurnId,
649
642
  role,
650
643
  text
651
644
  ),
652
645
  requireOpenTurn: false,
653
- nativeIdentity: { threadId: normalizedThreadId, turnId: codexAppServerNotificationTurnId(notification) || turn.turnId },
646
+ nativeIdentity: { threadId: normalizedThreadId, turnId: ownerTurnId },
654
647
  text
655
648
  });
656
649
  if (!written) {
@@ -660,7 +653,7 @@ export function createCodexAppServerOutput({
660
653
  }
661
654
  const payload = {
662
655
  conversationStream: await store.completeConversationStreamMessage(normalizedSessionId, streamMessageId, {
663
- text, role, nativeIdentity: { threadId: normalizedThreadId, turnId: codexAppServerNotificationTurnId(notification) || turn.turnId }
656
+ text, role, nativeIdentity: { threadId: normalizedThreadId, turnId: ownerTurnId }
664
657
  })
665
658
  };
666
659
  if (written) {
@@ -794,7 +787,7 @@ export function createCodexAppServerOutput({
794
787
 
795
788
  function codexAppServerSnapshotCursor(run = {}) {
796
789
  const cursor = run?.providerSnapshotCursor;
797
- if (!isPlainObject(cursor)) {
790
+ if (!isRecord(cursor)) {
798
791
  return {
799
792
  itemId: "",
800
793
  turnId: ""
@@ -847,7 +840,7 @@ export function createCodexAppServerOutput({
847
840
  const normalizedThreadId = normalizeCodexRunText(threadId);
848
841
  const normalizedTurnId = normalizeCodexRunText(observedTurn?.id);
849
842
  const items = (Array.isArray(observedTurn?.items) ? observedTurn.items : [])
850
- .filter((item) => isPlainObject(item) && normalizeCodexRunText(item.id));
843
+ .filter((item) => isRecord(item) && normalizeCodexRunText(item.id));
851
844
  if (!normalizedSessionId || !normalizedThreadId || !normalizedTurnId || !items.length) {
852
845
  return;
853
846
  }
@@ -8,8 +8,9 @@ import path from "node:path";
8
8
  import { setTimeout as delay } from "node:timers/promises";
9
9
  import { CodexAppServerJsonRpcClient, socketPathFromCodexAppServerEndpoint } from "./codexClient.js";
10
10
  import { createConversationProcessIdentity } from "./localExecution.js";
11
- import { isPlainObject, normalizeText } from "./normalize.js";
12
- import { prepareCodexModelCatalog } from "./codexConfiguration.js";
11
+ import { isRecord, normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
12
+ import { normalizeNullableText } from "./normalize.js";
13
+ import { copyCodexProviderHome, prepareCodexModelCatalog } from "./codexConfiguration.js";
13
14
  import { startCodexHistoryAdapter } from "./codexHistoryAdapter.js";
14
15
 
15
16
  const CODEX_APP_SERVER_METADATA_FILE = "runtime.json";
@@ -100,34 +101,34 @@ function codexAppServerLockDir(runtimeDir = "") {
100
101
  }
101
102
 
102
103
  export function normalizeCodexAppServerMetadata(metadata = {}) {
103
- const normalized = isPlainObject(metadata) ? metadata : {};
104
- const endpoint = normalizeText(normalized.endpoint);
104
+ const normalized = isRecord(metadata) ? metadata : {};
105
+ const endpoint = normalizeNullableText(normalized.endpoint);
105
106
  return {
106
- accountIdentitySignature: normalizeText(normalized.accountIdentitySignature),
107
- attachmentHostRoot: normalizeText(normalized.attachmentHostRoot),
108
- authStateSignature: normalizeText(normalized.authStateSignature),
107
+ accountIdentitySignature: normalizeNullableText(normalized.accountIdentitySignature),
108
+ attachmentHostRoot: normalizeNullableText(normalized.attachmentHostRoot),
109
+ authStateSignature: normalizeNullableText(normalized.authStateSignature),
109
110
  endpoint,
110
- executionId: normalizeText(normalized.executionId),
111
- executionMode: normalizeText(normalized.executionMode) || CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE,
112
- executionContextHash: normalizeText(normalized.executionContextHash),
113
- healthz: normalizeText(normalized.healthz),
114
- historyAdapterBaseUrl: normalizeText(normalized.historyAdapterBaseUrl),
115
- logPath: normalizeText(normalized.logPath),
111
+ executionId: normalizeNullableText(normalized.executionId),
112
+ executionMode: normalizeNullableText(normalized.executionMode) || CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE,
113
+ executionContextHash: normalizeNullableText(normalized.executionContextHash),
114
+ healthz: normalizeNullableText(normalized.healthz),
115
+ historyAdapterBaseUrl: normalizeNullableText(normalized.historyAdapterBaseUrl),
116
+ logPath: normalizeNullableText(normalized.logPath),
116
117
  pid: Number.isSafeInteger(Number(normalized.pid)) ? Number(normalized.pid) : null,
117
- processCwd: normalizeText(normalized.processCwd),
118
- processExitVerifiedAt: normalizeText(normalized.processExitVerifiedAt),
118
+ processCwd: normalizeNullableText(normalized.processCwd),
119
+ processExitVerifiedAt: normalizeNullableText(normalized.processExitVerifiedAt),
119
120
  processIdentity: runtimeProcessIdentity.normalize(normalized.processIdentity),
120
- processState: normalizeText(normalized.processState),
121
- provider: normalizeText(normalized.provider),
122
- readyz: normalizeText(normalized.readyz),
123
- runtimeDir: normalizeText(normalized.runtimeDir),
124
- runtimesHash: normalizeText(normalized.runtimesHash),
121
+ processState: normalizeNullableText(normalized.processState),
122
+ provider: normalizeNullableText(normalized.provider),
123
+ readyz: normalizeNullableText(normalized.readyz),
124
+ runtimeDir: normalizeNullableText(normalized.runtimeDir),
125
+ runtimesHash: normalizeNullableText(normalized.runtimesHash),
125
126
  schemaVersion: Number(normalized.schemaVersion || 0),
126
- socketPath: normalizeText(normalized.socketPath),
127
- startedAt: normalizeText(normalized.startedAt),
128
- terminalEnvHash: normalizeText(normalized.terminalEnvHash),
129
- toolHomeSource: normalizeText(normalized.toolHomeSource),
130
- transport: normalizeText(normalized.transport)
127
+ socketPath: normalizeNullableText(normalized.socketPath),
128
+ startedAt: normalizeNullableText(normalized.startedAt),
129
+ terminalEnvHash: normalizeNullableText(normalized.terminalEnvHash),
130
+ toolHomeSource: normalizeNullableText(normalized.toolHomeSource),
131
+ transport: normalizeNullableText(normalized.transport)
131
132
  };
132
133
  }
133
134
 
@@ -249,7 +250,7 @@ const CODEX_APP_SERVER_READY_TIMEOUT_MS = 60000;
249
250
 
250
251
  const CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS = 2000;
251
252
 
252
- const CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES = new Set([
253
+ export const CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES = new Set([
253
254
  "DBUS_SESSION_BUS_ADDRESS",
254
255
  "DBUS_STARTER_ADDRESS",
255
256
  "DBUS_STARTER_BUS_TYPE"
@@ -289,11 +290,6 @@ const CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS = Object.freeze({
289
290
  MISMATCH: "mismatch"
290
291
  });
291
292
 
292
- function normalizePositiveInteger(value, fallback) {
293
- const number = Number(value);
294
- return Number.isSafeInteger(number) && number > 0 ? number : fallback;
295
- }
296
-
297
293
  function stableHash(value) {
298
294
  return createHash("sha256")
299
295
  .update(String(value || ""))
@@ -302,7 +298,7 @@ function stableHash(value) {
302
298
  }
303
299
 
304
300
  export function codexAppServerExecutionMode(options = {}) {
305
- return normalizeText(options.executionMode) === CODEX_APP_SERVER_EXECUTION_MODES.HELPER
301
+ return normalizeNullableText(options.executionMode) === CODEX_APP_SERVER_EXECUTION_MODES.HELPER
306
302
  ? CODEX_APP_SERVER_EXECUTION_MODES.HELPER
307
303
  : CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE;
308
304
  }
@@ -312,7 +308,7 @@ export function codexAppServerIsHelper(options = {}) {
312
308
  }
313
309
 
314
310
  export async function assertExistingDirectory(dirPath = "", label = "directory") {
315
- const normalizedPath = normalizeText(dirPath);
311
+ const normalizedPath = normalizeNullableText(dirPath);
316
312
  if (!normalizedPath) {
317
313
  return;
318
314
  }
@@ -331,7 +327,7 @@ export function codexAppServerHelperWorkspaceDir(runtimeDir = "") {
331
327
  }
332
328
 
333
329
  function codexAppServerHelperCommandBaseEnv(env = process.env, codexHome = "") {
334
- const source = isPlainObject(env) ? env : {};
330
+ const source = isRecord(env) ? env : {};
335
331
  const cleared = Object.fromEntries(Object.keys({
336
332
  ...process.env,
337
333
  ...source
@@ -377,7 +373,7 @@ function codexAppServerUnixEndpoint(socketPath = "") {
377
373
  }
378
374
 
379
375
  function codexAppServerProjectTrustOverride(workdir = "") {
380
- const normalizedWorkdir = normalizeText(workdir);
376
+ const normalizedWorkdir = normalizeNullableText(workdir);
381
377
  if (!normalizedWorkdir) {
382
378
  return "";
383
379
  }
@@ -390,19 +386,19 @@ export function codexAppServerProcessCwd({
390
386
  runtimeDir = "",
391
387
  workdir = ""
392
388
  } = {}) {
393
- const normalizedWorkdir = normalizeText(workdir) ? path.resolve(workdir) : "";
389
+ const normalizedWorkdir = normalizeNullableText(workdir) ? path.resolve(workdir) : "";
394
390
  if (normalizedWorkdir) {
395
391
  return normalizedWorkdir;
396
392
  }
397
- const normalizedExecutionRoot = normalizeText(executionRoot) ? path.resolve(executionRoot) : "";
393
+ const normalizedExecutionRoot = normalizeNullableText(executionRoot) ? path.resolve(executionRoot) : "";
398
394
  if (normalizedExecutionRoot) {
399
395
  return normalizedExecutionRoot;
400
396
  }
401
- return normalizeText(runtimeDir) ? path.resolve(runtimeDir) : "";
397
+ return normalizeNullableText(runtimeDir) ? path.resolve(runtimeDir) : "";
402
398
  }
403
399
 
404
400
  function codexAppServerRuntimeDirIsManaged(runtimeDir = "") {
405
- const normalizedRuntimeDir = normalizeText(runtimeDir);
401
+ const normalizedRuntimeDir = normalizeNullableText(runtimeDir);
406
402
  if (!normalizedRuntimeDir) {
407
403
  return false;
408
404
  }
@@ -412,7 +408,7 @@ function codexAppServerRuntimeDirIsManaged(runtimeDir = "") {
412
408
  }
413
409
 
414
410
  async function removeCodexAppServerRuntimeDir(runtimeDir = "") {
415
- const normalizedRuntimeDir = normalizeText(runtimeDir);
411
+ const normalizedRuntimeDir = normalizeNullableText(runtimeDir);
416
412
  if (!codexAppServerRuntimeDirIsManaged(normalizedRuntimeDir)) {
417
413
  return false;
418
414
  }
@@ -441,26 +437,26 @@ function codexAppServerRuntimeCleanupCanSkip(error) {
441
437
 
442
438
  export function codexAppServerRuntimeIdentity(runtime = {}) {
443
439
  return [
444
- normalizeText(runtime.accountIdentitySignature),
445
- normalizeText(runtime.authStateSignature),
446
- normalizeText(runtime.endpoint),
447
- normalizeText(runtime.executionId),
448
- normalizeText(runtime.executionMode),
449
- normalizeText(runtime.runtimesHash),
450
- normalizeText(runtime.terminalEnvHash),
451
- normalizeText(runtime.socketPath),
452
- normalizeText(runtime.startedAt),
453
- normalizeText(runtime.pid)
440
+ normalizeNullableText(runtime.accountIdentitySignature),
441
+ normalizeNullableText(runtime.authStateSignature),
442
+ normalizeNullableText(runtime.endpoint),
443
+ normalizeNullableText(runtime.executionId),
444
+ normalizeNullableText(runtime.executionMode),
445
+ normalizeNullableText(runtime.runtimesHash),
446
+ normalizeNullableText(runtime.terminalEnvHash),
447
+ normalizeNullableText(runtime.socketPath),
448
+ normalizeNullableText(runtime.startedAt),
449
+ normalizeNullableText(runtime.pid)
454
450
  ].join("\0");
455
451
  }
456
452
 
457
453
  export function normalizeCodexAppServerTerminalEnv(terminalEnv = {}) {
458
- if (!isPlainObject(terminalEnv)) {
454
+ if (!isRecord(terminalEnv)) {
459
455
  return {};
460
456
  }
461
457
  return Object.fromEntries(Object.entries(terminalEnv)
462
458
  .map(([name, value]) => [
463
- normalizeText(name),
459
+ normalizeNullableText(name),
464
460
  String(value ?? "")
465
461
  ])
466
462
  .filter(([name, value]) => (
@@ -485,13 +481,13 @@ export function codexAppServerCommandBaseEnv({
485
481
  }
486
482
 
487
483
  export function codexAppServerCredentialHome(toolHomeSource = "", baseEnv = {}) {
488
- const home = normalizeText(toolHomeSource);
484
+ const home = normalizeNullableText(toolHomeSource);
489
485
  if (!home) {
490
486
  return {};
491
487
  }
492
488
  return {
493
489
  home,
494
- username: normalizeText(baseEnv.USER || baseEnv.LOGNAME)
490
+ username: normalizeNullableText(baseEnv.USER || baseEnv.LOGNAME)
495
491
  };
496
492
  }
497
493
 
@@ -501,8 +497,9 @@ export function codexAppServerTerminalEnvHash(terminalEnv = {}) {
501
497
  }
502
498
 
503
499
  function codexHistoryAdapterUrlIsValid(baseUrl, token) {
504
- // The previous adapter cannot serve thread-bound compaction recovery. Keep
505
- // its process identity readable, but retire it through normal owned cleanup.
500
+ // Only this adapter URL shape serves thread-bound compaction recovery. Any
501
+ // other shape keeps its process identity readable but is retired through
502
+ // normal owned cleanup.
506
503
  const match = typeof baseUrl === "string" && /^http:\/\/127\.0\.0\.1:([1-9]\d{0,4})\/[0-9a-f-]{36}\/v2$/iu.exec(baseUrl);
507
504
  return Boolean(match && Number(match[1]) <= 65535 && baseUrl.endsWith(`/${token}/v2`));
508
505
  }
@@ -510,7 +507,7 @@ function codexHistoryAdapterUrlIsValid(baseUrl, token) {
510
507
  function codexAppServerLivenessTimeoutMs(options = {}) {
511
508
  return normalizePositiveInteger(
512
509
  options.livenessTimeoutMs,
513
- normalizePositiveInteger(options.timeoutMs, CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS)
510
+ { fallback: normalizePositiveInteger(options.timeoutMs, { fallback: CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS }) }
514
511
  );
515
512
  }
516
513
 
@@ -523,10 +520,10 @@ function codexAppServerProcessCommandHash({
523
520
  } = {}) {
524
521
  return stableHash(JSON.stringify({
525
522
  codexArgs,
526
- codexCommand: normalizeText(codexCommand),
523
+ codexCommand: normalizeNullableText(codexCommand),
527
524
  executionMode: codexAppServerExecutionMode({ executionMode }),
528
- processCwd: normalizeText(processCwd),
529
- runtimeDir: normalizeText(runtimeDir)
525
+ processCwd: normalizeNullableText(processCwd),
526
+ runtimeDir: normalizeNullableText(runtimeDir)
530
527
  }));
531
528
  }
532
529
 
@@ -535,16 +532,39 @@ const codexAppServerRuntimePreparations = new Map();
535
532
  /** The production shared runtime: one process owner per stable runtime directory.
536
533
  * Execution and credential facilities retain host policy; threads do not own this process.
537
534
  */
535
+ /** Default environment names the runtime and its process leader share. */
536
+ export const CODEX_APP_SERVER_DEFAULT_ENVIRONMENT = Object.freeze({
537
+ runtimeToken: "JSKIT_EXECUTION_RUNTIME_TOKEN",
538
+ modelCatalogSource: "JSKIT_CODEX_MODEL_CATALOG_SOURCE",
539
+ commandHash: "JSKIT_EXECUTION_COMMAND_HASH"
540
+ });
541
+
542
+ /** The PreToolUse hook that routes native Bash commands through the host's wrapper. */
543
+ export function codexCommandHookDefinition(command) {
544
+ return [{ matcher: "^Bash$", hooks: [{ type: "command", command, timeout: 30 }] }];
545
+ }
546
+
547
+ // Inline TOML for `-c` overrides: bare keys, JSON-quoted strings.
548
+ function tomlInlineValue(value) {
549
+ if (Array.isArray(value)) {
550
+ return `[${value.map(tomlInlineValue).join(",")}]`;
551
+ }
552
+ if (isRecord(value)) {
553
+ return `{${Object.entries(value).map(([key, item]) => `${key}=${tomlInlineValue(item)}`).join(",")}}`;
554
+ }
555
+ return JSON.stringify(value);
556
+ }
557
+
538
558
  export function createCodexAppServerRuntime({ execution, credentials, configuration = {} }) {
539
559
  const {
540
560
  clientInfo = { name: "jskit", title: "JSKIT", version: "0.1.0" },
541
561
  commandHookCommand,
542
562
  errorPrefix = "",
543
- modelCatalogEnvironmentName = "JSKIT_CODEX_MODEL_CATALOG_SOURCE",
563
+ modelCatalogEnvironmentName = CODEX_APP_SERVER_DEFAULT_ENVIRONMENT.modelCatalogSource,
544
564
  processLabel = "jskit-codex-app-server",
545
565
  processPath = fileURLToPath(new URL("./codexProcessLeader.js", import.meta.url)),
546
- runtimeTokenEnvironmentName = "JSKIT_EXECUTION_RUNTIME_TOKEN",
547
- commandHashEnvironmentName = "JSKIT_EXECUTION_COMMAND_HASH",
566
+ runtimeTokenEnvironmentName = CODEX_APP_SERVER_DEFAULT_ENVIRONMENT.runtimeToken,
567
+ commandHashEnvironmentName = CODEX_APP_SERVER_DEFAULT_ENVIRONMENT.commandHash,
548
568
  socketPathHelp = "Configure a shorter host runtime directory.",
549
569
  umask = "0007"
550
570
  } = configuration;
@@ -563,8 +583,7 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
563
583
  const assertCodexAuthGenerationCurrent = credentials.assertCurrent;
564
584
 
565
585
  function codexAppServerSessionCommandHookConfig() {
566
- const command = commandHookCommand;
567
- return `hooks.PreToolUse=[{matcher="^Bash$",hooks=[{type="command",command=${JSON.stringify(command)},timeout=30}]}]`;
586
+ return `hooks.PreToolUse=${tomlInlineValue(codexCommandHookDefinition(commandHookCommand))}`;
568
587
  }
569
588
 
570
589
  async function acquireRuntimeLock(runtimeDir = "", options = {}) {
@@ -573,7 +592,7 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
573
592
  }
574
593
 
575
594
  // An empty or null umask inherits the host's own; hosts that share files set one.
576
- const startupUmask = normalizeText(umask);
595
+ const startupUmask = normalizeNullableText(umask);
577
596
  if (startupUmask && !/^[0-7]{3,4}$/u.test(startupUmask)) {
578
597
  throw new TypeError(`Codex app-server umask must be octal, not ${JSON.stringify(startupUmask)}.`);
579
598
  }
@@ -598,7 +617,7 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
598
617
  ].join("\n");
599
618
 
600
619
  function codexAppServerProcessMetadataIsIdentifiable(metadata = {}, runtimeDir = "") {
601
- const normalizedRuntimeDir = normalizeText(runtimeDir);
620
+ const normalizedRuntimeDir = normalizeNullableText(runtimeDir);
602
621
  return Boolean(
603
622
  Number(metadata.schemaVersion) === CODEX_APP_SERVER_METADATA_SCHEMA_VERSION &&
604
623
  metadata.provider === CODEX_APP_SERVER_PROVIDER_ID &&
@@ -654,7 +673,7 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
654
673
  ) {
655
674
  return stopCodexAppServerProcessGroup(metadata, options);
656
675
  }
657
- const executionId = normalizeText(metadata.executionId);
676
+ const executionId = normalizeNullableText(metadata.executionId);
658
677
  if (executionId) {
659
678
  const stopExecution = typeof options.stopExecution === "function"
660
679
  ? options.stopExecution
@@ -698,7 +717,7 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
698
717
  }
699
718
 
700
719
  async function stopCodexAppServerProcess(runtimeDir = "", options = {}) {
701
- const normalizedRuntimeDir = normalizeText(runtimeDir);
720
+ const normalizedRuntimeDir = normalizeNullableText(runtimeDir);
702
721
  if (!normalizedRuntimeDir) {
703
722
  return {
704
723
  processExitVerified: false,
@@ -732,12 +751,12 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
732
751
  if (!neverStarted) {
733
752
  try {
734
753
  processStop = await stopOwnedCodexAppServerExecution({
735
- executionId: normalizeText(executionId),
754
+ executionId: normalizeNullableText(executionId),
736
755
  pid: Number(pid),
737
756
  processIdentity,
738
757
  processState: CODEX_APP_SERVER_PROCESS_STATE.RUNNING,
739
758
  provider: CODEX_APP_SERVER_PROVIDER_ID,
740
- runtimeDir: normalizeText(runtimeDir),
759
+ runtimeDir: normalizeNullableText(runtimeDir),
741
760
  schemaVersion: CODEX_APP_SERVER_METADATA_SCHEMA_VERSION
742
761
  }, options);
743
762
  } catch {
@@ -770,7 +789,7 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
770
789
  }
771
790
 
772
791
  async function stopCodexAppServerRuntime(options = {}) {
773
- const runtimeDir = normalizeText(options.runtimeDir);
792
+ const runtimeDir = normalizeNullableText(options.runtimeDir);
774
793
  const preserveProcessExitProof = options.preserveProcessExitProof === true;
775
794
  if (!runtimeDir || !codexAppServerRuntimeDirIsManaged(runtimeDir)) {
776
795
  return {
@@ -806,7 +825,7 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
806
825
  };
807
826
  try {
808
827
  const existing = await readCodexAppServerMetadata(runtimeDir);
809
- const expectedAccount = normalizeText(options.expectedAccountIdentitySignature);
828
+ const expectedAccount = normalizeNullableText(options.expectedAccountIdentitySignature);
810
829
  const ownedRuntime = options.ownedRuntime;
811
830
  if (
812
831
  ownedRuntime &&
@@ -831,7 +850,7 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
831
850
  ? await execution.stopOwned({
832
851
  kind: "assistant",
833
852
  operationId: "codex-app-server",
834
- ownerId: normalizeText(options.ownerId) || stableHash(runtimeDir)
853
+ ownerId: normalizeNullableText(options.ownerId) || stableHash(runtimeDir)
835
854
  }, { reason: "codex-app-server-changeover" })
836
855
  : null;
837
856
  const ownerExitVerified = owned?.supported === true && owned?.ok === true && owned?.scopeEmpty === true;
@@ -896,9 +915,9 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
896
915
 
897
916
  function codexAppServerMetadataIsWellFormed(metadata = {}, options = {}) {
898
917
  const expectedAttachmentHostRoot = options.compatibility.attachmentHostRoot;
899
- const expectedAccountIdentitySignature = normalizeText(options.accountIdentitySignature);
918
+ const expectedAccountIdentitySignature = normalizeNullableText(options.accountIdentitySignature);
900
919
  const expectedExecutionMode = codexAppServerExecutionMode(options);
901
- const expectedToolHomeSource = codexAppServerIsHelper(options) ? "" : normalizeText(options.toolHomeSource);
920
+ const expectedToolHomeSource = codexAppServerIsHelper(options) ? "" : normalizeNullableText(options.toolHomeSource);
902
921
  const expectedTerminalEnvHash = codexAppServerTerminalEnvHash(codexAppServerIsHelper(options) ? {} : options.terminalEnv);
903
922
  const expectedRuntimesHash = options.compatibility.runtimesHash;
904
923
  const expectedExecutionContextHash = options.compatibility.executionContextHash;
@@ -933,41 +952,43 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
933
952
  timeoutMs = CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS,
934
953
  WebSocketImpl = WebSocket
935
954
  } = {}) {
936
- const normalizedEndpoint = normalizeText(endpoint);
955
+ const normalizedEndpoint = normalizeNullableText(endpoint);
937
956
  const socketPath = socketPathFromCodexAppServerEndpoint(normalizedEndpoint);
938
957
  if (!socketPath || !await fileExists(socketPath)) {
939
958
  return {
940
959
  status: CODEX_APP_SERVER_ENDPOINT_STATUS.MISSING
941
960
  };
942
961
  }
943
- const normalizedTimeoutMs = normalizePositiveInteger(timeoutMs, CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS);
962
+ const normalizedTimeoutMs = normalizePositiveInteger(timeoutMs, { fallback: CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS });
963
+ return {
964
+ status: await probeCodexAppServer(normalizedEndpoint, normalizedTimeoutMs, WebSocketImpl)
965
+ };
966
+ }
967
+
968
+ // One bounded connect-and-initialize handshake: responsive, timeout or unreachable.
969
+ async function probeCodexAppServer(endpoint, timeoutMs, WebSocketImpl) {
944
970
  const client = new CodexAppServerJsonRpcClient({
945
- endpoint: normalizedEndpoint,
946
- requestTimeoutMs: normalizedTimeoutMs,
971
+ endpoint,
972
+ requestTimeoutMs: timeoutMs,
947
973
  WebSocketImpl
948
974
  });
949
975
  let timeout = null;
950
976
  const probe = (async () => {
951
977
  await client.connect();
952
- await client.initialize({ clientInfo: clientInfo });
978
+ await client.initialize({ clientInfo });
953
979
  return CODEX_APP_SERVER_ENDPOINT_STATUS.RESPONSIVE;
954
980
  })();
955
981
  probe.catch(() => null);
956
982
  try {
957
- const status = await Promise.race([
983
+ return await Promise.race([
958
984
  probe,
959
985
  new Promise((resolve) => {
960
- timeout = setTimeout(() => resolve(CODEX_APP_SERVER_ENDPOINT_STATUS.TIMEOUT), normalizedTimeoutMs);
986
+ timeout = setTimeout(() => resolve(CODEX_APP_SERVER_ENDPOINT_STATUS.TIMEOUT), timeoutMs);
961
987
  timeout.unref?.();
962
988
  })
963
989
  ]);
964
- return {
965
- status
966
- };
967
990
  } catch {
968
- return {
969
- status: CODEX_APP_SERVER_ENDPOINT_STATUS.UNREACHABLE
970
- };
991
+ return CODEX_APP_SERVER_ENDPOINT_STATUS.UNREACHABLE;
971
992
  } finally {
972
993
  clearTimeout(timeout);
973
994
  client.close();
@@ -1089,35 +1110,10 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
1089
1110
  await delay(100);
1090
1111
  }
1091
1112
  const remainingMs = Math.max(1, timeoutMs - (Date.now() - startedAt));
1092
- if (!await fileExists(socketPath) || remainingMs <= 0) {
1093
- return false;
1094
- }
1095
- const client = new CodexAppServerJsonRpcClient({
1096
- endpoint,
1097
- requestTimeoutMs: remainingMs,
1098
- WebSocketImpl
1099
- });
1100
- let timeout = null;
1101
- const handshake = (async () => {
1102
- await client.connect();
1103
- await client.initialize({ clientInfo: clientInfo });
1104
- return true;
1105
- })();
1106
- handshake.catch(() => null);
1107
- try {
1108
- return await Promise.race([
1109
- handshake,
1110
- new Promise((resolve) => {
1111
- timeout = setTimeout(() => resolve(false), remainingMs);
1112
- timeout.unref?.();
1113
- })
1114
- ]);
1115
- } catch {
1113
+ if (!await fileExists(socketPath)) {
1116
1114
  return false;
1117
- } finally {
1118
- clearTimeout(timeout);
1119
- client.close();
1120
1115
  }
1116
+ return await probeCodexAppServer(endpoint, remainingMs, WebSocketImpl) === CODEX_APP_SERVER_ENDPOINT_STATUS.RESPONSIVE;
1121
1117
  }
1122
1118
 
1123
1119
  async function startCodexAppServerProcess({
@@ -1153,7 +1149,7 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
1153
1149
  await ensureWritablePrivateDirectory(runtimeDir);
1154
1150
  const helper = codexAppServerExecutionMode({ executionMode }) ===
1155
1151
  CODEX_APP_SERVER_EXECUTION_MODES.HELPER;
1156
- const normalizedToolHomeSource = normalizeText(toolHomeSource);
1152
+ const normalizedToolHomeSource = normalizeNullableText(toolHomeSource);
1157
1153
  if (normalizedToolHomeSource) {
1158
1154
  await assertExistingDirectory(normalizedToolHomeSource, "Codex credential home");
1159
1155
  }
@@ -1177,13 +1173,13 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
1177
1173
  if (providerHome) {
1178
1174
  // The canonical home contains only our curated provider projection. Copy
1179
1175
  // it into the existing isolated helper home; never load OpenAI auth here.
1180
- const sourceHome = path.join(normalizedToolHomeSource, ".codex");
1181
- const catalogPath = path.join(helperHome, "models.json");
1182
- const config = await readFile(path.join(sourceHome, "config.toml"), "utf8");
1183
- await writeFile(catalogPath, await readFile(path.join(sourceHome, "models.json")), { mode: 0o600 });
1184
- await writeFile(path.join(helperHome, "config.toml"), config.replace(
1185
- JSON.stringify(path.join(sourceHome, "models.json")), JSON.stringify(catalogPath)
1186
- ), { mode: 0o600 });
1176
+ try {
1177
+ await copyCodexProviderHome(path.join(normalizedToolHomeSource, ".codex"), helperHome);
1178
+ } catch (cause) {
1179
+ const error = new Error("Codex isolated helper runtime could not copy the provider home.", { cause });
1180
+ error.code = `${errorPrefix}codex_helper_runtime_start_failed`;
1181
+ throw error;
1182
+ }
1187
1183
  }
1188
1184
  }
1189
1185
  const processCwd = helper
@@ -1280,7 +1276,7 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
1280
1276
  label: "Codex assistant",
1281
1277
  lifecycle: "service",
1282
1278
  operationId: "codex-app-server",
1283
- ownerId: normalizeText(ownerId) || stableHash(runtimeDir)
1279
+ ownerId: normalizeNullableText(ownerId) || stableHash(runtimeDir)
1284
1280
  },
1285
1281
  processProfile: profileBinary?.isFile() ? {
1286
1282
  command: codexCommand, helper, executionMode,
@@ -1413,11 +1409,11 @@ export function createCodexAppServerRuntime({ execution, credentials, configurat
1413
1409
  }
1414
1410
 
1415
1411
  return {
1416
- accountIdentitySignature: normalizeText(accountIdentitySignature),
1412
+ accountIdentitySignature: normalizeNullableText(accountIdentitySignature),
1417
1413
  attachmentHostRoot: compatibility.attachmentHostRoot,
1418
1414
  authStateSignature: resolvedAuthStateSignature,
1419
1415
  endpoint,
1420
- executionId: normalizeText(startResult.execution?.id),
1416
+ executionId: normalizeNullableText(startResult.execution?.id),
1421
1417
  executionMode: helper
1422
1418
  ? CODEX_APP_SERVER_EXECUTION_MODES.HELPER
1423
1419
  : CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE,