@jskit-ai/assistant-core 0.1.208 → 0.1.211

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.
@@ -8,14 +8,14 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.1.208",
12
- "@jskit-ai/http-runtime": "0.1.228",
13
- "@jskit-ai/kernel": "0.1.230",
11
+ "@jskit-ai/assistant-core": "0.1.211",
12
+ "@jskit-ai/http-runtime": "0.1.231",
13
+ "@jskit-ai/kernel": "0.1.233",
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.47",
18
- "@jskit-ai/connectors-catalog": "0.1.47"
17
+ "@jskit-ai/connectors-core": "0.1.50",
18
+ "@jskit-ai/connectors-catalog": "0.1.50"
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.208",
3
+ "version": "0.1.211",
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.172",
51
- "@jskit-ai/resource-crud-core": "0.1.172",
50
+ "@jskit-ai/resource-core": "0.1.175",
51
+ "@jskit-ai/resource-crud-core": "0.1.175",
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.228",
61
- "@jskit-ai/kernel": "0.1.230",
60
+ "@jskit-ai/http-runtime": "0.1.231",
61
+ "@jskit-ai/kernel": "0.1.233",
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) throw compactionHistoryError("the original readable conversation is missing.");
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
- const { threadSettings, threadStartSettings } = await settings(normalizedWorkdir, config);
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
- this.client?.setRequestHandler?.((request) => this.handleServerRequest(request));
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
- this.client?.setRequestHandler?.((request) => this.handleServerRequest(request));
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,