@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.
- 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 +51 -40
- 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/codexGoalCommands.js +1 -1
- 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 +10 -16
- package/src/server/conversation/providers/codexDriver.js +27 -43
- 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/assistantScroll.browser.test.js +29 -6
- package/test/assistantSettingsResource.test.js +2 -2
- package/test/claudeConversationSession.test.js +20 -3
- package/test/claudeTurn.test.js +27 -7
- package/test/codexClient.test.js +20 -0
- package/test/codexConversationSession.test.js +5 -3
- 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
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { stat } from "node:fs/promises";
|
|
3
3
|
import { CodexAppServerAgentProvider } from "./codexProvider.js";
|
|
4
4
|
import { codexAppServerProjectHookTrustConfig } from "./codexConfiguration.js";
|
|
5
|
+
import { normalizeCodexRunText as normalizeText } from "./codexTurnState.js";
|
|
5
6
|
|
|
6
7
|
// Moved from Vibe64's codexTerminal provider owner. Caller-local provider keys
|
|
7
8
|
// are grouped without duplicating the inventory. Recovery and sharing always
|
|
@@ -9,7 +10,6 @@ import { codexAppServerProjectHookTrustConfig } from "./codexConfiguration.js";
|
|
|
9
10
|
const codexAppServerProviders = new Map();
|
|
10
11
|
const codexAppServerOwnedRuntimes = new Map();
|
|
11
12
|
const codexAppServerRuntimeLifecycles = new Map();
|
|
12
|
-
const normalizeText = value => String(value || "").trim();
|
|
13
13
|
|
|
14
14
|
export function codexAppServerOwnedRuntimeKey(providerKey = "", providerOptions = {}) {
|
|
15
15
|
const runtimeDir = normalizeText(providerOptions?.runtimeDir);
|
|
@@ -36,7 +36,7 @@ export function codexAppServerRuntimeStopWasVerified(result = {}) {
|
|
|
36
36
|
result?.runtimeDirRemoved === true;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
async function directoryExists(filePath = "") {
|
|
39
|
+
export async function directoryExists(filePath = "") {
|
|
40
40
|
try {
|
|
41
41
|
return (await stat(filePath)).isDirectory();
|
|
42
42
|
} catch (error) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { normalizeCodexRunText } from "./codexTurnState.js";
|
|
2
2
|
import { codexAppServerProjectHookTrustConfig } from "./codexConfiguration.js";
|
|
3
|
-
import { createCodexAppServerModelCatalogCache } from "./
|
|
3
|
+
import { createCodexAppServerModelCatalogCache } from "./codexModelCatalog.js";
|
|
4
4
|
import { readCodexAppServerAccountIdentity } from "./codexHelperExecution.js";
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// Provider acquisition and per-operation provider contexts for a run owner's
|
|
7
|
+
// managed threads, with their model catalogue and account identity reads.
|
|
7
8
|
export function createCodexProviderSelection({
|
|
8
9
|
providerSessions,
|
|
9
10
|
turnState,
|
|
@@ -16,6 +17,25 @@ export function createCodexProviderSelection({
|
|
|
16
17
|
cacheMs: modelCatalogCacheMs,
|
|
17
18
|
errorPrefix
|
|
18
19
|
});
|
|
20
|
+
function sessionConnection(sessionId, providerOptions) {
|
|
21
|
+
return providerSessions.context(normalizeCodexRunText(sessionId), providerOptions);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ensureSessionProvider(sessionId, providerOptions, threadId) {
|
|
25
|
+
return providerSessions.owner.ensureSession(sessionConnection(sessionId, providerOptions), threadId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function rememberManagedSession(sessionId, providerOptions, { executionRoot, workdir, threadId } = {}) {
|
|
29
|
+
const connection = sessionConnection(sessionId, providerOptions);
|
|
30
|
+
connection.runOwner.runtimeLifecycle.rememberManagedSession(connection.providerKey, {
|
|
31
|
+
providerOptions,
|
|
32
|
+
sessionId: normalizeCodexRunText(sessionId),
|
|
33
|
+
executionRoot,
|
|
34
|
+
threadId,
|
|
35
|
+
workdir
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
function managedProviderKeyForThread(sessionId, workdir, threadId) {
|
|
20
40
|
for (const [providerKey, managed] of providerSessions.managed) {
|
|
21
41
|
const provider = providerSessions.owner.providers.get(providerKey);
|
|
@@ -54,9 +74,7 @@ export function createCodexProviderSelection({
|
|
|
54
74
|
const provider = providerSessions.owner.providers.get(providerKey);
|
|
55
75
|
if (provider) {
|
|
56
76
|
return {
|
|
57
|
-
provider: await
|
|
58
|
-
providerSessions.context(normalizedSessionId, managed.providerOptions)
|
|
59
|
-
),
|
|
77
|
+
provider: await ensureSessionProvider(normalizedSessionId, managed.providerOptions),
|
|
60
78
|
providerKey,
|
|
61
79
|
providerOptions: managed.providerOptions
|
|
62
80
|
};
|
|
@@ -76,22 +94,18 @@ export function createCodexProviderSelection({
|
|
|
76
94
|
const activeProvider = await ensureCodexAppServerProviderForActiveTurn(session, managedIdentity);
|
|
77
95
|
const provider = activeProvider?.provider;
|
|
78
96
|
if (provider) return provider;
|
|
79
|
-
|
|
80
|
-
return await providerSessions.owner.ensureSession(
|
|
81
|
-
providerSessions.context(normalizeCodexRunText(sessionId), providerOptions)
|
|
82
|
-
);
|
|
97
|
+
return await ensureSessionProvider(sessionId, await prepareProviderOptions());
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
async function codexAppServerRuntimeForVisibleTerminal(
|
|
100
|
+
async function codexAppServerRuntimeForVisibleTerminal(rawSessionId, session, options, managedIdentity, prepareProviderOptions) {
|
|
101
|
+
const sessionId = normalizeCodexRunText(rawSessionId);
|
|
86
102
|
const activeProvider = await ensureCodexAppServerProviderForActiveTurn(session, managedIdentity);
|
|
87
103
|
const providerOptions = activeProvider?.providerOptions || await prepareProviderOptions();
|
|
88
|
-
const provider = activeProvider?.provider || await
|
|
89
|
-
providerSessions.context(normalizeCodexRunText(sessionId), providerOptions)
|
|
90
|
-
);
|
|
104
|
+
const provider = activeProvider?.provider || await ensureSessionProvider(sessionId, providerOptions);
|
|
91
105
|
await codexAppServerProjectHookTrustConfig(provider, options.workdir, {
|
|
92
106
|
persist: true
|
|
93
107
|
});
|
|
94
|
-
const providerKey = activeProvider?.providerKey ||
|
|
108
|
+
const providerKey = activeProvider?.providerKey || sessionConnection(sessionId, providerOptions).providerKey;
|
|
95
109
|
return providerSessions.owner.acquireRuntime({
|
|
96
110
|
operation: () => provider.ensureRuntime(),
|
|
97
111
|
provider,
|
|
@@ -112,9 +126,7 @@ export function createCodexProviderSelection({
|
|
|
112
126
|
);
|
|
113
127
|
if (activeProvider) return { provider: activeProvider.provider || null, reused: true };
|
|
114
128
|
return {
|
|
115
|
-
provider: await
|
|
116
|
-
normalizeCodexRunText(sessionId), await context.providerOptions(currentSession)
|
|
117
|
-
)),
|
|
129
|
+
provider: await ensureSessionProvider(sessionId, await context.providerOptions(currentSession)),
|
|
118
130
|
reused: false
|
|
119
131
|
};
|
|
120
132
|
}
|
|
@@ -130,9 +142,8 @@ export function createCodexProviderSelection({
|
|
|
130
142
|
const activeProvider = await ensureCodexAppServerProviderForActiveTurn(
|
|
131
143
|
currentSession, context.managedIdentity(currentSession)
|
|
132
144
|
);
|
|
133
|
-
return activeProvider?.provider ||
|
|
134
|
-
|
|
135
|
-
);
|
|
145
|
+
return activeProvider?.provider ||
|
|
146
|
+
await ensureSessionProvider(sessionId, await context.providerOptions(currentSession));
|
|
136
147
|
},
|
|
137
148
|
observationOwner(threadId) {
|
|
138
149
|
for (const [key, managed] of providerSessions.managed) {
|
|
@@ -150,9 +161,7 @@ export function createCodexProviderSelection({
|
|
|
150
161
|
return {
|
|
151
162
|
threadId: context.threadId,
|
|
152
163
|
async acquireProvider(current) {
|
|
153
|
-
return
|
|
154
|
-
normalizeCodexRunText(sessionId), await context.providerOptions(current)
|
|
155
|
-
));
|
|
164
|
+
return ensureSessionProvider(sessionId, await context.providerOptions(current));
|
|
156
165
|
}
|
|
157
166
|
};
|
|
158
167
|
}
|
|
@@ -160,22 +169,11 @@ export function createCodexProviderSelection({
|
|
|
160
169
|
function codexAppServerSessionObserverOptions(sessionId, providerOptions) {
|
|
161
170
|
return {
|
|
162
171
|
get providerKey() {
|
|
163
|
-
return
|
|
172
|
+
return sessionConnection(sessionId, providerOptions).providerKey;
|
|
164
173
|
}
|
|
165
174
|
};
|
|
166
175
|
}
|
|
167
176
|
|
|
168
|
-
function rememberCodexAppServerPreparedThread(sessionId, providerOptions, context, thread) {
|
|
169
|
-
const connection = providerSessions.context(sessionId, providerOptions);
|
|
170
|
-
connection.runOwner.runtimeLifecycle.rememberManagedSession(connection.providerKey, {
|
|
171
|
-
providerOptions,
|
|
172
|
-
sessionId,
|
|
173
|
-
executionRoot: context.executionRoot,
|
|
174
|
-
threadId: thread.threadId,
|
|
175
|
-
workdir: context.workdir
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
177
|
|
|
180
178
|
async function readExecutionProfileModelCatalog(sessionId, preparation) {
|
|
181
179
|
if (!preparation.assistantScope) {
|
|
@@ -184,9 +182,7 @@ export function createCodexProviderSelection({
|
|
|
184
182
|
restoration.projectRuntimeRoot, restoration.context
|
|
185
183
|
));
|
|
186
184
|
}
|
|
187
|
-
const provider = preparation.provider || await
|
|
188
|
-
providerSessions.context(normalizeCodexRunText(sessionId), await preparation.providerOptions)
|
|
189
|
-
);
|
|
185
|
+
const provider = preparation.provider || await ensureSessionProvider(sessionId, await preparation.providerOptions);
|
|
190
186
|
return readCodexAppServerModelCatalog(provider, { signal: preparation.signal });
|
|
191
187
|
}
|
|
192
188
|
|
|
@@ -196,7 +192,7 @@ export function createCodexProviderSelection({
|
|
|
196
192
|
restoration.projectRuntimeRoot, restoration.context
|
|
197
193
|
));
|
|
198
194
|
const providerOptions = await preparation.providerOptions;
|
|
199
|
-
const context =
|
|
195
|
+
const context = sessionConnection(sessionId, providerOptions);
|
|
200
196
|
const provider = await providerSessions.owner.withLifecycle(
|
|
201
197
|
() => providerSessions.owner.providerForSession(context),
|
|
202
198
|
context.providerKey,
|
|
@@ -216,6 +212,8 @@ export function createCodexProviderSelection({
|
|
|
216
212
|
codexAppServerControlContext,
|
|
217
213
|
codexAppServerObservationRecoveryContext,
|
|
218
214
|
codexAppServerSessionObserverOptions,
|
|
219
|
-
|
|
215
|
+
sessionConnection,
|
|
216
|
+
ensureSessionProvider,
|
|
217
|
+
rememberManagedSession
|
|
220
218
|
};
|
|
221
219
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure,
|
|
1
|
+
import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerErrorMessage } from "./codexTurnState.js";
|
|
2
2
|
import { createCodexAppServerDetachedTurnWatcher, waitForCodexAppServerTurn } from "./codexDetachedTurn.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { codexAppServerDeliveredTurn, sendCodexAppServerPrompt } from "./codexProvider.js";
|
|
4
|
+
import { resumeExactCodexAppServerThread, startFreshCodexAppServerThread } from "./codexRenewalThreads.js";
|
|
5
|
+
import { codexAppServerProviderTurnId, inspectCodexAppServerRenewalThread } from "./codexEvents.js";
|
|
5
6
|
|
|
6
7
|
// Private composition of unchanged operations from the original run owner.
|
|
7
8
|
export function createCodexRenewalCommands({
|
|
@@ -41,7 +42,9 @@ export function createCodexRenewalCommands({
|
|
|
41
42
|
|
|
42
43
|
observer.subscribeEvents(sessionId, provider, threadId,
|
|
43
44
|
selection.codexAppServerSessionObserverOptions(sessionId, context.providerOptions));
|
|
44
|
-
selection.
|
|
45
|
+
selection.rememberManagedSession(sessionId, context.providerOptions, {
|
|
46
|
+
executionRoot: context.executionRoot, workdir: context.workdir, threadId: resumed.threadId
|
|
47
|
+
});
|
|
45
48
|
|
|
46
49
|
let result = null;
|
|
47
50
|
let reconciled = Boolean(targetTurn);
|
|
@@ -112,8 +115,8 @@ export function createCodexRenewalCommands({
|
|
|
112
115
|
provider,
|
|
113
116
|
threadId
|
|
114
117
|
}, context);
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
const delivered = codexAppServerDeliveredTurn(delivery);
|
|
119
|
+
({ turnId, status } = delivered);
|
|
117
120
|
if (!turnId) {
|
|
118
121
|
throw errors.create("handover_identity_missing");
|
|
119
122
|
}
|
|
@@ -129,7 +132,7 @@ export function createCodexRenewalCommands({
|
|
|
129
132
|
watcher.failAfterDetailGrace(errors.create(
|
|
130
133
|
"failed",
|
|
131
134
|
{ status },
|
|
132
|
-
|
|
135
|
+
delivered.error
|
|
133
136
|
));
|
|
134
137
|
} else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
|
|
135
138
|
await watcher.completeNow(status);
|
|
@@ -250,8 +253,8 @@ export function createCodexRenewalCommands({
|
|
|
250
253
|
readOnly: true,
|
|
251
254
|
threadId
|
|
252
255
|
}, context);
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
const delivered = codexAppServerDeliveredTurn(delivery);
|
|
257
|
+
({ turnId, status } = delivered);
|
|
255
258
|
if (!turnId) {
|
|
256
259
|
throw errors.create("seed_identity_missing");
|
|
257
260
|
}
|
|
@@ -262,7 +265,7 @@ export function createCodexRenewalCommands({
|
|
|
262
265
|
watcher.failAfterDetailGrace(errors.create(
|
|
263
266
|
"failed",
|
|
264
267
|
{ status },
|
|
265
|
-
|
|
268
|
+
delivered.error
|
|
266
269
|
));
|
|
267
270
|
} else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
|
|
268
271
|
await watcher.completeNow(status);
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { normalizeNullableText as normalizeAgentText } from "./normalize.js";
|
|
2
|
+
import { codexAppServerProjectHookTrustConfig, codexAppServerTextHasControlCharacters } from "./codexConfiguration.js";
|
|
3
|
+
import {
|
|
4
|
+
codexAppServerRequestIsInvalid,
|
|
5
|
+
CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH,
|
|
6
|
+
CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT
|
|
7
|
+
} from "./codexProvider.js";
|
|
8
|
+
|
|
9
|
+
// Renewal thread operations: resume the exact retained thread, or prove a
|
|
10
|
+
// genuinely fresh successor thread for one renewal operation.
|
|
11
|
+
const CODEX_RENEWAL_UNMATERIALIZED_THREAD_SUFFIX =
|
|
12
|
+
"is not materialized yet; includeTurns is unavailable before first user message";
|
|
13
|
+
|
|
14
|
+
export function defineCodexRenewalThreadIds(value = [], {
|
|
15
|
+
errorCode = "codex_renewal_fresh_thread_required"
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (
|
|
18
|
+
!Array.isArray(value) ||
|
|
19
|
+
value.length > CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT
|
|
20
|
+
) {
|
|
21
|
+
throw codexRenewalThreadError(
|
|
22
|
+
errorCode,
|
|
23
|
+
"The successor assistant thread inventory is invalid."
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const threadIds = value.map((threadId) => normalizeAgentText(threadId));
|
|
27
|
+
if (
|
|
28
|
+
threadIds.some((threadId) => (
|
|
29
|
+
!threadId ||
|
|
30
|
+
threadId.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH ||
|
|
31
|
+
codexAppServerTextHasControlCharacters(threadId)
|
|
32
|
+
)) ||
|
|
33
|
+
new Set(threadIds).size !== threadIds.length
|
|
34
|
+
) {
|
|
35
|
+
throw codexRenewalThreadError(
|
|
36
|
+
errorCode,
|
|
37
|
+
"The successor assistant thread inventory contains an invalid identity."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return Object.freeze([...threadIds].sort());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function listCodexRenewalThreadIds(provider, workdir, errorCode) {
|
|
44
|
+
if (typeof provider?.listAppServerThreadsForCwd !== "function") {
|
|
45
|
+
throw codexRenewalThreadError(
|
|
46
|
+
errorCode,
|
|
47
|
+
"The assistant provider cannot inventory the successor's exact session threads."
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const normalizedWorkdir = normalizeAgentText(workdir);
|
|
51
|
+
const inventory = await provider.listAppServerThreadsForCwd({
|
|
52
|
+
cwd: normalizedWorkdir
|
|
53
|
+
});
|
|
54
|
+
if (normalizeAgentText(inventory?.cwd) !== normalizedWorkdir) {
|
|
55
|
+
throw codexRenewalThreadError(
|
|
56
|
+
errorCode,
|
|
57
|
+
"The assistant provider returned a thread inventory for a different session source."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return defineCodexRenewalThreadIds(inventory?.threadIds, { errorCode });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertCodexRenewalThreadSnapshot(threadSnapshot = null, {
|
|
64
|
+
threadId = "",
|
|
65
|
+
workdir = "",
|
|
66
|
+
errorCode
|
|
67
|
+
} = {}) {
|
|
68
|
+
const expectedThreadId = normalizeAgentText(threadId);
|
|
69
|
+
const expectedWorkdir = normalizeAgentText(workdir);
|
|
70
|
+
const actualThreadId = codexAppServerThreadResponseId(threadSnapshot);
|
|
71
|
+
const actualWorkdir = normalizeAgentText(
|
|
72
|
+
threadSnapshot?.cwd ||
|
|
73
|
+
threadSnapshot?.raw?.cwd ||
|
|
74
|
+
threadSnapshot?.response?.thread?.cwd
|
|
75
|
+
);
|
|
76
|
+
if (actualThreadId !== expectedThreadId || actualWorkdir !== expectedWorkdir) {
|
|
77
|
+
throw codexRenewalThreadError(
|
|
78
|
+
errorCode,
|
|
79
|
+
"The assistant provider could not verify the exact successor thread and session source.",
|
|
80
|
+
{
|
|
81
|
+
actualThreadId,
|
|
82
|
+
actualWorkdir,
|
|
83
|
+
expectedThreadId,
|
|
84
|
+
expectedWorkdir
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return threadSnapshot;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function codexRenewalThreadNeedsStatusRead(error = null, threadId = "") {
|
|
92
|
+
const normalizedThreadId = normalizeAgentText(threadId);
|
|
93
|
+
return Boolean(
|
|
94
|
+
normalizedThreadId &&
|
|
95
|
+
(
|
|
96
|
+
(
|
|
97
|
+
codexAppServerRequestIsInvalid(error, "thread/read") &&
|
|
98
|
+
normalizeAgentText(error?.message) ===
|
|
99
|
+
`thread ${normalizedThreadId} ${CODEX_RENEWAL_UNMATERIALIZED_THREAD_SUFFIX}`
|
|
100
|
+
) ||
|
|
101
|
+
(
|
|
102
|
+
Number(error?.code) === -32601 &&
|
|
103
|
+
normalizeAgentText(error?.method) === "thread/read"
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readCodexRenewalSuccessorThreadSnapshot({
|
|
110
|
+
provider,
|
|
111
|
+
threadId = "",
|
|
112
|
+
workdir = "",
|
|
113
|
+
errorCode
|
|
114
|
+
} = {}) {
|
|
115
|
+
try {
|
|
116
|
+
return assertCodexRenewalThreadSnapshot(
|
|
117
|
+
await provider.readThread(threadId),
|
|
118
|
+
{ threadId, workdir, errorCode }
|
|
119
|
+
);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (
|
|
122
|
+
!codexRenewalThreadNeedsStatusRead(error, threadId) ||
|
|
123
|
+
typeof provider?.readThreadStatus !== "function"
|
|
124
|
+
) {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return assertCodexRenewalThreadSnapshot(
|
|
128
|
+
await provider.readThreadStatus(threadId),
|
|
129
|
+
{ threadId, workdir, errorCode }
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function codexRenewalThreadError(code, message, details = {}, {
|
|
135
|
+
retryable = false
|
|
136
|
+
} = {}) {
|
|
137
|
+
const error = new Error(message);
|
|
138
|
+
error.code = code;
|
|
139
|
+
error.details = {
|
|
140
|
+
...details,
|
|
141
|
+
retryable
|
|
142
|
+
};
|
|
143
|
+
error.retryable = retryable;
|
|
144
|
+
return error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function codexAppServerThreadResponseId(thread = null, fallback = "") {
|
|
148
|
+
return normalizeAgentText(
|
|
149
|
+
thread?.id ||
|
|
150
|
+
thread?.response?.thread?.id ||
|
|
151
|
+
fallback
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Resume only the supplied retained thread; native read failures never replace it. */
|
|
156
|
+
export async function resumeExactCodexAppServerThread({
|
|
157
|
+
expectedThreadId = "",
|
|
158
|
+
provider,
|
|
159
|
+
settings,
|
|
160
|
+
projectHooks = false,
|
|
161
|
+
workdir = "",
|
|
162
|
+
errorCode = "codex_renewal_thread_unreadable"
|
|
163
|
+
} = {}) {
|
|
164
|
+
const normalizedWorkdir = normalizeAgentText(workdir);
|
|
165
|
+
const normalizedExpectedThreadId = normalizeAgentText(expectedThreadId);
|
|
166
|
+
if (
|
|
167
|
+
typeof provider?.ensureRuntime !== "function" ||
|
|
168
|
+
typeof provider?.resumeThread !== "function" ||
|
|
169
|
+
typeof provider?.readThread !== "function"
|
|
170
|
+
) {
|
|
171
|
+
throw codexRenewalThreadError(
|
|
172
|
+
errorCode,
|
|
173
|
+
"The old assistant provider cannot read its exact main thread.",
|
|
174
|
+
{ expectedThreadId: normalizedExpectedThreadId }
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const appServerRuntime = await provider.ensureRuntime();
|
|
178
|
+
const config = projectHooks
|
|
179
|
+
? await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir)
|
|
180
|
+
: null;
|
|
181
|
+
const { threadSettings } = await settings(normalizedWorkdir, config);
|
|
182
|
+
let thread = null;
|
|
183
|
+
let threadSnapshot = null;
|
|
184
|
+
try {
|
|
185
|
+
thread = await provider.resumeThread(normalizedExpectedThreadId, threadSettings);
|
|
186
|
+
const resumedThreadId = codexAppServerThreadResponseId(thread, normalizedExpectedThreadId);
|
|
187
|
+
if (resumedThreadId !== normalizedExpectedThreadId) {
|
|
188
|
+
throw codexRenewalThreadError(
|
|
189
|
+
errorCode,
|
|
190
|
+
"The old assistant provider resumed a different thread.",
|
|
191
|
+
{
|
|
192
|
+
expectedThreadId: normalizedExpectedThreadId,
|
|
193
|
+
resumedThreadId
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
threadSnapshot = await provider.readThread(normalizedExpectedThreadId);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (
|
|
200
|
+
error?.code === errorCode ||
|
|
201
|
+
codexAppServerRequestIsInvalid(error, "thread/resume") ||
|
|
202
|
+
codexAppServerRequestIsInvalid(error, "thread/read")
|
|
203
|
+
) {
|
|
204
|
+
if (error?.code === errorCode) {
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
throw codexRenewalThreadError(
|
|
208
|
+
errorCode,
|
|
209
|
+
"The old assistant thread cannot be read. Write or edit the handover manually instead.",
|
|
210
|
+
{
|
|
211
|
+
expectedThreadId: normalizedExpectedThreadId,
|
|
212
|
+
providerError: normalizeAgentText(error?.message)
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
return Object.freeze({
|
|
219
|
+
appServerRuntime,
|
|
220
|
+
thread,
|
|
221
|
+
threadId: normalizedExpectedThreadId,
|
|
222
|
+
threadSnapshot,
|
|
223
|
+
threadSettings
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Preserve the original baseline → create/adopt → durable identity → first-read order. */
|
|
228
|
+
export async function startFreshCodexAppServerThread({
|
|
229
|
+
provider,
|
|
230
|
+
settings,
|
|
231
|
+
projectHooks = false,
|
|
232
|
+
identity,
|
|
233
|
+
resumableThreadId = "",
|
|
234
|
+
forbiddenThreadId = "",
|
|
235
|
+
operationId = "",
|
|
236
|
+
workdir = "",
|
|
237
|
+
errorCode = "codex_renewal_fresh_thread_required",
|
|
238
|
+
applicationName = "The application"
|
|
239
|
+
} = {}) {
|
|
240
|
+
const normalizedWorkdir = normalizeAgentText(workdir);
|
|
241
|
+
const normalizedForbiddenThreadId = normalizeAgentText(forbiddenThreadId);
|
|
242
|
+
const normalizedOperationId = normalizeAgentText(operationId);
|
|
243
|
+
if (resumableThreadId && resumableThreadId === normalizedForbiddenThreadId) {
|
|
244
|
+
throw codexRenewalThreadError(
|
|
245
|
+
errorCode,
|
|
246
|
+
"The renewed session cannot reuse the old session's assistant thread.",
|
|
247
|
+
{ forbiddenThreadId: normalizedForbiddenThreadId }
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (
|
|
251
|
+
typeof provider?.ensureRuntime !== "function" ||
|
|
252
|
+
typeof provider?.resumeThread !== "function" ||
|
|
253
|
+
typeof provider?.readThread !== "function" ||
|
|
254
|
+
typeof provider?.startThread !== "function"
|
|
255
|
+
) {
|
|
256
|
+
throw codexRenewalThreadError(
|
|
257
|
+
errorCode,
|
|
258
|
+
"The assistant provider cannot prove a genuinely fresh renewal thread."
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const appServerRuntime = await provider.ensureRuntime();
|
|
262
|
+
const config = projectHooks
|
|
263
|
+
? await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir)
|
|
264
|
+
: null;
|
|
265
|
+
const { threadSettings, threadStartSettings } = await settings(normalizedWorkdir, config);
|
|
266
|
+
let fresh = false;
|
|
267
|
+
let thread = null;
|
|
268
|
+
let threadSnapshot = null;
|
|
269
|
+
let baselineThreadIds = Object.freeze([]);
|
|
270
|
+
if (resumableThreadId) {
|
|
271
|
+
try {
|
|
272
|
+
thread = await provider.resumeThread(resumableThreadId, threadSettings);
|
|
273
|
+
const resumedThreadId = codexAppServerThreadResponseId(thread, resumableThreadId);
|
|
274
|
+
if (resumedThreadId !== resumableThreadId) {
|
|
275
|
+
throw codexRenewalThreadError(
|
|
276
|
+
errorCode,
|
|
277
|
+
"The assistant provider resumed a different renewal thread.",
|
|
278
|
+
{
|
|
279
|
+
expectedThreadId: resumableThreadId,
|
|
280
|
+
resumedThreadId
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
threadSnapshot = await readCodexRenewalSuccessorThreadSnapshot({
|
|
285
|
+
provider,
|
|
286
|
+
threadId: resumableThreadId,
|
|
287
|
+
workdir: normalizedWorkdir,
|
|
288
|
+
errorCode
|
|
289
|
+
});
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (
|
|
292
|
+
error?.code === errorCode ||
|
|
293
|
+
codexAppServerRequestIsInvalid(error, "thread/resume") ||
|
|
294
|
+
codexAppServerRequestIsInvalid(error, "thread/read")
|
|
295
|
+
) {
|
|
296
|
+
if (error?.code === errorCode) {
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
throw codexRenewalThreadError(
|
|
300
|
+
errorCode,
|
|
301
|
+
`The previously started renewal thread is no longer readable; ${applicationName} will not replace it silently.`,
|
|
302
|
+
{
|
|
303
|
+
expectedThreadId: resumableThreadId,
|
|
304
|
+
providerError: normalizeAgentText(error?.message)
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
if (!normalizedOperationId) {
|
|
312
|
+
throw codexRenewalThreadError(
|
|
313
|
+
errorCode,
|
|
314
|
+
"Starting a fresh successor thread requires its exact renewal operation id."
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
baselineThreadIds = await identity.readBaseline();
|
|
318
|
+
if (!baselineThreadIds) {
|
|
319
|
+
baselineThreadIds = await listCodexRenewalThreadIds(
|
|
320
|
+
provider,
|
|
321
|
+
normalizedWorkdir,
|
|
322
|
+
errorCode
|
|
323
|
+
);
|
|
324
|
+
await identity.writeBaseline(baselineThreadIds);
|
|
325
|
+
}
|
|
326
|
+
const currentThreadIds = await listCodexRenewalThreadIds(
|
|
327
|
+
provider,
|
|
328
|
+
normalizedWorkdir,
|
|
329
|
+
errorCode
|
|
330
|
+
);
|
|
331
|
+
const baseline = new Set(baselineThreadIds);
|
|
332
|
+
const candidateThreadIds = currentThreadIds.filter((threadId) => !baseline.has(threadId));
|
|
333
|
+
if (candidateThreadIds.length > 1) {
|
|
334
|
+
throw codexRenewalThreadError(
|
|
335
|
+
errorCode,
|
|
336
|
+
`More than one unclaimed assistant thread appeared for this renewal; ${applicationName} will not guess which one is authoritative.`,
|
|
337
|
+
{ candidateThreadIds }
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (candidateThreadIds.length === 1) {
|
|
341
|
+
const [candidateThreadId] = candidateThreadIds;
|
|
342
|
+
thread = await provider.resumeThread(candidateThreadId, threadSettings);
|
|
343
|
+
const resumedThreadId = codexAppServerThreadResponseId(thread, candidateThreadId);
|
|
344
|
+
if (resumedThreadId !== candidateThreadId) {
|
|
345
|
+
throw codexRenewalThreadError(
|
|
346
|
+
errorCode,
|
|
347
|
+
"The assistant provider resumed a different recovered renewal thread.",
|
|
348
|
+
{
|
|
349
|
+
expectedThreadId: candidateThreadId,
|
|
350
|
+
resumedThreadId
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
threadSnapshot = await readCodexRenewalSuccessorThreadSnapshot({
|
|
355
|
+
provider,
|
|
356
|
+
threadId: candidateThreadId,
|
|
357
|
+
workdir: normalizedWorkdir,
|
|
358
|
+
errorCode
|
|
359
|
+
});
|
|
360
|
+
} else {
|
|
361
|
+
thread = await provider.startThread(threadStartSettings);
|
|
362
|
+
fresh = true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const threadId = codexAppServerThreadResponseId(thread, resumableThreadId);
|
|
366
|
+
if (
|
|
367
|
+
!threadId ||
|
|
368
|
+
threadId === normalizedForbiddenThreadId ||
|
|
369
|
+
(fresh && baselineThreadIds.includes(threadId))
|
|
370
|
+
) {
|
|
371
|
+
throw codexRenewalThreadError(
|
|
372
|
+
errorCode,
|
|
373
|
+
threadId
|
|
374
|
+
? "The assistant provider reused the old session's thread instead of starting a fresh one."
|
|
375
|
+
: "The assistant provider did not return a fresh renewal thread id.",
|
|
376
|
+
{
|
|
377
|
+
forbiddenThreadId: normalizedForbiddenThreadId,
|
|
378
|
+
threadId
|
|
379
|
+
}
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
await identity.write({ appServerRuntime, threadId, workdir: normalizedWorkdir });
|
|
383
|
+
if (fresh) {
|
|
384
|
+
try {
|
|
385
|
+
threadSnapshot = await readCodexRenewalSuccessorThreadSnapshot({
|
|
386
|
+
provider,
|
|
387
|
+
threadId,
|
|
388
|
+
workdir: normalizedWorkdir,
|
|
389
|
+
errorCode
|
|
390
|
+
});
|
|
391
|
+
} catch (error) {
|
|
392
|
+
throw codexRenewalThreadError(
|
|
393
|
+
errorCode,
|
|
394
|
+
`The newly started renewal thread cannot be read; ${applicationName} will not replace it silently.`,
|
|
395
|
+
{
|
|
396
|
+
providerError: normalizeAgentText(error?.message),
|
|
397
|
+
threadId
|
|
398
|
+
},
|
|
399
|
+
{ retryable: true }
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return Object.freeze({
|
|
404
|
+
appServerRuntime,
|
|
405
|
+
fresh,
|
|
406
|
+
thread,
|
|
407
|
+
threadId,
|
|
408
|
+
threadSnapshot,
|
|
409
|
+
threadStartSettings,
|
|
410
|
+
threadSettings
|
|
411
|
+
});
|
|
412
|
+
}
|