@jskit-ai/assistant-core 0.1.209 → 0.1.212
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/messageDelivery.js +7 -1
- package/src/server/conversation/codexHistoryAdapter.js +55 -2
- package/src/server/conversation/codexMessageCommands.js +2 -0
- package/src/server/conversation/codexMessageDelivery.js +7 -2
- package/src/server/conversation/codexNotificationObserver.js +15 -2
- package/src/server/conversation/codexProvider.js +64 -7
- package/src/server/conversation/codexTurn.js +16 -2
- package/src/server/conversation/codexTurnRecovery.js +104 -18
- package/src/server/conversation/continuity.js +3 -0
- package/src/server/conversation/openCodeRuntime.js +4 -0
- package/src/server/conversation/providers/codexDriver.js +106 -36
- package/src/server/conversation/providers/opencodeDriver.js +94 -37
- package/src/server/conversation/runtime.js +67 -7
- package/src/server/conversation/tools.js +5 -2
- package/src/server/conversation/transcript.js +2 -2
- package/src/server/hosts/openCodeProcess.js +1 -0
- package/test/claudeConversationSession.test.js +49 -0
- package/test/codexConversationSession.test.js +346 -0
- package/test/codexHistoryAdapter.test.js +103 -0
- package/test/codexTurn.test.js +542 -0
- package/test/conversationSession.test.js +19 -0
- package/test/conversationTools.test.js +298 -0
- package/test/messageDelivery.test.js +18 -0
- package/test/openCodeRuntime.test.js +29 -0
- package/test/opencodeConversationSession.test.js +354 -0
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/kernel": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.1.212",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.232",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.234",
|
|
14
14
|
"@mdi/js": "^7.4.47",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.1.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.1.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.1.51",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.51"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.212",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
48
48
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
49
49
|
"@ai-sdk/xai": "^4.0.58",
|
|
50
|
-
"@jskit-ai/resource-core": "0.1.
|
|
51
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
50
|
+
"@jskit-ai/resource-core": "0.1.176",
|
|
51
|
+
"@jskit-ai/resource-crud-core": "0.1.176",
|
|
52
52
|
"@mdi/js": "^7.4.47",
|
|
53
53
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
54
54
|
"ai": "^7.0.101",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"ws": "^8.20.1"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
61
|
-
"@jskit-ai/kernel": "0.1.
|
|
60
|
+
"@jskit-ai/http-runtime": "0.1.232",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.234",
|
|
62
62
|
"vue": "^3.5.13",
|
|
63
63
|
"vuetify": "^4.0.0"
|
|
64
64
|
},
|
|
@@ -190,6 +190,12 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
function rejectUnsent(messageId, error) {
|
|
194
|
+
if (find(messageId)?.status !== "uncertain") return false;
|
|
195
|
+
fail(messageId, error);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
193
199
|
function restoreUncertain(request, savedTurns = []) {
|
|
194
200
|
const messageId = messageText(request?.messageId);
|
|
195
201
|
if (!messageId || savedTurns.some(turn => (turn.user || turn.system)?.messageId === messageId &&
|
|
@@ -236,7 +242,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
236
242
|
state.sending = false;
|
|
237
243
|
}
|
|
238
244
|
|
|
239
|
-
return { accept, cancel, edit, find, reconcile, remove, resend, reset, restoreUncertain, send, state, turns };
|
|
245
|
+
return { accept, cancel, edit, find, reconcile, remove, resend, reset, restoreUncertain, rejectUnsent, send, state, turns };
|
|
240
246
|
}
|
|
241
247
|
|
|
242
248
|
export { createAssistantMessageDelivery, unmatchedOptimisticMessages };
|
|
@@ -2,6 +2,7 @@ import { open, realpath } from "node:fs/promises";
|
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
6
|
import { gunzipSync, zstdDecompressSync } from "node:zlib";
|
|
6
7
|
import { nativeAiModel, nativeAiProvider } from "../../shared/nativeProviders.js";
|
|
7
8
|
|
|
@@ -112,6 +113,12 @@ function compactionHistoryError(reason, statusCode = 422) {
|
|
|
112
113
|
), { statusCode });
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
function retainedUserRecord(item) {
|
|
117
|
+
if (item?.type !== "message" || item.role !== "user") return null;
|
|
118
|
+
const { encrypted_content, id, ...record } = item;
|
|
119
|
+
return record;
|
|
120
|
+
}
|
|
121
|
+
|
|
115
122
|
// Scan a fixed snapshot without retaining the lifetime transcript. A single
|
|
116
123
|
// record still has a transport-sized bound; incomplete native appends are ignored.
|
|
117
124
|
async function* readCodexHistoryRows(file, start, end, signal) {
|
|
@@ -189,6 +196,7 @@ async function restoreCompactedHistory(body, { destination, historyPath, codexHo
|
|
|
189
196
|
boundary.count += 1;
|
|
190
197
|
boundary.start = readableStart;
|
|
191
198
|
boundary.end = offset;
|
|
199
|
+
boundary.retainedUsers = replacement.map(retainedUserRecord).filter(Boolean);
|
|
192
200
|
}
|
|
193
201
|
}
|
|
194
202
|
if (typeof row.payload?.message === "string" && row.payload.message.trim() &&
|
|
@@ -225,17 +233,30 @@ async function restoreCompactedHistory(body, { destination, historyPath, codexHo
|
|
|
225
233
|
// Count the original request plus each added item and its separating comma.
|
|
226
234
|
// These are transport bytes, not a token estimate for the destination model.
|
|
227
235
|
let restoredBytes = Buffer.byteLength(JSON.stringify(body));
|
|
228
|
-
for (const item of body.input) {
|
|
236
|
+
for (const [index, item] of body.input.entries()) {
|
|
229
237
|
input.push(item);
|
|
230
238
|
if (item?.type !== "compaction") continue;
|
|
231
239
|
const history = boundaries.get(item.encrypted_content).history;
|
|
240
|
+
// Only copies retained by this exact native boundary and still preceding
|
|
241
|
+
// it in the request can replace an archived occurrence. A later user turn
|
|
242
|
+
// with the same words is a distinct instruction, not a retained copy.
|
|
243
|
+
const requestUsers = body.input.slice(0, index).map(retainedUserRecord).filter(Boolean);
|
|
244
|
+
const retainedUsers = boundaries.get(item.encrypted_content).retainedUsers.filter((record) => {
|
|
245
|
+
const index = requestUsers.findIndex((requestRecord) => isDeepStrictEqual(requestRecord, record));
|
|
246
|
+
if (index < 0) return false;
|
|
247
|
+
requestUsers.splice(index, 1);
|
|
248
|
+
return true;
|
|
249
|
+
});
|
|
232
250
|
const readable = [];
|
|
233
251
|
const images = [];
|
|
252
|
+
let retainedCopies = 0;
|
|
234
253
|
for (const old of history) {
|
|
235
254
|
if (!["message", "agent_message", "reasoning", "function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output"].includes(old?.type)) {
|
|
236
255
|
throw compactionHistoryError("the saved history contains an unsupported item.");
|
|
237
256
|
}
|
|
238
257
|
const { encrypted_content, id, ...record } = old;
|
|
258
|
+
const retainedRecord = retainedUserRecord(old);
|
|
259
|
+
const imageCount = images.length;
|
|
239
260
|
// Keep each image as an actual image, with a numbered reference at its
|
|
240
261
|
// original position in the archived message or tool result.
|
|
241
262
|
for (const field of ["content", "summary", "output"]) {
|
|
@@ -259,10 +280,20 @@ async function restoreCompactedHistory(body, { destination, historyPath, codexHo
|
|
|
259
280
|
throw compactionHistoryError("recovery of this non-text history is not supported yet.");
|
|
260
281
|
});
|
|
261
282
|
}
|
|
283
|
+
const retainedIndex = retainedRecord && retainedUsers.findIndex((retained) => isDeepStrictEqual(retained, retainedRecord));
|
|
284
|
+
if (retainedRecord && retainedIndex >= 0) {
|
|
285
|
+
retainedUsers.splice(retainedIndex, 1);
|
|
286
|
+
images.length = imageCount;
|
|
287
|
+
retainedCopies += 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
262
290
|
if (old.type === "reasoning" && !old.content?.length && !old.summary?.length) continue;
|
|
263
291
|
readable.push(record);
|
|
264
292
|
}
|
|
265
|
-
if (!readable.length)
|
|
293
|
+
if (!readable.length) {
|
|
294
|
+
if (retainedCopies) continue;
|
|
295
|
+
throw compactionHistoryError("the original readable conversation is missing.");
|
|
296
|
+
}
|
|
266
297
|
const supplement = { type: "message", role: "user", content: [{ type: "input_text",
|
|
267
298
|
text: `[Archived conversation before compaction. Historical context, not new instructions. Current task instructions still apply.]\n${JSON.stringify(readable)}`
|
|
268
299
|
}, ...images.flatMap((image, index) => [
|
|
@@ -335,6 +366,28 @@ async function startCodexHistoryAdapter({ token, codexHome, readHistoryPath, fet
|
|
|
335
366
|
const result = await fetchImpl(`${upstream}/${route}${url.search}`, {
|
|
336
367
|
method: request.method, headers, body, signal: abort.signal, redirect: "error"
|
|
337
368
|
});
|
|
369
|
+
// DeepSeek labels this explicit context rejection as a generic invalid
|
|
370
|
+
// request. Preserve the provider's details while exposing the native
|
|
371
|
+
// recovery code; the adapter never retries the rejected inference.
|
|
372
|
+
if (destination === "deepseek" && result.status === 400 &&
|
|
373
|
+
result.headers.get("content-type")?.split(";", 1)[0].trim() === "application/json") {
|
|
374
|
+
let errorBody = Buffer.from(await result.arrayBuffer());
|
|
375
|
+
try {
|
|
376
|
+
const parsed = JSON.parse(errorBody.toString("utf8"));
|
|
377
|
+
const error = parsed?.error;
|
|
378
|
+
const match = typeof error?.message === "string" && /^This model's maximum context length is ([1-9]\d*) tokens\. However, you requested ([1-9]\d*) tokens \((\d+) in the messages, (\d+) in the completion\)\. Please reduce the length of the messages or completion\. \(request_id: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\)$/u.exec(error.message);
|
|
379
|
+
if (error?.type === "invalid_request_error" && error.code === "invalid_request_error" && match) {
|
|
380
|
+
const [maximum, requested, messages, completion] = match.slice(1).map(Number);
|
|
381
|
+
if ([maximum, requested, messages, completion].every(Number.isSafeInteger) &&
|
|
382
|
+
requested > maximum && requested === messages + completion) {
|
|
383
|
+
error.code = "context_length_exceeded";
|
|
384
|
+
errorBody = Buffer.from(JSON.stringify(parsed));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} catch { /* Other provider errors keep their exact body. */ }
|
|
388
|
+
response.writeHead(result.status, Object.fromEntries(forwardedHeaders(result.headers))).end(errorBody);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
338
391
|
response.writeHead(result.status, Object.fromEntries(forwardedHeaders(result.headers)));
|
|
339
392
|
if (!result.body) { response.end(); return; }
|
|
340
393
|
const stream = Readable.fromWeb(result.body);
|
|
@@ -214,6 +214,7 @@ export function createCodexMessageCommands({
|
|
|
214
214
|
const observerOptions = source.observerOptions || codexAppServerSessionObserverOptions(sessionId, source.providerOptions);
|
|
215
215
|
const thread = await ensureCodexAppServerThread({
|
|
216
216
|
...(source.preparation || facility.preparation(currentSession)),
|
|
217
|
+
...(preparation.applicationTools ? { applicationTools: preparation.applicationTools, providerReady: preparation.providerReady } : {}),
|
|
217
218
|
observeThread: threadId => subscribeCodexAppServerEvents(sessionId, provider, threadId, observerOptions),
|
|
218
219
|
provider,
|
|
219
220
|
workdir: source.workdir ?? workdir
|
|
@@ -390,6 +391,7 @@ export function createCodexMessageCommands({
|
|
|
390
391
|
const selected = await selectCodexAppServerMessageTurn(sessionId, { messageId, startedAt }, context.selection);
|
|
391
392
|
if (Object.hasOwn(selected, "value")) return selected;
|
|
392
393
|
const { provider, threadId, turnId } = selected;
|
|
394
|
+
if (prepare.providerReady) await prepare.providerReady({ provider, threadId });
|
|
393
395
|
const prepared = { ...context, turnOwnership, messageId, actorContext };
|
|
394
396
|
const policy = await prepare.prepareMessage(input, prepared, { starting: false, selected });
|
|
395
397
|
if (policy) return policy;
|
|
@@ -61,7 +61,8 @@ export function createCodexAppServerMessageDelivery({
|
|
|
61
61
|
messageId = "",
|
|
62
62
|
turnMetadata = null,
|
|
63
63
|
attachments = [],
|
|
64
|
-
nativeIdentity = null
|
|
64
|
+
nativeIdentity = null,
|
|
65
|
+
data
|
|
65
66
|
) {
|
|
66
67
|
const normalizedSessionId = normalizeCodexRunText(sessionId);
|
|
67
68
|
const message = normalizeCodexRunText(text);
|
|
@@ -78,12 +79,15 @@ export function createCodexAppServerMessageDelivery({
|
|
|
78
79
|
if (pendingMessage?.recording) {
|
|
79
80
|
return pendingMessage.recording;
|
|
80
81
|
}
|
|
82
|
+
const authored = pendingMessage?.authoredInput;
|
|
83
|
+
const applicationData = authored?.messageId === normalizeCodexRunText(messageId) ? authored.data : data;
|
|
81
84
|
const recording = (async () => {
|
|
82
85
|
const written = await runtime.store.writeConversationUserMessage(normalizedSessionId, {
|
|
83
86
|
attachments,
|
|
84
87
|
nativeIdentity,
|
|
85
88
|
messageId: normalizeCodexRunText(messageId),
|
|
86
89
|
text: message,
|
|
90
|
+
...(applicationData !== undefined ? { data: structuredClone(applicationData) } : {}),
|
|
87
91
|
turnMetadata: messageMetadata.delivered
|
|
88
92
|
? await messageMetadata.delivered(runtime.store, normalizedSessionId, turnMetadata)
|
|
89
93
|
: turnMetadata
|
|
@@ -157,7 +161,8 @@ export function createCodexAppServerMessageDelivery({
|
|
|
157
161
|
await writeCodexAppServerDeliveredUserMessage(
|
|
158
162
|
{ store }, normalizedSessionId, recoveredMessage.displayMessage, receiptId,
|
|
159
163
|
recoveredMessage.turnMetadata, recoveredMessage.displayAttachments,
|
|
160
|
-
{ threadId: normalizedThreadId, turnId: codexAppServerNotificationTurnId(notification) }
|
|
164
|
+
{ threadId: normalizedThreadId, turnId: codexAppServerNotificationTurnId(notification) },
|
|
165
|
+
recoveredMessage.data
|
|
161
166
|
);
|
|
162
167
|
}
|
|
163
168
|
const turn = turnStateFromAgentRun(run || {});
|
|
@@ -250,6 +250,17 @@ export function createCodexNotificationObserver({
|
|
|
250
250
|
});
|
|
251
251
|
}
|
|
252
252
|
const classification = classifyCodexAppServerEvent(notification);
|
|
253
|
+
if (classification.kind === "provider_error" && classification.text &&
|
|
254
|
+
codexAppServerNotificationParams(notification).willRetry !== true) {
|
|
255
|
+
runCodexAppServerNotificationTask(notificationContext, async () => {
|
|
256
|
+
return onNotificationSignal("provider_error", {
|
|
257
|
+
sessionId: normalizedSessionId, threadId: normalizedThreadId, provider,
|
|
258
|
+
turnId: await resolveCodexAppServerTurnId(normalizedSessionId, normalizedThreadId,
|
|
259
|
+
codexAppServerNotificationTurnId(notification)),
|
|
260
|
+
error: classification.text
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
253
264
|
if (classification.kind === "hook_prompt") {
|
|
254
265
|
codexAppServerAutomaticHookThreads.add(normalizedThreadId);
|
|
255
266
|
return;
|
|
@@ -389,7 +400,8 @@ export function createCodexNotificationObserver({
|
|
|
389
400
|
error: codexAppServerNotificationError(notification),
|
|
390
401
|
provider,
|
|
391
402
|
status,
|
|
392
|
-
usageLimitExceeded: codexAppServerNotificationUsageLimitExceeded(notification)
|
|
403
|
+
usageLimitExceeded: codexAppServerNotificationUsageLimitExceeded(notification),
|
|
404
|
+
deferFailureDetails: true
|
|
393
405
|
});
|
|
394
406
|
});
|
|
395
407
|
return;
|
|
@@ -430,7 +442,8 @@ export function createCodexNotificationObserver({
|
|
|
430
442
|
error: codexAppServerNotificationError(notification),
|
|
431
443
|
provider,
|
|
432
444
|
status,
|
|
433
|
-
usageLimitExceeded: codexAppServerNotificationUsageLimitExceeded(notification)
|
|
445
|
+
usageLimitExceeded: codexAppServerNotificationUsageLimitExceeded(notification),
|
|
446
|
+
deferFailureDetails: true
|
|
434
447
|
});
|
|
435
448
|
});
|
|
436
449
|
return;
|
|
@@ -233,6 +233,19 @@ async function deleteCodexAppServerHelperThread({ provider, threadId, turnId = "
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
function codexApplicationToolConfiguration(tools) {
|
|
237
|
+
const dynamicTools = (tools?.schemas || []).map(({ function: tool }) => ({
|
|
238
|
+
type: "function", name: tool.name, description: tool.description, inputSchema: tool.parameters
|
|
239
|
+
}));
|
|
240
|
+
return { dynamicTools, toolSchemaIdentity: createHash("sha256").update(JSON.stringify(dynamicTools)).digest("hex") };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function assertCodexApplicationToolSchemaIdentity(threadId, savedIdentity, toolSchemaIdentity) {
|
|
244
|
+
if (threadId && (savedIdentity || codexApplicationToolConfiguration().toolSchemaIdentity) !== toolSchemaIdentity) {
|
|
245
|
+
throw new Error("This Codex conversation was created with different application tool entry points. Start a new conversation to enable or remove application tools.");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
236
249
|
/** Retained thread selection keeps observation ahead of a resume that can start work. */
|
|
237
250
|
async function ensureCodexAppServerThread({
|
|
238
251
|
observeThread,
|
|
@@ -240,6 +253,8 @@ async function ensureCodexAppServerThread({
|
|
|
240
253
|
settings,
|
|
241
254
|
projectHooks = false,
|
|
242
255
|
identity,
|
|
256
|
+
applicationTools,
|
|
257
|
+
providerReady,
|
|
243
258
|
workdir = "",
|
|
244
259
|
onStage = () => {}
|
|
245
260
|
} = {}) {
|
|
@@ -254,13 +269,23 @@ async function ensureCodexAppServerThread({
|
|
|
254
269
|
const appServerRuntime = availability?.runtime || await provider.ensureRuntime();
|
|
255
270
|
onStage({ durationMs: Date.now() - stageStartedAt, stage: "runtime" });
|
|
256
271
|
const existingThreadId = identity.read(normalizedWorkdir);
|
|
272
|
+
const toolConfiguration = applicationTools ? codexApplicationToolConfiguration(applicationTools) : null;
|
|
273
|
+
if (toolConfiguration) {
|
|
274
|
+
if (typeof identity.readToolSchemaIdentity !== "function") throw new TypeError("Bound Codex application tools require the original schema identity reader and writer.");
|
|
275
|
+
assertCodexApplicationToolSchemaIdentity(existingThreadId, await identity.readToolSchemaIdentity(normalizedWorkdir), toolConfiguration.toolSchemaIdentity);
|
|
276
|
+
}
|
|
257
277
|
let config = null;
|
|
258
278
|
if (projectHooks) {
|
|
259
279
|
stageStartedAt = Date.now();
|
|
260
280
|
config = await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir);
|
|
261
281
|
onStage({ durationMs: Date.now() - stageStartedAt, stage: "hook-config" });
|
|
262
282
|
}
|
|
263
|
-
|
|
283
|
+
let { threadSettings, threadStartSettings } = await settings(normalizedWorkdir, config);
|
|
284
|
+
if (toolConfiguration) {
|
|
285
|
+
threadSettings = { ...threadSettings, dynamicTools: toolConfiguration.dynamicTools };
|
|
286
|
+
threadStartSettings = { ...threadStartSettings, dynamicTools: toolConfiguration.dynamicTools };
|
|
287
|
+
}
|
|
288
|
+
if (providerReady) await providerReady({ provider, threadId: existingThreadId });
|
|
264
289
|
let thread = null;
|
|
265
290
|
stageStartedAt = Date.now();
|
|
266
291
|
if (existingThreadId) {
|
|
@@ -283,7 +308,9 @@ async function ensureCodexAppServerThread({
|
|
|
283
308
|
throw new Error("Codex app-server did not return a thread id.");
|
|
284
309
|
}
|
|
285
310
|
stageStartedAt = Date.now();
|
|
286
|
-
await identity.write({ appServerRuntime, threadId, workdir: normalizedWorkdir
|
|
311
|
+
await identity.write({ appServerRuntime, threadId, workdir: normalizedWorkdir,
|
|
312
|
+
...(toolConfiguration ? { toolSchemaIdentity: toolConfiguration.toolSchemaIdentity } : {}) });
|
|
313
|
+
if (providerReady) await providerReady({ provider, threadId });
|
|
287
314
|
onStage({ durationMs: Date.now() - stageStartedAt, stage: "identity-metadata" });
|
|
288
315
|
return {
|
|
289
316
|
appServerRuntime,
|
|
@@ -1265,6 +1292,7 @@ export class CodexAppServerAgentProvider {
|
|
|
1265
1292
|
this.runtimeStopOwner = null;
|
|
1266
1293
|
this.runtimePromise = null;
|
|
1267
1294
|
this.serverRequestHandler = null;
|
|
1295
|
+
this.threadRequestHandlers = new Map();
|
|
1268
1296
|
this.conversationRuntime = createCodexConversationAdapter({
|
|
1269
1297
|
readInstructions: (params, threadId) => this.options.readInstructions?.(params, threadId),
|
|
1270
1298
|
client: () => this.activeClient(),
|
|
@@ -1427,7 +1455,7 @@ export class CodexAppServerAgentProvider {
|
|
|
1427
1455
|
};
|
|
1428
1456
|
}
|
|
1429
1457
|
|
|
1430
|
-
async handleServerRequest(request = {}) {
|
|
1458
|
+
async handleServerRequest(request = {}, sourceClient = this.client) {
|
|
1431
1459
|
if (
|
|
1432
1460
|
this.isHelperProvider() &&
|
|
1433
1461
|
request.method === CODEX_APP_SERVER_CHATGPT_REFRESH_METHOD
|
|
@@ -1439,6 +1467,15 @@ export class CodexAppServerAgentProvider {
|
|
|
1439
1467
|
error.code = -32601;
|
|
1440
1468
|
throw error;
|
|
1441
1469
|
}
|
|
1470
|
+
const threadHandler = request.method === "item/tool/call" && this.threadRequestHandlers.get(request.params?.threadId);
|
|
1471
|
+
if (threadHandler) {
|
|
1472
|
+
if (sourceClient !== this.client) {
|
|
1473
|
+
const error = new Error("This retired Codex connection does not authorize that native tool request.");
|
|
1474
|
+
error.code = -32601;
|
|
1475
|
+
throw error;
|
|
1476
|
+
}
|
|
1477
|
+
return threadHandler.handler(request);
|
|
1478
|
+
}
|
|
1442
1479
|
if (typeof this.serverRequestHandler === "function") {
|
|
1443
1480
|
return this.serverRequestHandler(request);
|
|
1444
1481
|
}
|
|
@@ -1643,7 +1680,7 @@ export class CodexAppServerAgentProvider {
|
|
|
1643
1680
|
requestTimeoutMs: this.options.requestTimeoutMs,
|
|
1644
1681
|
WebSocketImpl: this.options.WebSocketImpl
|
|
1645
1682
|
});
|
|
1646
|
-
client.setRequestHandler((request) => this.handleServerRequest(request));
|
|
1683
|
+
client.setRequestHandler((request) => this.handleServerRequest(request, client));
|
|
1647
1684
|
let initializeResult = null;
|
|
1648
1685
|
try {
|
|
1649
1686
|
await client.connect();
|
|
@@ -1972,15 +2009,34 @@ export class CodexAppServerAgentProvider {
|
|
|
1972
2009
|
setServerRequestHandler(callback) {
|
|
1973
2010
|
const handler = typeof callback === "function" ? callback : null;
|
|
1974
2011
|
this.serverRequestHandler = handler;
|
|
1975
|
-
|
|
2012
|
+
const client = this.client;
|
|
2013
|
+
client?.setRequestHandler?.((request) => this.handleServerRequest(request, client));
|
|
1976
2014
|
return () => {
|
|
1977
2015
|
if (this.serverRequestHandler === handler) {
|
|
1978
2016
|
this.serverRequestHandler = null;
|
|
1979
|
-
|
|
2017
|
+
const client = this.client;
|
|
2018
|
+
client?.setRequestHandler?.((request) => this.handleServerRequest(request, client));
|
|
1980
2019
|
}
|
|
1981
2020
|
};
|
|
1982
2021
|
}
|
|
1983
2022
|
|
|
2023
|
+
registerThreadRequestHandler(threadId, handler) {
|
|
2024
|
+
if (typeof threadId !== "string" || !threadId.trim() || typeof handler !== "function") {
|
|
2025
|
+
throw new TypeError("Codex tool dispatch requires its exact thread and handler.");
|
|
2026
|
+
}
|
|
2027
|
+
if (this.threadRequestHandlers.has(threadId)) throw new Error("This Codex thread already has an application tool owner.");
|
|
2028
|
+
const registration = { handler };
|
|
2029
|
+
this.threadRequestHandlers.set(threadId, registration);
|
|
2030
|
+
const client = this.client;
|
|
2031
|
+
client?.setRequestHandler?.(request => this.handleServerRequest(request, client));
|
|
2032
|
+
return Object.freeze({
|
|
2033
|
+
isCurrent: () => this.threadRequestHandlers.get(threadId) === registration,
|
|
2034
|
+
release: () => {
|
|
2035
|
+
if (this.threadRequestHandlers.get(threadId) === registration) this.threadRequestHandlers.delete(threadId);
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1984
2040
|
async startThread({ hostContext, ...params } = {}) {
|
|
1985
2041
|
params = await this.conversationRuntime.prepareInstructions({ ...params, hostContext });
|
|
1986
2042
|
delete params.hostContext;
|
|
@@ -2666,6 +2722,7 @@ export class CodexAppServerAgentProvider {
|
|
|
2666
2722
|
this.planUsage = null;
|
|
2667
2723
|
this.planUsagePending = null;
|
|
2668
2724
|
this.notificationSubscribers.clear();
|
|
2725
|
+
this.threadRequestHandlers.clear();
|
|
2669
2726
|
this.client?.close();
|
|
2670
2727
|
this.client = null;
|
|
2671
2728
|
this.initializeResult = null;
|
|
@@ -2704,7 +2761,7 @@ export class CodexAppServerAgentProvider {
|
|
|
2704
2761
|
export {
|
|
2705
2762
|
CODEX_APP_SERVER_INVALID_REQUEST_CODE, assertCodexAuthPreflightReady,
|
|
2706
2763
|
createCodexAppServerModelCatalogCache, codexAppServerProviderConnectionGeneration,
|
|
2707
|
-
ensureCodexAppServerThread, sendCodexAppServerPrompt,
|
|
2764
|
+
ensureCodexAppServerThread, codexApplicationToolConfiguration, assertCodexApplicationToolSchemaIdentity, sendCodexAppServerPrompt,
|
|
2708
2765
|
resumeExactCodexAppServerThread, startFreshCodexAppServerThread,
|
|
2709
2766
|
defineCodexRenewalThreadIds, codexRenewalThreadError,
|
|
2710
2767
|
codexAppServerEndpointForTarget, codexAppServerRequestIsInvalid, codexAppServerThreadIsMissing,
|
|
@@ -110,10 +110,22 @@ export function createCodexAppServerRunOwner({
|
|
|
110
110
|
runInContext = (_context, operation) => operation(),
|
|
111
111
|
notificationQueue = null,
|
|
112
112
|
activeReconcileMs = 2000,
|
|
113
|
+
finalizingGraceMs = 10000,
|
|
114
|
+
finalizingGraceAfterHistoryRead = false,
|
|
115
|
+
failureDetailGraceMs = 0,
|
|
113
116
|
steerFailedCode = "codex_turn_steer_failed",
|
|
114
117
|
interruptFailedCode = "codex_turn_interrupt_failed",
|
|
115
118
|
errorPrefix = ""
|
|
116
119
|
} = {}) {
|
|
120
|
+
if (!Number.isSafeInteger(finalizingGraceMs) || finalizingGraceMs < 1) {
|
|
121
|
+
throw new TypeError("Invalid Codex finalizing grace.");
|
|
122
|
+
}
|
|
123
|
+
if (typeof finalizingGraceAfterHistoryRead !== "boolean") {
|
|
124
|
+
throw new TypeError("Invalid Codex finalizing grace clock.");
|
|
125
|
+
}
|
|
126
|
+
if (!Number.isSafeInteger(failureDetailGraceMs) || failureDetailGraceMs < 0) {
|
|
127
|
+
throw new TypeError("Invalid Codex failure detail grace.");
|
|
128
|
+
}
|
|
117
129
|
if (providerSessions && (serverClosingError || !providerSessions.managed)) {
|
|
118
130
|
providerSessions = { ...providerSessions, managed: providerSessions.managed || new Map() };
|
|
119
131
|
}
|
|
@@ -126,7 +138,6 @@ export function createCodexAppServerRunOwner({
|
|
|
126
138
|
CONTROL_RECONFIGURATION: "control_reconfiguration",
|
|
127
139
|
INTERRUPTED: "interrupted"
|
|
128
140
|
};
|
|
129
|
-
const finalizingGraceMs = 10000;
|
|
130
141
|
const snapshotRecoveryItemLimit = 25;
|
|
131
142
|
const helperOwnershipError = createCodexHelperOwnershipError({ errorPrefix });
|
|
132
143
|
const helperLifecycle = helperThreads ? createCodexHelperThreadLifecycle({
|
|
@@ -165,7 +176,7 @@ export function createCodexAppServerRunOwner({
|
|
|
165
176
|
} = codexProviderSelection;
|
|
166
177
|
const acquireOutputProvider = providerSessions?.outputContext
|
|
167
178
|
? async ({ sessionId, runtime, session }) => {
|
|
168
|
-
const prepared = providerSessions.outputContext({ sessionId, runtime, session });
|
|
179
|
+
const prepared = await providerSessions.outputContext({ sessionId, runtime, session });
|
|
169
180
|
if (!prepared) return null;
|
|
170
181
|
return acquireCodexAppServerOutputProvider(
|
|
171
182
|
sessionId, session, prepared.managedIdentity, prepared.providerOptions
|
|
@@ -285,6 +296,9 @@ export function createCodexAppServerRunOwner({
|
|
|
285
296
|
captureContext,
|
|
286
297
|
runInContext,
|
|
287
298
|
activeReconcileMs,
|
|
299
|
+
finalizingGraceAfterHistoryRead,
|
|
300
|
+
failureDetailGraceMs,
|
|
301
|
+
notificationQueue: codexAppServerNotificationQueue,
|
|
288
302
|
hasRuntime,
|
|
289
303
|
recoverAdmission,
|
|
290
304
|
turnOutcomes,
|