@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.
- package/examples/conversation/package.json +5 -5
- package/package.json +5 -5
- package/src/client/conversation/AssistantTranscript.vue +26 -29
- package/src/client/conversation/LongTextPreviewBlocks.vue +11 -0
- package/src/client/conversation/messageDelivery.js +8 -2
- package/src/client/conversation/useAssistantAttachments.js +1 -1
- package/src/server/conversation/agentRun.js +12 -198
- package/src/server/conversation/attachments.js +6 -5
- package/src/server/conversation/boundedToolLoop.js +160 -0
- package/src/server/conversation/claudeHistory.js +20 -16
- package/src/server/conversation/claudeProcess.js +2 -2
- package/src/server/conversation/claudeTurn.js +41 -38
- package/src/server/conversation/codexAuthPreflight.js +104 -0
- package/src/server/conversation/codexClient.js +11 -16
- package/src/server/conversation/codexConfiguration.js +112 -98
- package/src/server/conversation/codexConversationStore.js +189 -0
- package/src/server/conversation/codexConversations.js +47 -44
- package/src/server/conversation/codexDetachedTurn.js +12 -11
- package/src/server/conversation/codexEvents.js +95 -99
- package/src/server/conversation/codexHelperExecution.js +3 -13
- package/src/server/conversation/codexHelperThreadLedger.js +7 -13
- package/src/server/conversation/codexHelperThreadRecovery.js +2 -16
- package/src/server/conversation/codexHelperThreads.js +5 -6
- package/src/server/conversation/codexHistoryAdapter.js +32 -26
- package/src/server/conversation/codexMessageCommands.js +22 -22
- package/src/server/conversation/codexMessageDelivery.js +12 -7
- package/src/server/conversation/codexModelCatalog.js +97 -0
- package/src/server/conversation/codexNotificationObserver.js +24 -40
- package/src/server/conversation/codexNotificationQueue.js +2 -3
- package/src/server/conversation/codexOutput.js +20 -27
- package/src/server/conversation/codexProcess.js +123 -127
- package/src/server/conversation/codexProcessLeader.js +3 -3
- package/src/server/conversation/codexProvider.js +31 -614
- package/src/server/conversation/codexProviderOwner.js +2 -2
- package/src/server/conversation/codexProviderSelection.js +38 -40
- package/src/server/conversation/codexRenewalCommands.js +13 -10
- package/src/server/conversation/codexRenewalThreads.js +412 -0
- package/src/server/conversation/codexRunJournal.js +24 -42
- package/src/server/conversation/codexRuntimeLifecycle.js +19 -15
- package/src/server/conversation/codexSessionCleanup.js +11 -10
- package/src/server/conversation/codexSessionReconciliation.js +6 -6
- package/src/server/conversation/codexThreadReadiness.js +19 -33
- package/src/server/conversation/codexThreadStatus.js +3 -3
- package/src/server/conversation/codexTurn.js +124 -297
- package/src/server/conversation/codexTurnRecovery.js +49 -28
- package/src/server/conversation/codexTurnState.js +34 -13
- package/src/server/conversation/commandWrapper.js +3 -0
- package/src/server/conversation/deliveryTiming.js +14 -5
- package/src/server/conversation/index.js +1 -0
- package/src/server/conversation/localExecution.js +13 -17
- package/src/server/conversation/normalize.js +8 -3
- package/src/server/conversation/openCodeClient.js +9 -10
- package/src/server/conversation/openCodeProcess.js +9 -5
- package/src/server/conversation/openCodeRuntime.js +10 -11
- package/src/server/conversation/openCodeTurn.js +43 -29
- package/src/server/conversation/providers/claudeDriver.js +8 -16
- package/src/server/conversation/providers/codexDriver.js +25 -41
- package/src/server/conversation/providers/opencodeDriver.js +17 -42
- package/src/server/conversation/providers/scoped.js +48 -4
- package/src/server/conversation/runtime.js +50 -49
- package/src/server/conversation/runtimeStateUpgrade.js +34 -22
- package/src/server/conversation/transcript.js +10 -10
- package/src/server/hosts/codexConfiguration.js +1 -0
- package/src/server/hosts/codexProvider.js +8 -6
- package/src/server/index.js +2 -1
- package/src/server/lib/assistantToolLoop.js +1 -153
- package/src/server/lib/serviceToolCatalog.js +17 -42
- package/src/shared/assistantResource.js +4 -2
- package/src/shared/conversation/attachments.js +4 -0
- package/src/shared/conversation/longTextBlocks.js +54 -19
- package/src/shared/conversation/numberedQuestionSugar.js +17 -10
- package/src/shared/conversation/pagination.js +4 -6
- package/src/shared/conversation/patches.js +1 -4
- package/src/shared/conversation/streaming.js +3 -11
- package/src/shared/conversation/turns.js +11 -0
- package/src/shared/index.js +1 -0
- package/test/assistantBoundedToolLoop.test.js +2 -2
- package/test/assistantResource.test.js +4 -4
- package/test/assistantSettingsResource.test.js +2 -2
- package/test/claudeConversationSession.test.js +11 -1
- package/test/claudeTurn.test.js +27 -7
- package/test/codexClient.test.js +20 -0
- package/test/codexConversationSession.test.js +2 -2
- package/test/codexConversationsCheckpoint.test.js +51 -0
- package/test/codexModelCatalog.test.js +1 -1
- package/test/codexProviderHome.test.js +51 -0
- package/test/codexProviderSelection.test.js +71 -0
- package/test/codexTurn.test.js +222 -6
- package/test/conversationAgentRun.test.js +2 -1
- package/test/conversationAttachments.test.js +27 -2
- package/test/conversationDeliveryTiming.test.js +21 -0
- package/test/conversationNormalize.test.js +11 -0
- package/test/fixtures/nestedListQuestionMessage.md +37 -0
- package/test/frameworkActions.test.js +2 -2
- package/test/longTextBlocks.test.js +102 -0
- package/test/messageDelivery.test.js +7 -1
- package/test/openCodeClient.test.js +20 -0
- package/test/openCodeRuntime.test.js +1 -1
- package/test/openCodeTurn.test.js +90 -0
- package/test/opencodeConversationSession.test.js +1 -1
- package/test/serviceToolCatalog.test.js +27 -0
|
@@ -3,7 +3,8 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import {
|
|
6
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
7
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
7
8
|
|
|
8
9
|
const execute = promisify(execFile);
|
|
9
10
|
const CODEX_THREAD_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
@@ -40,9 +41,9 @@ export function codexAppServerThreadSettings({
|
|
|
40
41
|
...(config && typeof config === "object" && !Array.isArray(config) ? config : {})
|
|
41
42
|
},
|
|
42
43
|
cwd,
|
|
43
|
-
...(typeof systemPrompt === "string" && systemPrompt.trim() ? { systemPrompt:
|
|
44
|
+
...(typeof systemPrompt === "string" && systemPrompt.trim() ? { systemPrompt: normalizeNullableText(systemPrompt) } : {}),
|
|
44
45
|
...(hostContext ? { hostContext } : {}),
|
|
45
|
-
model:
|
|
46
|
+
model: normalizeNullableText(model) || effectiveSettings.model,
|
|
46
47
|
...(effectiveSettings.modelProviderId ? { modelProvider: effectiveSettings.modelProviderId } : {}),
|
|
47
48
|
sandbox
|
|
48
49
|
};
|
|
@@ -73,11 +74,11 @@ export function codexAppServerTurnSettings({
|
|
|
73
74
|
const settings = {
|
|
74
75
|
approvalPolicy,
|
|
75
76
|
cwd,
|
|
76
|
-
model:
|
|
77
|
+
model: normalizeNullableText(model) || effectiveSettings.model,
|
|
77
78
|
sandboxPolicy: externalSandbox ? { networkAccess: "enabled", type: "externalSandbox" } : sandboxPolicy
|
|
78
79
|
};
|
|
79
80
|
if (effectiveSettings.request.reasoning !== false) {
|
|
80
|
-
settings.effort =
|
|
81
|
+
settings.effort = normalizeNullableText(effort) || effectiveSettings.thinking;
|
|
81
82
|
}
|
|
82
83
|
if (effectiveSettings.request.summary !== false) {
|
|
83
84
|
settings.summary = reasoningSummary;
|
|
@@ -115,22 +116,22 @@ export function codexInteractiveArguments({
|
|
|
115
116
|
export async function codexAppServerProjectHookTrustConfig(provider, cwd = "", {
|
|
116
117
|
persist = false
|
|
117
118
|
} = {}) {
|
|
118
|
-
const normalizedCwd =
|
|
119
|
+
const normalizedCwd = normalizeNullableText(cwd);
|
|
119
120
|
if (!normalizedCwd || typeof provider?.listHooks !== "function") {
|
|
120
121
|
return null;
|
|
121
122
|
}
|
|
122
123
|
await provider.trustProject(normalizedCwd);
|
|
123
124
|
const result = await provider.listHooks([normalizedCwd]);
|
|
124
125
|
const record = (Array.isArray(result?.data) ? result.data : [])
|
|
125
|
-
.find((item) =>
|
|
126
|
+
.find((item) => normalizeNullableText(item?.cwd) === normalizedCwd);
|
|
126
127
|
const trustedHooks = (Array.isArray(record?.hooks) ? record.hooks : [])
|
|
127
128
|
// Session flags contain the host's command hook. Trust its exact hash
|
|
128
129
|
// alongside project hooks; unrelated account and plugin hooks stay separate.
|
|
129
130
|
.filter((hook) => hook?.enabled === true && ["project", "sessionFlags"].includes(hook?.source))
|
|
130
131
|
.map((hook) => ({
|
|
131
|
-
currentHash:
|
|
132
|
-
key:
|
|
133
|
-
trustStatus:
|
|
132
|
+
currentHash: normalizeNullableText(hook?.currentHash),
|
|
133
|
+
key: normalizeNullableText(hook?.key),
|
|
134
|
+
trustStatus: normalizeNullableText(hook?.trustStatus)
|
|
134
135
|
}))
|
|
135
136
|
.filter(({ currentHash, key }) => key && currentHash);
|
|
136
137
|
if (trustedHooks.length === 0) {
|
|
@@ -277,7 +278,7 @@ function deepFreezeCodexAppServerHelperConfig(value) {
|
|
|
277
278
|
}
|
|
278
279
|
|
|
279
280
|
function codexAppServerIsolationPolicyError(message = "", details = {}) {
|
|
280
|
-
const error = new Error(
|
|
281
|
+
const error = new Error(normalizeNullableText(message) || "Codex cannot prove the helper execution policy.");
|
|
281
282
|
error.code = "codex_policy_unenforceable";
|
|
282
283
|
error.details = Object.freeze({ ...details });
|
|
283
284
|
for (const [name, value] of Object.entries(error.details)) {
|
|
@@ -288,7 +289,7 @@ function codexAppServerIsolationPolicyError(message = "", details = {}) {
|
|
|
288
289
|
return error;
|
|
289
290
|
}
|
|
290
291
|
|
|
291
|
-
/**
|
|
292
|
+
/** Native helper isolation verification; hosts supply their profile policy. */
|
|
292
293
|
export function createCodexAppServerIsolation({
|
|
293
294
|
clientName = "jskit",
|
|
294
295
|
minimumVersion = "0.151.0",
|
|
@@ -346,8 +347,8 @@ export function createCodexAppServerIsolation({
|
|
|
346
347
|
);
|
|
347
348
|
}
|
|
348
349
|
const context = await provider.currentHelperExecutionContext();
|
|
349
|
-
const cwd =
|
|
350
|
-
const accountIdentitySignature =
|
|
350
|
+
const cwd = normalizeNullableText(context?.cwd);
|
|
351
|
+
const accountIdentitySignature = normalizeNullableText(context?.accountIdentitySignature);
|
|
351
352
|
if (
|
|
352
353
|
context?.executionMode !== "helper" ||
|
|
353
354
|
!cwd ||
|
|
@@ -366,7 +367,7 @@ export function createCodexAppServerIsolation({
|
|
|
366
367
|
}
|
|
367
368
|
|
|
368
369
|
function codexAppServerSemanticVersionParts(value = "") {
|
|
369
|
-
const match =
|
|
370
|
+
const match = normalizeNullableText(value).match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u);
|
|
370
371
|
if (!match) {
|
|
371
372
|
return null;
|
|
372
373
|
}
|
|
@@ -375,7 +376,7 @@ export function createCodexAppServerIsolation({
|
|
|
375
376
|
}
|
|
376
377
|
|
|
377
378
|
function codexAppServerUserAgentVersionParts(value = "") {
|
|
378
|
-
const userAgent =
|
|
379
|
+
const userAgent = normalizeNullableText(value);
|
|
379
380
|
if (
|
|
380
381
|
!userAgent ||
|
|
381
382
|
userAgent.length > userAgentMaxLength ||
|
|
@@ -398,7 +399,7 @@ export function createCodexAppServerIsolation({
|
|
|
398
399
|
{ minimumVersion }
|
|
399
400
|
);
|
|
400
401
|
}
|
|
401
|
-
const userAgent =
|
|
402
|
+
const userAgent = normalizeNullableText(provider.currentServerInfo()?.userAgent);
|
|
402
403
|
const actualParts = codexAppServerUserAgentVersionParts(userAgent);
|
|
403
404
|
if (!actualParts) {
|
|
404
405
|
throw createError(
|
|
@@ -482,8 +483,8 @@ export function createCodexAppServerIsolation({
|
|
|
482
483
|
}
|
|
483
484
|
const record = result.data[0];
|
|
484
485
|
if (
|
|
485
|
-
|
|
486
|
-
|
|
486
|
+
normalizeNullableText(record?.cwd) !== cwd ||
|
|
487
|
+
normalizeNullableText(record?.cwd).length > hookFieldMaxLength ||
|
|
487
488
|
!Array.isArray(record.hooks) ||
|
|
488
489
|
!Array.isArray(record.errors) ||
|
|
489
490
|
record.hooks.length > hookMaxCount ||
|
|
@@ -500,10 +501,10 @@ export function createCodexAppServerIsolation({
|
|
|
500
501
|
);
|
|
501
502
|
}
|
|
502
503
|
const hooks = record.hooks.map((hook) => {
|
|
503
|
-
const key =
|
|
504
|
-
const currentHash =
|
|
505
|
-
const handlerType =
|
|
506
|
-
const sourcePath =
|
|
504
|
+
const key = normalizeNullableText(hook?.key);
|
|
505
|
+
const currentHash = normalizeNullableText(hook?.currentHash);
|
|
506
|
+
const handlerType = normalizeNullableText(hook?.handlerType);
|
|
507
|
+
const sourcePath = normalizeNullableText(hook?.sourcePath);
|
|
507
508
|
if (
|
|
508
509
|
!key ||
|
|
509
510
|
[key, currentHash, handlerType, sourcePath].some((field) => (
|
|
@@ -600,6 +601,21 @@ export function createCodexAppServerIsolation({
|
|
|
600
601
|
return frozenConfig;
|
|
601
602
|
}
|
|
602
603
|
|
|
604
|
+
function isolationConfigFrom(configResult, hookResult, cwd, { effort = "", summary = "" } = {}) {
|
|
605
|
+
const mcpServerNames = codexAppServerHelperMcpServerNames(configResult);
|
|
606
|
+
const hookState = codexAppServerHelperHookState(hookResult, cwd);
|
|
607
|
+
return {
|
|
608
|
+
mcpServerNames,
|
|
609
|
+
hookState,
|
|
610
|
+
config: codexAppServerHelperIsolationConfig({
|
|
611
|
+
effort,
|
|
612
|
+
summary,
|
|
613
|
+
hookKeys: hookState.hookKeys,
|
|
614
|
+
mcpServerNames
|
|
615
|
+
})
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
603
619
|
async function codexAppServerHelperIsolationState(provider, { effort = "", summary = "" } = {}) {
|
|
604
620
|
assertCodexAppServerHelperCompatibility(provider);
|
|
605
621
|
if (
|
|
@@ -627,16 +643,13 @@ export function createCodexAppServerIsolation({
|
|
|
627
643
|
"Codex app-server reconnected during helper policy verification."
|
|
628
644
|
);
|
|
629
645
|
}
|
|
630
|
-
const mcpServerNames =
|
|
631
|
-
|
|
646
|
+
const { mcpServerNames, hookState, config } = isolationConfigFrom(configResult, hookResult, executionContext.cwd, {
|
|
647
|
+
effort,
|
|
648
|
+
summary
|
|
649
|
+
});
|
|
632
650
|
return Object.freeze({
|
|
633
651
|
accountIdentitySignature: executionContext.accountIdentitySignature,
|
|
634
|
-
config
|
|
635
|
-
effort,
|
|
636
|
-
summary,
|
|
637
|
-
hookKeys: hookState.hookKeys,
|
|
638
|
-
mcpServerNames
|
|
639
|
-
}),
|
|
652
|
+
config,
|
|
640
653
|
connectionGeneration: generation,
|
|
641
654
|
executionCwd: executionContext.cwd,
|
|
642
655
|
hookFingerprint: hookState.fingerprint,
|
|
@@ -654,7 +667,7 @@ export function createCodexAppServerIsolation({
|
|
|
654
667
|
}
|
|
655
668
|
|
|
656
669
|
function codexAppServerHelperVerificationFailure(error, cleanupError, threadId = "") {
|
|
657
|
-
const normalizedThreadId =
|
|
670
|
+
const normalizedThreadId = normalizeNullableText(threadId);
|
|
658
671
|
if (!cleanupError) {
|
|
659
672
|
error.codexAppServerHelperThreadId = normalizedThreadId;
|
|
660
673
|
error.codexAppServerHelperThreadRetired = true;
|
|
@@ -686,34 +699,19 @@ export function createCodexAppServerIsolation({
|
|
|
686
699
|
throw codexAppServerHelperVerificationFailure(error, cleanupError, threadId);
|
|
687
700
|
}
|
|
688
701
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
typeof provider?.startThread !== "function" ||
|
|
692
|
-
typeof provider?.deleteThread !== "function"
|
|
693
|
-
) {
|
|
694
|
-
throw createError(
|
|
695
|
-
"Codex helper execution cannot own and clean up its app-server thread."
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
const prepared = await prepare();
|
|
699
|
-
const thread = await provider.startThread(prepared.settings);
|
|
700
|
-
const threadId = normalizeText(thread?.id);
|
|
701
|
-
if (!threadId) {
|
|
702
|
-
throw createError(
|
|
703
|
-
"Codex app-server did not return a helper thread id."
|
|
704
|
-
);
|
|
705
|
-
}
|
|
702
|
+
// Re-inspect after the native thread exists; a changed surface retires it.
|
|
703
|
+
async function verifyHelperThread(provider, enforcement, thread, threadId, phase) {
|
|
706
704
|
try {
|
|
707
705
|
const verified = await codexAppServerHelperIsolationState(
|
|
708
706
|
provider,
|
|
709
707
|
{
|
|
710
|
-
effort:
|
|
711
|
-
summary:
|
|
708
|
+
effort: enforcement.config.model_reasoning_effort,
|
|
709
|
+
summary: enforcement.config.model_reasoning_summary
|
|
712
710
|
}
|
|
713
711
|
);
|
|
714
|
-
if (!codexAppServerHelperIsolationMatches(
|
|
712
|
+
if (!codexAppServerHelperIsolationMatches(enforcement, verified)) {
|
|
715
713
|
throw createError(
|
|
716
|
-
|
|
714
|
+
`Codex execution surfaces changed while the helper thread was ${phase}.`
|
|
717
715
|
);
|
|
718
716
|
}
|
|
719
717
|
return Object.freeze({
|
|
@@ -730,6 +728,26 @@ export function createCodexAppServerIsolation({
|
|
|
730
728
|
}
|
|
731
729
|
}
|
|
732
730
|
|
|
731
|
+
async function startCodexAppServerHelperThread(provider, prepare) {
|
|
732
|
+
if (
|
|
733
|
+
typeof provider?.startThread !== "function" ||
|
|
734
|
+
typeof provider?.deleteThread !== "function"
|
|
735
|
+
) {
|
|
736
|
+
throw createError(
|
|
737
|
+
"Codex helper execution cannot own and clean up its app-server thread."
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const prepared = await prepare();
|
|
741
|
+
const thread = await provider.startThread(prepared.settings);
|
|
742
|
+
const threadId = normalizeNullableText(thread?.id);
|
|
743
|
+
if (!threadId) {
|
|
744
|
+
throw createError(
|
|
745
|
+
"Codex app-server did not return a helper thread id."
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
return verifyHelperThread(provider, prepared.enforcement, thread, threadId, "starting");
|
|
749
|
+
}
|
|
750
|
+
|
|
733
751
|
async function resumeCodexAppServerHelperThread(provider, threadId, prepare) {
|
|
734
752
|
if (
|
|
735
753
|
typeof provider?.resumeThread !== "function" ||
|
|
@@ -739,7 +757,7 @@ export function createCodexAppServerIsolation({
|
|
|
739
757
|
"Codex helper execution cannot safely resume and clean up its app-server thread."
|
|
740
758
|
);
|
|
741
759
|
}
|
|
742
|
-
const normalizedThreadId =
|
|
760
|
+
const normalizedThreadId = normalizeNullableText(threadId);
|
|
743
761
|
if (!normalizedThreadId) {
|
|
744
762
|
throw createError(
|
|
745
763
|
"Codex helper resume requires a controller-owned thread id."
|
|
@@ -751,42 +769,11 @@ export function createCodexAppServerIsolation({
|
|
|
751
769
|
normalizedThreadId,
|
|
752
770
|
prepared.settings
|
|
753
771
|
);
|
|
754
|
-
|
|
755
|
-
const verified = await codexAppServerHelperIsolationState(
|
|
756
|
-
provider,
|
|
757
|
-
{
|
|
758
|
-
effort: enforcement.config.model_reasoning_effort,
|
|
759
|
-
summary: enforcement.config.model_reasoning_summary
|
|
760
|
-
}
|
|
761
|
-
);
|
|
762
|
-
if (!codexAppServerHelperIsolationMatches(enforcement, verified)) {
|
|
763
|
-
throw createError(
|
|
764
|
-
"Codex execution surfaces changed while the helper thread was resuming."
|
|
765
|
-
);
|
|
766
|
-
}
|
|
767
|
-
return Object.freeze({
|
|
768
|
-
enforcement: verified,
|
|
769
|
-
thread,
|
|
770
|
-
threadId: normalizedThreadId
|
|
771
|
-
});
|
|
772
|
-
} catch (error) {
|
|
773
|
-
await throwAfterCodexAppServerHelperVerificationFailure({
|
|
774
|
-
error,
|
|
775
|
-
provider,
|
|
776
|
-
threadId: normalizedThreadId
|
|
777
|
-
});
|
|
778
|
-
}
|
|
772
|
+
return verifyHelperThread(provider, enforcement, thread, normalizedThreadId, "resuming");
|
|
779
773
|
}
|
|
780
774
|
|
|
781
775
|
function configuration({ configResult, hookResult, workdir, effort = "", summary = "" }) {
|
|
782
|
-
|
|
783
|
-
const hookState = codexAppServerHelperHookState(hookResult, workdir);
|
|
784
|
-
return codexAppServerHelperIsolationConfig({
|
|
785
|
-
effort,
|
|
786
|
-
summary,
|
|
787
|
-
hookKeys: hookState.hookKeys,
|
|
788
|
-
mcpServerNames
|
|
789
|
-
});
|
|
776
|
+
return isolationConfigFrom(configResult, hookResult, workdir, { effort, summary }).config;
|
|
790
777
|
}
|
|
791
778
|
|
|
792
779
|
// Native Helper requests retain their verified configuration object and the
|
|
@@ -924,19 +911,19 @@ export function codexCatalogReasoningEfforts(model = {}) {
|
|
|
924
911
|
return new Set((Array.isArray(model?.supportedReasoningEfforts)
|
|
925
912
|
? model.supportedReasoningEfforts
|
|
926
913
|
: [])
|
|
927
|
-
.map((option) =>
|
|
914
|
+
.map((option) => normalizeNullableText(option?.reasoningEffort))
|
|
928
915
|
.filter(Boolean));
|
|
929
916
|
}
|
|
930
917
|
|
|
931
918
|
export function codexCatalogModels(rows) {
|
|
932
919
|
return rows.map((model) => ({
|
|
933
|
-
id:
|
|
934
|
-
label:
|
|
920
|
+
id: normalizeNullableText(model.model),
|
|
921
|
+
label: normalizeNullableText(model.displayName) || normalizeNullableText(model.model),
|
|
935
922
|
status: "available",
|
|
936
923
|
variants: [...codexCatalogReasoningEfforts(model)]
|
|
937
924
|
.map((variantId) => ({
|
|
938
|
-
id:
|
|
939
|
-
label:
|
|
925
|
+
id: normalizeNullableText(variantId),
|
|
926
|
+
label: normalizeNullableText(variantId).replace(/^./u, (value) => value.toUpperCase())
|
|
940
927
|
}))
|
|
941
928
|
}));
|
|
942
929
|
}
|
|
@@ -991,12 +978,21 @@ export function codexProviderConfiguration(provider, apiKey) {
|
|
|
991
978
|
};
|
|
992
979
|
}
|
|
993
980
|
|
|
981
|
+
/** Where a curated provider home keeps the model catalogue its config.toml names. */
|
|
982
|
+
export function codexProviderCatalogPath(codexHome) {
|
|
983
|
+
return path.join(codexHome, "models.json");
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function codexProviderCatalogSetting(codexHome) {
|
|
987
|
+
return `model_catalog_json = ${JSON.stringify(codexProviderCatalogPath(codexHome))}`;
|
|
988
|
+
}
|
|
989
|
+
|
|
994
990
|
/** Format a private native config file from an already-authorized provider and key. */
|
|
995
991
|
export function codexProviderFileConfiguration(provider, key, codexHome) {
|
|
996
992
|
return [
|
|
997
993
|
`model = ${JSON.stringify(provider.models[0].id)}`,
|
|
998
994
|
`model_provider = ${JSON.stringify(provider.id)}`,
|
|
999
|
-
|
|
995
|
+
codexProviderCatalogSetting(codexHome),
|
|
1000
996
|
`model_reasoning_effort = ${JSON.stringify(provider.models[0].defaultThinking)}`,
|
|
1001
997
|
'model_reasoning_summary = "none"',
|
|
1002
998
|
`web_search = "${provider.webSearch ? "live" : "disabled"}"`,
|
|
@@ -1010,6 +1006,24 @@ export function codexProviderFileConfiguration(provider, key, codexHome) {
|
|
|
1010
1006
|
].join("\n");
|
|
1011
1007
|
}
|
|
1012
1008
|
|
|
1009
|
+
/**
|
|
1010
|
+
* Copy a curated provider home's config and catalogue into another home, with
|
|
1011
|
+
* the copied config naming the copied catalogue. A config that does not name
|
|
1012
|
+
* its own home's catalogue exactly once is refused rather than left pointing
|
|
1013
|
+
* at the source home.
|
|
1014
|
+
*/
|
|
1015
|
+
export async function copyCodexProviderHome(sourceHome, targetHome) {
|
|
1016
|
+
const config = await readFile(path.join(sourceHome, "config.toml"), "utf8");
|
|
1017
|
+
const sourceSetting = codexProviderCatalogSetting(sourceHome);
|
|
1018
|
+
if (config.split(sourceSetting).length !== 2) {
|
|
1019
|
+
throw new Error("Codex provider config does not name its own model catalogue.");
|
|
1020
|
+
}
|
|
1021
|
+
await writeFile(codexProviderCatalogPath(targetHome),
|
|
1022
|
+
await readFile(codexProviderCatalogPath(sourceHome)), { mode: 0o600 });
|
|
1023
|
+
await writeFile(path.join(targetHome, "config.toml"),
|
|
1024
|
+
config.replace(sourceSetting, codexProviderCatalogSetting(targetHome)), { mode: 0o600 });
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1013
1027
|
export async function verifyCodexProviderKey(provider, apiKey, fetchImpl = fetch) {
|
|
1014
1028
|
let response;
|
|
1015
1029
|
try {
|
|
@@ -1057,9 +1071,9 @@ export function createCodexAccountReader({
|
|
|
1057
1071
|
} = {}) {
|
|
1058
1072
|
function codexAppServerHelperAuthError(code = "", message = "") {
|
|
1059
1073
|
const error = new Error(
|
|
1060
|
-
|
|
1074
|
+
normalizeNullableText(message) || "Codex helper authentication is unavailable. Reconnect Codex and retry."
|
|
1061
1075
|
);
|
|
1062
|
-
error.code =
|
|
1076
|
+
error.code = normalizeNullableText(code) || `${errorPrefix}codex_helper_auth_unavailable`;
|
|
1063
1077
|
return error;
|
|
1064
1078
|
}
|
|
1065
1079
|
|
|
@@ -1079,7 +1093,7 @@ export function createCodexAccountReader({
|
|
|
1079
1093
|
function codexAccountIdentitySignature(authMode = "", identity = "") {
|
|
1080
1094
|
return `sha256:${createHash("sha256")
|
|
1081
1095
|
.update(`${identityNamespace}\0`, "utf8")
|
|
1082
|
-
.update(
|
|
1096
|
+
.update(normalizeNullableText(authMode), "utf8")
|
|
1083
1097
|
.update("\0", "utf8")
|
|
1084
1098
|
.update(String(identity || ""), "utf8")
|
|
1085
1099
|
.digest("hex")}`;
|
|
@@ -1137,7 +1151,7 @@ export function createCodexAccountReader({
|
|
|
1137
1151
|
}
|
|
1138
1152
|
|
|
1139
1153
|
async function readCodexSelectedAccountAuth(options = {}) {
|
|
1140
|
-
const toolHomeSource =
|
|
1154
|
+
const toolHomeSource = normalizeNullableText(options.toolHomeSource);
|
|
1141
1155
|
if (!toolHomeSource || !path.isAbsolute(toolHomeSource)) {
|
|
1142
1156
|
throw codexAppServerHelperAuthError(
|
|
1143
1157
|
`${errorPrefix}codex_helper_auth_unavailable`,
|
|
@@ -1145,7 +1159,7 @@ export function createCodexAccountReader({
|
|
|
1145
1159
|
);
|
|
1146
1160
|
}
|
|
1147
1161
|
const auth = await readBoundedCodexAuthJson(path.join(toolHomeSource, ".codex", "auth.json"));
|
|
1148
|
-
const storedMode =
|
|
1162
|
+
const storedMode = normalizeNullableText(auth.auth_mode).toLowerCase();
|
|
1149
1163
|
if (storedMode === "chatgpt") {
|
|
1150
1164
|
const tokens = isRecord(auth.tokens) ? auth.tokens : {};
|
|
1151
1165
|
const accountId = boundedCodexAuthText(tokens.account_id, {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
2
|
+
import { createReentrantConversationStorage } from "./storage.js";
|
|
3
|
+
import { isCompletedEnvelopeTurn } from "./transcript.js";
|
|
4
|
+
import { conversationAgentRunRecord, conversationAgentRunEvent } from "./agentRun.js";
|
|
5
|
+
|
|
6
|
+
/** The original native run occupies its bound conversation segment. Reading a
|
|
7
|
+
* binding without a run never constructs historical ownership or writes state.
|
|
8
|
+
*/
|
|
9
|
+
export function createCodexConversationRunStore({ storage, scope, segmentId, isCurrent, clock = () => new Date() } = {}) {
|
|
10
|
+
if (typeof isCurrent !== "function") throw new TypeError("A native run store requires its provider ownership fence.");
|
|
11
|
+
storage = createReentrantConversationStorage(storage);
|
|
12
|
+
const runId = "codex_app_server";
|
|
13
|
+
|
|
14
|
+
function assertScope(sessionId, requestedRunId = runId) {
|
|
15
|
+
if (sessionId !== scope) throw new Error("The native run belongs to another conversation.");
|
|
16
|
+
if (normalizeNullableText(requestedRunId) !== runId) throw new TypeError("This binding stores only its Codex app-server run.");
|
|
17
|
+
if (!isCurrent()) throw new Error("A retired provider cannot change a native binding.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function currentMetadata(transaction) {
|
|
21
|
+
const metadata = await transaction.readMetadata();
|
|
22
|
+
if (!isCurrent() || metadata.runtime?.segmentId !== segmentId || metadata.runtime?.engine !== "codex") {
|
|
23
|
+
throw new Error("A retired native conversation cannot change the current binding.");
|
|
24
|
+
}
|
|
25
|
+
return metadata;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function readAgentRun(sessionId, requestedRunId) {
|
|
29
|
+
assertScope(sessionId, requestedRunId);
|
|
30
|
+
return storage.read(scope, async transaction => {
|
|
31
|
+
const metadata = await currentMetadata(transaction);
|
|
32
|
+
return conversationAgentRunRecord(metadata.runtime.binding?.codexAppServerRun, runId);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mutateSession(sessionId, operation) {
|
|
37
|
+
assertScope(sessionId);
|
|
38
|
+
return storage.write(scope, async transaction => {
|
|
39
|
+
await currentMetadata(transaction);
|
|
40
|
+
return operation(transaction);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
readAgentRun,
|
|
46
|
+
mutateSession,
|
|
47
|
+
async getSession(sessionId) {
|
|
48
|
+
const run = await readAgentRun(sessionId, runId);
|
|
49
|
+
return { sessionId, agentRuns: run ? [run] : [] };
|
|
50
|
+
},
|
|
51
|
+
async writeAgentRunEvent(sessionId, requestedRunId, input = {}) {
|
|
52
|
+
assertScope(sessionId, requestedRunId);
|
|
53
|
+
return mutateSession(sessionId, async transaction => {
|
|
54
|
+
const previous = await readAgentRun(sessionId, runId) || { events: [], id: runId };
|
|
55
|
+
const record = conversationAgentRunEvent(previous, runId, input, { now: clock });
|
|
56
|
+
const metadata = await currentMetadata(transaction);
|
|
57
|
+
metadata.runtime.binding.codexAppServerRun = record;
|
|
58
|
+
await transaction.writeMetadata(metadata);
|
|
59
|
+
return record;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The native owner uses the existing transcript and transient streams through
|
|
66
|
+
* the same fenced transaction as its original run. Its private delivery read is
|
|
67
|
+
* a projection of the one authored request, never another stored journal.
|
|
68
|
+
*/
|
|
69
|
+
export function createCodexConversationStore({ storage, scope, segmentId, isCurrent, transcript, streams } = {}) {
|
|
70
|
+
const runStore = createCodexConversationRunStore({ storage, scope, segmentId, isCurrent });
|
|
71
|
+
const assertCurrent = sessionId => {
|
|
72
|
+
if (sessionId !== scope || !isCurrent()) throw new Error("A retired provider cannot change this conversation.");
|
|
73
|
+
};
|
|
74
|
+
const reader = name => (sessionId, ...args) => {
|
|
75
|
+
assertCurrent(sessionId);
|
|
76
|
+
return transcript[name](sessionId, ...args);
|
|
77
|
+
};
|
|
78
|
+
async function nativeAuthorship(transaction, identity) {
|
|
79
|
+
if (!identity?.threadId || !identity.turnId) return undefined;
|
|
80
|
+
const { runtime } = await transaction.readMetadata();
|
|
81
|
+
const run = runtime.binding?.codexAppServerRun;
|
|
82
|
+
if (runtime.segmentId !== segmentId || runtime.binding.threadId !== identity.threadId ||
|
|
83
|
+
run?.providerThreadId !== identity.threadId || run.providerTurnId !== identity.turnId || !run.outerTurnId) return undefined;
|
|
84
|
+
for (const turnId of (await transaction.listTurnIds()).reverse()) {
|
|
85
|
+
const turn = await transaction.readTurn(turnId);
|
|
86
|
+
if (turn?.metadata?.runtime?.segmentId !== segmentId) continue;
|
|
87
|
+
if (turn.user?.messageId === run.outerTurnId) return { origin: "user", messageId: run.outerTurnId, turnId };
|
|
88
|
+
if (turn.system?.messageId === run.outerTurnId && turn.system.origin === "application") {
|
|
89
|
+
return { origin: "application", messageId: run.outerTurnId, turnId,
|
|
90
|
+
...(isCompletedEnvelopeTurn(turn) ? { completedEnvelope: true } : {}) };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const request = runtime.request;
|
|
94
|
+
if (request?.messageId === run.outerTurnId && ["user", "application"].includes(request.origin)) {
|
|
95
|
+
return { origin: request.origin, messageId: request.messageId,
|
|
96
|
+
...(request.origin === "application" && request.completedEnvelope === true ? { completedEnvelope: true } : {}) };
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const writer = name => (sessionId, input = {}) => runStore.mutateSession(sessionId, async transaction => {
|
|
101
|
+
const authorship = await nativeAuthorship(transaction, input.nativeIdentity);
|
|
102
|
+
if (authorship?.completedEnvelope && !authorship.turnId) {
|
|
103
|
+
throw new Error("The native output's original turn no longer exists.");
|
|
104
|
+
}
|
|
105
|
+
// Queued human rows do not own an already admitted private native response.
|
|
106
|
+
// Corrections retain their original explicit target and message identity.
|
|
107
|
+
const written = await transcript[name](sessionId, {
|
|
108
|
+
...input, ...(name !== "upsertConversationAssistantMessage" && authorship?.completedEnvelope && authorship.turnId
|
|
109
|
+
? { turnId: authorship.turnId } : {})
|
|
110
|
+
});
|
|
111
|
+
if (!written || written.metadata?.runtime?.segmentId) return written;
|
|
112
|
+
const origin = authorship?.origin;
|
|
113
|
+
await transaction.updateTurnMetadata(written.turnId, { runtime: {
|
|
114
|
+
status: "complete", engine: "codex", segmentId, ...(origin ? { origin } : {}),
|
|
115
|
+
...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {})
|
|
116
|
+
} });
|
|
117
|
+
return transaction.readTurn(written.turnId);
|
|
118
|
+
});
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
...runStore,
|
|
121
|
+
readConversationLog: reader("readConversationLog"),
|
|
122
|
+
conversationMessageIdExists: reader("conversationMessageIdExists"),
|
|
123
|
+
writeConversationAssistantMessage: writer("writeConversationAssistantMessage"),
|
|
124
|
+
writeConversationCommentaryMessage: writer("writeConversationCommentaryMessage"),
|
|
125
|
+
writeConversationThinkingMessage: writer("writeConversationThinkingMessage"),
|
|
126
|
+
upsertConversationAssistantMessage: writer("upsertConversationAssistantMessage"),
|
|
127
|
+
async writeConversationUserMessage(sessionId, input = {}) {
|
|
128
|
+
return runStore.mutateSession(sessionId, async transaction => {
|
|
129
|
+
if (input.messageId && await transaction.hasMessage(input.messageId)) return null;
|
|
130
|
+
const metadata = await transaction.readMetadata();
|
|
131
|
+
const request = input.authoredRequest?.messageId === input.messageId ? input.authoredRequest
|
|
132
|
+
: metadata.runtime.request?.messageId === input.messageId ? metadata.runtime.request : null;
|
|
133
|
+
if (!request) {
|
|
134
|
+
const written = await transcript.writeConversationUserMessage(sessionId, input);
|
|
135
|
+
if (!written) return null;
|
|
136
|
+
await transaction.updateTurnMetadata(written.turnId, { runtime: {
|
|
137
|
+
status: "running", engine: "codex", segmentId, origin: "user",
|
|
138
|
+
...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {})
|
|
139
|
+
} });
|
|
140
|
+
return transaction.readTurn(written.turnId);
|
|
141
|
+
}
|
|
142
|
+
const turnId = await transaction.nextTurnId();
|
|
143
|
+
await transaction.appendMessage(turnId, {
|
|
144
|
+
role: request.origin === "application" ? "system" : "user",
|
|
145
|
+
messageId: request.messageId, text: request.text, origin: request.origin || "user",
|
|
146
|
+
attachments: request.attachments || input.attachments || [], at: request.at || new Date().toISOString(),
|
|
147
|
+
...(request.data !== undefined ? { data: request.data } : {}), ...(request.goal ? { goal: request.goal } : {}),
|
|
148
|
+
turnMetadata: { ...input.turnMetadata, runtime: { ...input.turnMetadata?.runtime,
|
|
149
|
+
status: "running", engine: "codex", segmentId, origin: request.origin || "user",
|
|
150
|
+
...(metadata.runtime.request?.messageId === input.messageId && metadata.runtime.request.completedEnvelope === true
|
|
151
|
+
? { completedEnvelope: true } : {}),
|
|
152
|
+
...(input.nativeIdentity?.turnId ? { nativeTurnId: input.nativeIdentity.turnId } : {}),
|
|
153
|
+
...(request.goal ? { goalMessageId: request.messageId } : {}) } }
|
|
154
|
+
});
|
|
155
|
+
return transaction.readTurn(turnId);
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
readMetadataValue(sessionId, key) {
|
|
159
|
+
assertCurrent(sessionId);
|
|
160
|
+
if (key !== "assistant_delivery") throw new Error("The native conversation requested unknown delivery metadata.");
|
|
161
|
+
return storage.read(sessionId, async transaction => {
|
|
162
|
+
const { runtime } = await transaction.readMetadata();
|
|
163
|
+
if (!isCurrent() || runtime.segmentId !== segmentId || runtime.engine !== "codex") {
|
|
164
|
+
throw new Error("A retired native conversation cannot read current delivery metadata.");
|
|
165
|
+
}
|
|
166
|
+
const request = runtime.request;
|
|
167
|
+
return JSON.stringify({ engines: { codex: { pending: request && { ...request,
|
|
168
|
+
displayMessage: request.displayMessage ?? request.text,
|
|
169
|
+
displayAttachments: request.displayAttachments ?? request.attachments } } } });
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
updateConversationStream(sessionId, input) {
|
|
173
|
+
return runStore.mutateSession(sessionId, async transaction => {
|
|
174
|
+
const authorship = await nativeAuthorship(transaction, input.nativeIdentity);
|
|
175
|
+
return streams.update(sessionId, { ...input, origin: authorship?.origin, authorship });
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
completeConversationStreamMessage(sessionId, messageId, options) {
|
|
179
|
+
assertCurrent(sessionId);
|
|
180
|
+
if (options?.nativeIdentity) return runStore.mutateSession(sessionId, async transaction => {
|
|
181
|
+
const authorship = await nativeAuthorship(transaction, options.nativeIdentity);
|
|
182
|
+
return streams.complete(sessionId, messageId, { ...options, authorship,
|
|
183
|
+
...(!authorship ? { text: undefined } : {}) });
|
|
184
|
+
});
|
|
185
|
+
return streams.complete(sessionId, messageId, options);
|
|
186
|
+
},
|
|
187
|
+
clearConversationStream(sessionId) { assertCurrent(sessionId); return streams.clear(sessionId); }
|
|
188
|
+
});
|
|
189
|
+
}
|