codex-relay 1.4.9 → 1.4.11

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.
@@ -250,6 +250,7 @@ const PendingInputRequestQuestionSchema = z.object({
250
250
  });
251
251
  const PendingInputRequestSchema = z.object({
252
252
  id: z.string().min(1),
253
+ isBlocking: z.boolean().default(true),
253
254
  questions: z.array(PendingInputRequestQuestionSchema),
254
255
  threadId: z.string().min(1),
255
256
  turnId: z.string().optional()
package/dist/src.js CHANGED
@@ -3,7 +3,7 @@ import { a as getConnectUrlCandidates, c as permanentClientSessionExpiresAt, i a
3
3
  import { createRequire } from "node:module";
4
4
  import qrcode from "qrcode-terminal";
5
5
  import { execFile, spawn } from "node:child_process";
6
- import fs, { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
6
+ import fs, { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync, writeFileSync } from "node:fs";
7
7
  import { access, appendFile, copyFile, mkdir, open, readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
8
8
  import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
@@ -36,6 +36,7 @@ const stdioAppServerArgs = [
36
36
  ];
37
37
  const sharedWindowsServerUrl = "ws://127.0.0.1:8788";
38
38
  const windowsShellExtensions = new Set([".bat", ".cmd"]);
39
+ const require = createRequire(import.meta.url);
39
40
  function resolveCodexAppServerMode(input = {}) {
40
41
  const env = input.env ?? process.env;
41
42
  const platform$2 = input.platform ?? platform();
@@ -59,35 +60,46 @@ function resolveCodexAppServerMode(input = {}) {
59
60
  }
60
61
  function resolveCodexAppServerSpawn(input = {}) {
61
62
  const platform$3 = input.platform ?? platform();
62
- const command = resolveCodexBinary(input.env ?? process.env);
63
+ const executable = resolveCodexExecutable(input.env ?? process.env);
63
64
  const isWindows = platform$3 === "win32";
64
65
  return {
65
- command,
66
- args: [...stdioAppServerArgs],
67
- shell: isWindows && shouldUseWindowsShell(command),
66
+ command: executable.command,
67
+ args: [...executable.args, ...stdioAppServerArgs],
68
+ shell: isWindows && executable.explicit && shouldUseWindowsShell(executable.command),
68
69
  windowsHide: isWindows
69
70
  };
70
71
  }
71
72
  function resolveCodexSharedAppServerSpawn(input = {}) {
72
73
  const platform$4 = input.platform ?? platform();
73
- const command = resolveCodexBinary(input.env ?? process.env);
74
+ const executable = resolveCodexExecutable(input.env ?? process.env);
74
75
  const isWindows = platform$4 === "win32";
75
76
  return {
76
- command,
77
+ command: executable.command,
77
78
  args: [
79
+ ...executable.args,
78
80
  "app-server",
79
81
  "--listen",
80
82
  resolveCodexSharedAppServerRemoteAddress(platform$4)
81
83
  ],
82
- shell: isWindows && shouldUseWindowsShell(command),
84
+ shell: isWindows && executable.explicit && shouldUseWindowsShell(executable.command),
83
85
  windowsHide: isWindows
84
86
  };
85
87
  }
86
88
  function resolveCodexSharedAppServerRemoteAddress(platform$1 = platform()) {
87
89
  return platform$1 === "win32" ? sharedWindowsServerUrl : "unix://";
88
90
  }
89
- function resolveCodexBinary(env) {
90
- return env.CODEX_BIN?.trim() || "codex";
91
+ function resolveCodexExecutable(env) {
92
+ const configuredBinary = env.CODEX_BIN?.trim();
93
+ if (configuredBinary) return {
94
+ args: [],
95
+ command: configuredBinary,
96
+ explicit: true
97
+ };
98
+ return {
99
+ args: [join(dirname(require.resolve("@openai/codex/package.json")), "bin", "codex.js")],
100
+ command: process.execPath,
101
+ explicit: false
102
+ };
91
103
  }
92
104
  function shouldUseWindowsShell(command) {
93
105
  const extension = extname(command).toLowerCase();
@@ -155,6 +167,9 @@ var CodexAppServerClient = class {
155
167
  sharedReconnectEnabled = false;
156
168
  sharedServer;
157
169
  socket;
170
+ activeTurnIdsByThreadId = /* @__PURE__ */ new Map();
171
+ terminalTurnIdsByThreadId = /* @__PURE__ */ new Map();
172
+ subscribedThreadIds = /* @__PURE__ */ new Set();
158
173
  onStartupFallback;
159
174
  startSharedServer;
160
175
  constructor(options = {}) {
@@ -171,13 +186,33 @@ var CodexAppServerClient = class {
171
186
  return this.ensureInitialized();
172
187
  }
173
188
  async listThreads(limit = 80) {
174
- return (await this.request("thread/list", {
175
- limit,
176
- sortKey: "updated_at",
177
- sortDirection: "desc",
178
- sourceKinds: [],
179
- archived: false
180
- })).data;
189
+ const threads = [];
190
+ const seenCursors = /* @__PURE__ */ new Set();
191
+ let cursor;
192
+ do {
193
+ const response = await this.request("thread/list", {
194
+ archived: false,
195
+ limit,
196
+ sortDirection: "desc",
197
+ sortKey: "recency_at",
198
+ sourceKinds: [
199
+ "cli",
200
+ "vscode",
201
+ "exec",
202
+ "appServer"
203
+ ],
204
+ ...cursor ? {
205
+ cursor,
206
+ useStateDbOnly: true
207
+ } : {}
208
+ });
209
+ threads.push(...response.data);
210
+ const nextCursor = response.nextCursor ?? void 0;
211
+ if (!nextCursor || seenCursors.has(nextCursor)) break;
212
+ seenCursors.add(nextCursor);
213
+ cursor = nextCursor;
214
+ } while (cursor);
215
+ return threads;
181
216
  }
182
217
  async readThread(threadId, options = {}) {
183
218
  return (await this.request("thread/read", {
@@ -195,19 +230,39 @@ var CodexAppServerClient = class {
195
230
  return this.request("account/rateLimits/read", null);
196
231
  }
197
232
  async startThread(params) {
198
- return (await this.request("thread/start", params)).thread;
233
+ const response = await this.request("thread/start", params);
234
+ this.subscribedThreadIds.add(response.thread.id);
235
+ return response.thread;
199
236
  }
200
237
  async resumeThread(params) {
201
- return (await this.request("thread/resume", params)).thread;
238
+ const response = await this.request("thread/resume", params.excludeTurns ? {
239
+ ...params,
240
+ initialTurnsPage: {
241
+ itemsView: "summary",
242
+ limit: 1,
243
+ sortDirection: "desc"
244
+ }
245
+ } : params);
246
+ this.subscribedThreadIds.add(response.thread.id);
247
+ this.rememberActiveTurn({
248
+ ...response.thread,
249
+ turns: response.initialTurnsPage?.data ?? response.thread.turns
250
+ });
251
+ return response.thread;
202
252
  }
203
253
  async startTurn(params) {
204
- return (await this.request("turn/start", params)).turn;
254
+ const response = await this.request("turn/start", params);
255
+ this.rememberTurn(params.threadId, response.turn);
256
+ return response.turn;
205
257
  }
206
258
  async interruptTurn(params) {
207
259
  await this.request("turn/interrupt", params);
208
260
  }
209
261
  async archiveThread(params) {
210
262
  await this.request("thread/archive", params);
263
+ this.subscribedThreadIds.delete(params.threadId);
264
+ this.activeTurnIdsByThreadId.delete(params.threadId);
265
+ this.terminalTurnIdsByThreadId.delete(params.threadId);
211
266
  }
212
267
  async setThreadName(params) {
213
268
  await this.request("thread/name/set", params);
@@ -215,6 +270,9 @@ var CodexAppServerClient = class {
215
270
  async rollbackThread(params) {
216
271
  return (await this.request("thread/rollback", params)).thread;
217
272
  }
273
+ async revertThread(params) {
274
+ return (await this.request("thread/revert", params)).thread;
275
+ }
218
276
  async getThreadGoal(params) {
219
277
  return (await this.request("thread/goal/get", params)).goal;
220
278
  }
@@ -255,6 +313,9 @@ var CodexAppServerClient = class {
255
313
  this.stopStdioCodexAppServer();
256
314
  this.stopSharedCodexAppServer();
257
315
  this.initialized = void 0;
316
+ this.activeTurnIdsByThreadId.clear();
317
+ this.terminalTurnIdsByThreadId.clear();
318
+ this.subscribedThreadIds.clear();
258
319
  }
259
320
  async request(method, params) {
260
321
  await this.ensureInitialized();
@@ -322,8 +383,90 @@ var CodexAppServerClient = class {
322
383
  title: "Codex Relay Mobile Server",
323
384
  version: "1.2.0"
324
385
  },
325
- capabilities: { experimentalApi: true }
386
+ capabilities: {
387
+ experimentalApi: true,
388
+ requestAttestation: false
389
+ }
326
390
  });
391
+ await this.writeJson({ method: "initialized" });
392
+ for (const threadId of this.subscribedThreadIds) try {
393
+ const disconnectedTurnId = this.activeTurnIdsByThreadId.get(threadId);
394
+ const response = await this.requestRaw("thread/resume", {
395
+ excludeTurns: true,
396
+ initialTurnsPage: {
397
+ itemsView: "summary",
398
+ limit: 1,
399
+ sortDirection: "desc"
400
+ },
401
+ threadId
402
+ });
403
+ this.reconcileResumedThread({
404
+ ...response.thread,
405
+ turns: response.initialTurnsPage?.data ?? response.thread.turns
406
+ }, disconnectedTurnId);
407
+ } catch (error) {
408
+ relayDebugLog("app_server.thread.resubscribe_failed", {
409
+ message: error instanceof Error ? error.message : String(error),
410
+ threadId
411
+ });
412
+ }
413
+ }
414
+ reconcileResumedThread(thread, disconnectedTurnId) {
415
+ if (disconnectedTurnId) {
416
+ const disconnectedTurn = thread.turns?.find((turn) => turn.id === disconnectedTurnId);
417
+ if (disconnectedTurn && !isAppServerTurnInProgress(disconnectedTurn)) this.dispatchNotification({
418
+ method: "turn/completed",
419
+ params: {
420
+ threadId: thread.id,
421
+ turn: disconnectedTurn
422
+ }
423
+ });
424
+ else this.dispatchNotification({
425
+ method: "thread/status/changed",
426
+ params: {
427
+ status: thread.status,
428
+ threadId: thread.id
429
+ }
430
+ });
431
+ }
432
+ this.rememberActiveTurn(thread);
433
+ }
434
+ rememberActiveTurn(thread) {
435
+ const activeTurn = [...thread.turns ?? []].reverse().find((turn) => isAppServerTurnInProgress(turn));
436
+ if (activeTurn && this.terminalTurnIdsByThreadId.get(thread.id) !== activeTurn.id) {
437
+ this.activeTurnIdsByThreadId.set(thread.id, activeTurn.id);
438
+ this.terminalTurnIdsByThreadId.delete(thread.id);
439
+ } else this.activeTurnIdsByThreadId.delete(thread.id);
440
+ }
441
+ rememberTurn(threadId, turn) {
442
+ if (isAppServerTurnInProgress(turn)) {
443
+ if (this.terminalTurnIdsByThreadId.get(threadId) !== turn.id) {
444
+ this.activeTurnIdsByThreadId.set(threadId, turn.id);
445
+ this.terminalTurnIdsByThreadId.delete(threadId);
446
+ }
447
+ } else if (this.activeTurnIdsByThreadId.get(threadId) === turn.id) this.activeTurnIdsByThreadId.delete(threadId);
448
+ }
449
+ dispatchNotification(notification) {
450
+ const params = notification.params;
451
+ const record = params && typeof params === "object" ? params : {};
452
+ const threadId = typeof record.threadId === "string" ? record.threadId : void 0;
453
+ const turn = record.turn && typeof record.turn === "object" ? record.turn : void 0;
454
+ const turnId = typeof record.turnId === "string" ? record.turnId : typeof turn?.id === "string" ? turn.id : void 0;
455
+ if (notification.method === "turn/started" && threadId && turnId) {
456
+ if (this.terminalTurnIdsByThreadId.get(threadId) !== turnId) {
457
+ this.activeTurnIdsByThreadId.set(threadId, turnId);
458
+ this.terminalTurnIdsByThreadId.delete(threadId);
459
+ }
460
+ } else if ([
461
+ "turn/completed",
462
+ "turn/failed",
463
+ "turn/aborted",
464
+ "turn/cancelled"
465
+ ].includes(notification.method) && threadId) {
466
+ if (turnId) this.terminalTurnIdsByThreadId.set(threadId, turnId);
467
+ if (!turnId || this.activeTurnIdsByThreadId.get(threadId) === turnId) this.activeTurnIdsByThreadId.delete(threadId);
468
+ }
469
+ for (const handler of this.notificationHandlers) handler(notification);
327
470
  }
328
471
  startStdioCodexAppServer() {
329
472
  const spawnConfig = resolveCodexAppServerSpawn();
@@ -528,7 +671,7 @@ var CodexAppServerClient = class {
528
671
  } catch {
529
672
  return;
530
673
  }
531
- if (typeof message.method === "string" && typeof message.id === "number") {
674
+ if (typeof message.method === "string" && (typeof message.id === "number" || typeof message.id === "string")) {
532
675
  debugAppServer("server-request", message.method, message.id);
533
676
  const request = {
534
677
  id: message.id,
@@ -545,7 +688,7 @@ var CodexAppServerClient = class {
545
688
  method: message.method,
546
689
  params: message.params
547
690
  };
548
- for (const handler of this.notificationHandlers) handler(notification);
691
+ this.dispatchNotification(notification);
549
692
  return;
550
693
  }
551
694
  if (typeof message.id !== "number") return;
@@ -644,6 +787,15 @@ function connectRemoteAddress(remoteAddress) {
644
787
  function sharedCodexAppServerSocketPath() {
645
788
  return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "app-server-control", "app-server-control.sock");
646
789
  }
790
+ function isAppServerTurnInProgress(turn) {
791
+ if (turn.completedAt !== null && turn.completedAt !== void 0) return false;
792
+ const status = typeof turn.status === "string" ? turn.status : turn.status && typeof turn.status === "object" && "type" in turn.status ? String(turn.status.type) : "";
793
+ return [
794
+ "active",
795
+ "inprogress",
796
+ "running"
797
+ ].includes(status.toLowerCase().replace(/[^a-z0-9]/g, ""));
798
+ }
647
799
  function asError(error) {
648
800
  return error instanceof Error ? error : new Error(String(error));
649
801
  }
@@ -672,7 +824,8 @@ function extractStreamText(event) {
672
824
  if (!event || typeof event !== "object") return;
673
825
  const record = event;
674
826
  const item = record.item && typeof record.item === "object" ? record.item : void 0;
675
- const candidate = record.delta ?? record.text ?? record.message ?? item?.text ?? item?.aggregated_output ?? item?.command;
827
+ const error = record.error && typeof record.error === "object" ? record.error : void 0;
828
+ const candidate = record.delta ?? record.text ?? record.message ?? error?.message ?? item?.text ?? item?.message ?? item?.aggregated_output ?? item?.command;
676
829
  return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
677
830
  }
678
831
  function classifyStreamEvent(event) {
@@ -1472,11 +1625,47 @@ function createApp(options = {}) {
1472
1625
  const appServerMutationTails = /* @__PURE__ */ new Map();
1473
1626
  const workspaceTerminalSessions = /* @__PURE__ */ new Map();
1474
1627
  const activeAppServerTurnIdsByThreadId = /* @__PURE__ */ new Map();
1628
+ const appServerHistoryGenerationsByThreadId = /* @__PURE__ */ new Map();
1475
1629
  const appServerHistoryLoadsByThreadId = /* @__PURE__ */ new Map();
1630
+ const appServerRolloutPathsByThreadId = /* @__PURE__ */ new Map();
1476
1631
  const steeringThreads = /* @__PURE__ */ new Set();
1477
1632
  const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
1478
1633
  const activeStreamControllers = /* @__PURE__ */ new Map();
1479
- const threadOptions = { workingDirectory: workspacePath };
1634
+ const threadOptions = {
1635
+ threadSource: "codex-relay",
1636
+ workingDirectory: workspacePath
1637
+ };
1638
+ const advanceAppServerHistoryGeneration = (threadId) => {
1639
+ appServerHistoryGenerationsByThreadId.set(threadId, (appServerHistoryGenerationsByThreadId.get(threadId) ?? 0) + 1);
1640
+ };
1641
+ const invalidateAppServerHistory = (threadId) => {
1642
+ advanceAppServerHistoryGeneration(threadId);
1643
+ appServerHistoryLoadsByThreadId.delete(threadId);
1644
+ appServerRolloutPathsByThreadId.delete(threadId);
1645
+ messagesByThreadId.delete(threadId);
1646
+ };
1647
+ if (appServer && typeof appServer.onNotification === "function") appServer.onNotification((notification) => {
1648
+ const params = recordParams(notification);
1649
+ const threadId = firstString(params, ["threadId"]);
1650
+ if (!threadId) return;
1651
+ if (notification.method === "thread/reverted") {
1652
+ invalidateAppServerHistory(threadId);
1653
+ return;
1654
+ }
1655
+ if (notification.method === "item/completed") {
1656
+ advanceAppServerHistoryGeneration(threadId);
1657
+ const item = params?.item;
1658
+ if (item && typeof item === "object" && item.type !== "userMessage") upsertAppServerItemMessage(messagesByThreadId, threadId, firstString(params, ["turnId"]), item);
1659
+ return;
1660
+ }
1661
+ if (notification.method === "turn/completed") {
1662
+ advanceAppServerHistoryGeneration(threadId);
1663
+ const turn = appServerTurnFromParams(params);
1664
+ if (turn) {
1665
+ for (const item of turn.items) if (item.type !== "userMessage") upsertAppServerItemMessage(messagesByThreadId, threadId, turn.id, item);
1666
+ }
1667
+ }
1668
+ });
1480
1669
  function runThreadOperation(threadId, operation) {
1481
1670
  const result = (threadOperationTails.get(threadId) ?? Promise.resolve()).then(operation, operation);
1482
1671
  const tail = result.then(() => void 0, () => void 0);
@@ -1503,13 +1692,18 @@ function createApp(options = {}) {
1503
1692
  const scheduleAppServerHistoryLoad = (threadId, cachedMessages) => {
1504
1693
  if (!appServer || appServerHistoryLoadsByThreadId.has(threadId)) return;
1505
1694
  const startedAt = Date.now();
1695
+ const historyGeneration = appServerHistoryGenerationsByThreadId.get(threadId) ?? 0;
1506
1696
  relayDebugLog("thread.detail.history.background_started", {
1507
1697
  cachedMessageCount: cachedMessages.length,
1508
1698
  threadId
1509
1699
  });
1510
1700
  const load = appServer.readThread(threadId, { includeTurns: true }).then((threadWithTurns) => {
1511
- const mappedThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: true });
1512
- const messages = mergeAppServerMessagesWithLocalStatus(mapAppServerMessages(threadWithTurns), messagesByThreadId.get(threadId) ?? cachedMessages);
1701
+ if ((appServerHistoryGenerationsByThreadId.get(threadId) ?? 0) !== historyGeneration) return;
1702
+ const hasCompleteHistory = appServerThreadHasCompleteHistory(threadWithTurns);
1703
+ const mappedThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: hasCompleteHistory });
1704
+ const appMessages = mapAppServerMessages(threadWithTurns);
1705
+ const localMessages = messagesByThreadId.get(threadId) ?? cachedMessages;
1706
+ const messages = hasCompleteHistory ? mergeAppServerMessagesWithLocalStatus(appMessages, localMessages) : mergeThreadMessagePages(appMessages, localMessages);
1513
1707
  messagesByThreadId.set(threadId, messages);
1514
1708
  relayDebugLog("thread.detail.history.background_completed", {
1515
1709
  durationMs: Date.now() - startedAt,
@@ -1524,7 +1718,7 @@ function createApp(options = {}) {
1524
1718
  threadId
1525
1719
  });
1526
1720
  }).finally(() => {
1527
- appServerHistoryLoadsByThreadId.delete(threadId);
1721
+ if (appServerHistoryLoadsByThreadId.get(threadId) === load) appServerHistoryLoadsByThreadId.delete(threadId);
1528
1722
  });
1529
1723
  appServerHistoryLoadsByThreadId.set(threadId, load);
1530
1724
  };
@@ -2203,13 +2397,27 @@ function createApp(options = {}) {
2203
2397
  const turns = threadBeforeRewind.turns ?? [];
2204
2398
  const turnIndex = turns.findIndex((turn) => turn.id === parsed.data.turnId);
2205
2399
  if (turnIndex < 0) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", "The selected message is no longer available to rewind."), 404);
2206
- const rolledBackThread = await appServer.rollbackThread({
2400
+ let rolledBackThread;
2401
+ if (threadBeforeRewind.historyMode === "paginated" && typeof appServer.revertThread === "function") try {
2402
+ rolledBackThread = await appServer.revertThread({
2403
+ beforeTurnId: parsed.data.turnId,
2404
+ threadId
2405
+ });
2406
+ } catch (error) {
2407
+ if (!/method not found|unsupported|-32601/i.test(errorMessage(error))) throw error;
2408
+ rolledBackThread = await appServer.rollbackThread({
2409
+ threadId,
2410
+ numTurns: turns.length - turnIndex
2411
+ });
2412
+ }
2413
+ else rolledBackThread = await appServer.rollbackThread({
2207
2414
  threadId,
2208
2415
  numTurns: turns.length - turnIndex
2209
2416
  });
2210
2417
  const threadWithTurns = rolledBackThread.turns === void 0 ? await appServer.readThread(threadId, { includeTurns: true }) : rolledBackThread;
2211
2418
  const thread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: true });
2212
2419
  const messages = mapAppServerMessages(threadWithTurns);
2420
+ invalidateAppServerHistory(threadId);
2213
2421
  messagesByThreadId.set(threadId, messages);
2214
2422
  liveThreads.delete(threadId);
2215
2423
  activeAppServerTurnIdsByThreadId.delete(threadId);
@@ -2239,18 +2447,29 @@ function createApp(options = {}) {
2239
2447
  const thread = await appServer.readThread(threadId, { includeTurns: false });
2240
2448
  if (isSubagentThread(thread)) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
2241
2449
  const mappedThread = rememberAppServerThread(threads, thread);
2242
- const cachedMessages = messagesByThreadId.get(threadId) ?? [];
2450
+ let cachedMessages = messagesByThreadId.get(threadId) ?? [];
2451
+ if (thread.path) {
2452
+ const previousRolloutPath = appServerRolloutPathsByThreadId.get(threadId);
2453
+ if (previousRolloutPath && previousRolloutPath !== thread.path) {
2454
+ invalidateAppServerHistory(threadId);
2455
+ cachedMessages = [];
2456
+ }
2457
+ appServerRolloutPathsByThreadId.set(threadId, thread.path);
2458
+ }
2243
2459
  let loadedMessages = false;
2244
2460
  let messages = cachedMessages;
2245
2461
  let responseThread = preserveKnownRunningThreadState(mappedThread, wasKnownRunning);
2246
2462
  if (forceRefresh && responseThread.state !== "running") {
2247
2463
  const threadWithTurns = await appServer.readThread(threadId, { includeTurns: true });
2248
- responseThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: true });
2249
- messages = mapAppServerMessages(threadWithTurns);
2464
+ const hasCompleteHistory = appServerThreadHasCompleteHistory(threadWithTurns);
2465
+ responseThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: hasCompleteHistory });
2466
+ const appMessages = mapAppServerMessages(threadWithTurns);
2467
+ messages = hasCompleteHistory ? mergeAppServerMessagesWithLocalStatus(appMessages, cachedMessages) : mergeThreadMessagePages(appMessages, cachedMessages);
2250
2468
  messagesByThreadId.set(threadId, messages);
2251
2469
  loadedMessages = true;
2252
2470
  } else {
2253
- const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath);
2471
+ const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath, thread.path ?? void 0);
2472
+ if (rolloutHistory.rolloutPath) appServerRolloutPathsByThreadId.set(threadId, rolloutHistory.rolloutPath);
2254
2473
  if (rolloutHistory.messages.length > 0) {
2255
2474
  messages = mergeThreadMessagePages(rolloutHistory.messages, cachedMessages);
2256
2475
  responseThread = rememberRolloutThreadMessages(threads, responseThread, messages, rolloutHistory.messageCountLowerBound);
@@ -2265,8 +2484,10 @@ function createApp(options = {}) {
2265
2484
  }
2266
2485
  if (!loadedMessages && responseThread.state !== "running") {
2267
2486
  const threadWithTurns = await appServer.readThread(threadId, { includeTurns: true });
2268
- responseThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: true });
2269
- messages = mergeAppServerMessagesWithLocalStatus(mapAppServerMessages(threadWithTurns), cachedMessages);
2487
+ const hasCompleteHistory = appServerThreadHasCompleteHistory(threadWithTurns);
2488
+ responseThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: hasCompleteHistory });
2489
+ const appMessages = mapAppServerMessages(threadWithTurns);
2490
+ messages = hasCompleteHistory ? mergeAppServerMessagesWithLocalStatus(appMessages, cachedMessages) : mergeThreadMessagePages(appMessages, cachedMessages);
2270
2491
  messagesByThreadId.set(threadId, messages);
2271
2492
  loadedMessages = true;
2272
2493
  } else if (!loadedMessages) scheduleAppServerHistoryLoad(threadId, cachedMessages);
@@ -2927,11 +3148,10 @@ async function createAppServerThreadRecord(input) {
2927
3148
  const thread = await input.appServer.startThread({
2928
3149
  approvalPolicy: runtime.approvalPolicy,
2929
3150
  cwd: input.workspacePath,
2930
- experimentalRawEvents: false,
2931
3151
  model: input.options.model ?? null,
2932
- persistExtendedHistory: true,
2933
3152
  sandbox: runtime.sandbox,
2934
- serviceTier: input.options.serviceTier ?? null
3153
+ serviceTier: input.options.serviceTier ?? null,
3154
+ threadSource: "codex-relay"
2935
3155
  });
2936
3156
  const metadata = ThreadSummarySchema.parse({
2937
3157
  ...mapAppServerThread({
@@ -3101,11 +3321,25 @@ async function runPromptStreamed(input) {
3101
3321
  message: assistantMessage
3102
3322
  });
3103
3323
  for await (const event of streamed.events) {
3324
+ activeThreadId = replaceLocalThreadId(input.threads, input.messagesByThreadId, input.liveThreads, activeThreadId, getThreadId(thread));
3104
3325
  const kind = classifyStreamEvent(event);
3105
3326
  const text = extractStreamText(event);
3106
3327
  if (kind === "error") throw new Error(text ?? "Codex run failed.");
3107
- if (!text) continue;
3108
3328
  if (kind === "assistant") {
3329
+ if (!text) continue;
3330
+ if (isCompletedAgentMessageEvent(event)) {
3331
+ assistantMessage = updateMessage(input.messagesByThreadId, activeThreadId, assistantMessage.id, { content: text });
3332
+ threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
3333
+ state: "running",
3334
+ lastResult: assistantMessage.content
3335
+ });
3336
+ sendSse(input.controller, input.encoder, input.secureSession, {
3337
+ type: "thread.message.created",
3338
+ thread: threadSummary,
3339
+ message: assistantMessage
3340
+ });
3341
+ continue;
3342
+ }
3109
3343
  const assistantPatch = appendMessageDelta(input.messagesByThreadId, activeThreadId, assistantMessage.id, text);
3110
3344
  assistantMessage = assistantPatch.message;
3111
3345
  updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
@@ -3119,14 +3353,18 @@ async function runPromptStreamed(input) {
3119
3353
  delta: assistantPatch.delta
3120
3354
  });
3121
3355
  } else {
3122
- const structured = structuredStreamMessage(kind, event, text);
3123
- const statusMessage = appendMessage(input.messagesByThreadId, activeThreadId, {
3356
+ if (!text && !isRenderableStructuredStreamEvent(event)) continue;
3357
+ const structured = structuredStreamMessage(kind, event, text ?? "Codex activity");
3358
+ const itemId = firstString(eventItem(event), ["id"]);
3359
+ const existingMessage = itemId ? input.messagesByThreadId.get(activeThreadId)?.find((message) => message.id === itemId) : void 0;
3360
+ const message = {
3124
3361
  role: kind,
3125
3362
  kind: structured.kind,
3126
3363
  content: structured.content,
3127
3364
  details: structured.details,
3128
- state: "completed"
3129
- });
3365
+ state: streamItemState(event)
3366
+ };
3367
+ const statusMessage = existingMessage ? updateMessage(input.messagesByThreadId, activeThreadId, existingMessage.id, message) : itemId ? appendMessageWithId(input.messagesByThreadId, activeThreadId, itemId, message) : appendMessage(input.messagesByThreadId, activeThreadId, message);
3130
3368
  threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
3131
3369
  sendSse(input.controller, input.encoder, input.secureSession, {
3132
3370
  type: "thread.message.created",
@@ -3179,6 +3417,7 @@ async function startAppServerTurn(appServer, threadId, input) {
3179
3417
  const runtime = resolveAppServerRuntime(input.runOptions, input.workspacePath);
3180
3418
  const params = {
3181
3419
  approvalPolicy: runtime.approvalPolicy,
3420
+ clientUserMessageId: input.id,
3182
3421
  collaborationMode: appServerCollaborationMode(input.runOptions),
3183
3422
  cwd: input.workspacePath,
3184
3423
  effort: input.runOptions.reasoningEffort ?? null,
@@ -3213,9 +3452,8 @@ async function resumeAppServerThread(appServer, threadId, input, runtime) {
3213
3452
  await appServer.resumeThread({
3214
3453
  approvalPolicy: runtime.approvalPolicy,
3215
3454
  cwd: input.workspacePath,
3216
- excludeTurns: false,
3455
+ excludeTurns: true,
3217
3456
  model: input.runOptions.model ?? null,
3218
- persistExtendedHistory: true,
3219
3457
  sandbox: runtime.sandbox,
3220
3458
  serviceTier: input.runOptions.serviceTier ?? null,
3221
3459
  threadId
@@ -3262,10 +3500,12 @@ async function streamRunningAppServerThread(input) {
3262
3500
  let observedInputRequest = false;
3263
3501
  let observedTurnActivity = false;
3264
3502
  let producedTurnOutput = false;
3503
+ const asyncAgentMessageIds = /* @__PURE__ */ new Set();
3265
3504
  let threadSummary = input.threads.get(input.threadId);
3266
3505
  let cleanupNotificationHandler = () => void 0;
3267
3506
  let cleanupRequestHandler = () => void 0;
3268
3507
  let handlersCleaned = false;
3508
+ let streamFinished = false;
3269
3509
  const cleanupHandlers = () => {
3270
3510
  if (handlersCleaned) return;
3271
3511
  handlersCleaned = true;
@@ -3286,6 +3526,7 @@ async function streamRunningAppServerThread(input) {
3286
3526
  if (approval.kind === "structuredUserInput") {
3287
3527
  const pending = {
3288
3528
  appServer: input.appServer,
3529
+ isBlocking: approval.isBlocking,
3289
3530
  kind: approval.kind,
3290
3531
  method: request.method,
3291
3532
  questions: approval.questions,
@@ -3330,6 +3571,7 @@ async function streamRunningAppServerThread(input) {
3330
3571
  const finish = (error) => {
3331
3572
  if (settled) return;
3332
3573
  settled = true;
3574
+ streamFinished = true;
3333
3575
  cleanupHandlers();
3334
3576
  if (error === void 0) resolve();
3335
3577
  else reject(error);
@@ -3340,6 +3582,20 @@ async function streamRunningAppServerThread(input) {
3340
3582
  if (notificationThreadId && notificationThreadId !== input.threadId) return;
3341
3583
  try {
3342
3584
  switch (notification.method) {
3585
+ case "serverRequest/resolved": {
3586
+ const requestId = appServerRequestIdFromParams(params);
3587
+ if (requestId === void 0) return;
3588
+ const approvalId = appServerApprovalId(input.threadId, requestId);
3589
+ const pending = input.pendingApprovals.get(approvalId);
3590
+ if (!pending || pending.threadId !== input.threadId) return;
3591
+ input.pendingApprovals.delete(approvalId);
3592
+ if (pending.kind === "structuredUserInput") sendSse(input.controller, input.encoder, input.secureSession, {
3593
+ type: "thread.input_request.resolved",
3594
+ requestId: approvalId,
3595
+ threadId: input.threadId
3596
+ });
3597
+ return;
3598
+ }
3343
3599
  case "thread/status/changed": {
3344
3600
  const state = mapAppServerThreadState(params?.status);
3345
3601
  if (state !== "running" && observedTurnActivity) return;
@@ -3375,11 +3631,13 @@ async function streamRunningAppServerThread(input) {
3375
3631
  if (!item || typeof item !== "object") return;
3376
3632
  const message = upsertAppServerItemMessage(input.messagesByThreadId, input.threadId, firstString(params, ["turnId"]) ?? activeTurnId, item);
3377
3633
  if (!message) return;
3378
- if (message.role !== "user") producedTurnOutput = true;
3379
- if (message.role === "assistant") assistantMessageId = message.id;
3634
+ const isAsyncAgentMessage = isAsyncAppServerAgentMessage(item);
3635
+ if (isAsyncAgentMessage) asyncAgentMessageIds.add(message.id);
3636
+ if (message.role !== "user" && !isAsyncAgentMessage) producedTurnOutput = true;
3637
+ if (message.role === "assistant" && !isAsyncAgentMessage) assistantMessageId = message.id;
3380
3638
  threadSummary = updateThread(input.threads, input.messagesByThreadId, input.threadId, {
3381
3639
  state: "running",
3382
- lastResult: message.role === "assistant" ? message.content : void 0
3640
+ ...message.role === "assistant" && !isAsyncAgentMessage ? { lastResult: message.content } : {}
3383
3641
  });
3384
3642
  sendSse(input.controller, input.encoder, input.secureSession, {
3385
3643
  type: notification.method === "item/completed" && message.role === "assistant" ? "thread.message.completed" : "thread.message.created",
@@ -3399,13 +3657,16 @@ async function streamRunningAppServerThread(input) {
3399
3657
  state: "streaming",
3400
3658
  turnId: firstString(params, ["turnId"]) ?? activeTurnId
3401
3659
  });
3402
- assistantMessageId = itemId;
3403
- producedTurnOutput = true;
3660
+ const isAsyncAgentMessage = asyncAgentMessageIds.has(itemId);
3661
+ if (!isAsyncAgentMessage) {
3662
+ assistantMessageId = itemId;
3663
+ producedTurnOutput = true;
3664
+ }
3404
3665
  const patch = appendMessageDelta(input.messagesByThreadId, input.threadId, itemId, delta);
3405
3666
  const message = patch.message;
3406
3667
  updateThread(input.threads, input.messagesByThreadId, input.threadId, {
3407
3668
  state: "running",
3408
- lastResult: message.content
3669
+ ...isAsyncAgentMessage ? {} : { lastResult: message.content }
3409
3670
  });
3410
3671
  if (patch.delta) sendSse(input.controller, input.encoder, input.secureSession, {
3411
3672
  type: "thread.message.delta",
@@ -3441,6 +3702,27 @@ async function streamRunningAppServerThread(input) {
3441
3702
  case "turn/failed": {
3442
3703
  observedTurnActivity = true;
3443
3704
  const state = terminalTurnState(notification.method, params);
3705
+ const terminalTurn = appServerTurnFromParams(params);
3706
+ if (terminalTurn) {
3707
+ activeTurnId = terminalTurn.id;
3708
+ for (const item of terminalTurn.items) {
3709
+ const message = upsertAppServerItemMessage(input.messagesByThreadId, input.threadId, terminalTurn.id, item);
3710
+ if (!message) continue;
3711
+ const isAsyncAgentMessage = isAsyncAppServerAgentMessage(item);
3712
+ if (isAsyncAgentMessage) asyncAgentMessageIds.add(message.id);
3713
+ if (message.role !== "user" && !isAsyncAgentMessage) producedTurnOutput = true;
3714
+ if (message.role === "assistant" && !isAsyncAgentMessage) assistantMessageId = message.id;
3715
+ threadSummary = updateThread(input.threads, input.messagesByThreadId, input.threadId, {
3716
+ state: "running",
3717
+ ...message.role === "assistant" && !isAsyncAgentMessage ? { lastResult: message.content } : {}
3718
+ });
3719
+ sendSse(input.controller, input.encoder, input.secureSession, {
3720
+ type: message.role === "assistant" ? "thread.message.completed" : "thread.message.created",
3721
+ thread: threadSummary,
3722
+ message
3723
+ });
3724
+ }
3725
+ }
3444
3726
  relayDebugLog("app_server.turn.terminal", {
3445
3727
  method: notification.method,
3446
3728
  state,
@@ -3460,7 +3742,8 @@ async function streamRunningAppServerThread(input) {
3460
3742
  finish();
3461
3743
  return;
3462
3744
  }
3463
- if (assistantMessageId) {
3745
+ const assistantMessage = assistantMessageId ? input.messagesByThreadId.get(input.threadId)?.find((message) => message.id === assistantMessageId) : void 0;
3746
+ if (assistantMessageId && assistantMessage?.state !== "completed") {
3464
3747
  const completedMessage = updateMessage(input.messagesByThreadId, input.threadId, assistantMessageId, { state: "completed" });
3465
3748
  sendSse(input.controller, input.encoder, input.secureSession, {
3466
3749
  type: "thread.message.completed",
@@ -3518,8 +3801,15 @@ async function streamRunningAppServerThread(input) {
3518
3801
  removeAbortListener = () => input.signal.removeEventListener("abort", onAbort);
3519
3802
  });
3520
3803
  try {
3521
- const appServerThread = await Promise.race([input.appServer.readThread(input.threadId, { includeTurns: false }), aborted.then(() => void 0)]);
3804
+ const appServerThread = await Promise.race([typeof input.appServer.resumeThread === "function" ? input.appServer.resumeThread({
3805
+ excludeTurns: true,
3806
+ threadId: input.threadId
3807
+ }) : input.appServer.readThread(input.threadId, { includeTurns: false }), aborted.then(() => void 0)]);
3522
3808
  if (!appServerThread || input.signal.aborted) return;
3809
+ if (streamFinished) {
3810
+ await completed;
3811
+ return;
3812
+ }
3523
3813
  threadSummary = rememberAppServerThread(input.threads, appServerThread);
3524
3814
  sendSse(input.controller, input.encoder, input.secureSession, {
3525
3815
  type: "thread.state.changed",
@@ -3553,6 +3843,7 @@ async function runAppServerPromptStreamed(input) {
3553
3843
  let handedOffToQueuedTurn = false;
3554
3844
  let observedInputRequest = false;
3555
3845
  let producedTurnOutput = false;
3846
+ const asyncAgentMessageIds = /* @__PURE__ */ new Set();
3556
3847
  let userMessage = appendMessage(input.messagesByThreadId, activeThreadId, {
3557
3848
  role: "user",
3558
3849
  content: displayPrompt,
@@ -3589,6 +3880,7 @@ async function runAppServerPromptStreamed(input) {
3589
3880
  if (approval.kind === "structuredUserInput") {
3590
3881
  const pending = {
3591
3882
  appServer: input.appServer,
3883
+ isBlocking: approval.isBlocking,
3592
3884
  kind: approval.kind,
3593
3885
  method: request.method,
3594
3886
  questions: approval.questions,
@@ -3668,7 +3960,8 @@ async function runAppServerPromptStreamed(input) {
3668
3960
  resolveCompleted();
3669
3961
  return;
3670
3962
  }
3671
- if (assistantMessageId) {
3963
+ const assistantMessage = assistantMessageId ? input.messagesByThreadId.get(activeThreadId)?.find((message) => message.id === assistantMessageId) : void 0;
3964
+ if (assistantMessageId && assistantMessage?.state !== "completed") {
3672
3965
  const completedMessage = updateMessage(input.messagesByThreadId, activeThreadId, assistantMessageId, { state: "completed" });
3673
3966
  sendSse(input.controller, input.encoder, input.secureSession, {
3674
3967
  type: "thread.message.completed",
@@ -3723,8 +4016,7 @@ async function runAppServerPromptStreamed(input) {
3723
4016
  cleanupNotificationHandler();
3724
4017
  resolveCompleted();
3725
4018
  }
3726
- async function processReturnedTurn(turn) {
3727
- if (finalizedTurnIds.has(turn.id)) return;
4019
+ function processTurnItems(turn) {
3728
4020
  activeTurnId = turn.id;
3729
4021
  input.activeAppServerTurnIdsByThreadId.set(activeThreadId, turn.id);
3730
4022
  const turnIsRunning = isAppServerTurnRunning(turn);
@@ -3750,11 +4042,13 @@ async function runAppServerPromptStreamed(input) {
3750
4042
  }
3751
4043
  const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, turn.id, item);
3752
4044
  if (!message) continue;
3753
- if (message.role !== "user") producedTurnOutput = true;
3754
- if (message.role === "assistant" && turnIsRunning) assistantMessageId = message.id;
4045
+ const isAsyncAgentMessage = isAsyncAppServerAgentMessage(item);
4046
+ if (isAsyncAgentMessage) asyncAgentMessageIds.add(message.id);
4047
+ if (message.role !== "user" && !isAsyncAgentMessage) producedTurnOutput = true;
4048
+ if (message.role === "assistant" && !isAsyncAgentMessage) assistantMessageId = message.id;
3755
4049
  threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
3756
4050
  state: "running",
3757
- lastResult: message.role === "assistant" ? message.content : void 0
4051
+ ...message.role === "assistant" && !isAsyncAgentMessage ? { lastResult: message.content } : {}
3758
4052
  });
3759
4053
  sendSse(input.controller, input.encoder, input.secureSession, {
3760
4054
  type: message.role === "assistant" ? "thread.message.completed" : "thread.message.created",
@@ -3762,7 +4056,11 @@ async function runAppServerPromptStreamed(input) {
3762
4056
  message
3763
4057
  });
3764
4058
  }
3765
- if (turnIsRunning) return;
4059
+ return turnIsRunning;
4060
+ }
4061
+ async function processReturnedTurn(turn) {
4062
+ if (finalizedTurnIds.has(turn.id)) return;
4063
+ if (processTurnItems(turn)) return;
3766
4064
  const params = { turn };
3767
4065
  const state = terminalTurnState("turn/completed", params);
3768
4066
  await finishTerminalTurn({
@@ -3788,6 +4086,20 @@ async function runAppServerPromptStreamed(input) {
3788
4086
  if (threadId && threadId !== activeThreadId) return;
3789
4087
  try {
3790
4088
  switch (notification.method) {
4089
+ case "serverRequest/resolved": {
4090
+ const requestId = appServerRequestIdFromParams(params);
4091
+ if (requestId === void 0) return;
4092
+ const approvalId = appServerApprovalId(activeThreadId, requestId);
4093
+ const pending = input.pendingApprovals.get(approvalId);
4094
+ if (!pending || pending.threadId !== activeThreadId) return;
4095
+ input.pendingApprovals.delete(approvalId);
4096
+ if (pending.kind === "structuredUserInput") sendSse(input.controller, input.encoder, input.secureSession, {
4097
+ type: "thread.input_request.resolved",
4098
+ requestId: approvalId,
4099
+ threadId: activeThreadId
4100
+ });
4101
+ return;
4102
+ }
3791
4103
  case "thread/status/changed": {
3792
4104
  const status = params?.status;
3793
4105
  const state = mapAppServerThreadState(status);
@@ -3840,11 +4152,13 @@ async function runAppServerPromptStreamed(input) {
3840
4152
  const turnId = firstString(params, ["turnId"]) ?? activeTurnId;
3841
4153
  const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, turnId, item);
3842
4154
  if (!message) return;
3843
- if (message.role !== "user") producedTurnOutput = true;
3844
- if (message.role === "assistant") assistantMessageId = message.id;
4155
+ const isAsyncAgentMessage = isAsyncAppServerAgentMessage(item);
4156
+ if (isAsyncAgentMessage) asyncAgentMessageIds.add(message.id);
4157
+ if (message.role !== "user" && !isAsyncAgentMessage) producedTurnOutput = true;
4158
+ if (message.role === "assistant" && !isAsyncAgentMessage) assistantMessageId = message.id;
3845
4159
  threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
3846
4160
  state: "running",
3847
- lastResult: message.role === "assistant" ? message.content : void 0
4161
+ ...message.role === "assistant" && !isAsyncAgentMessage ? { lastResult: message.content } : {}
3848
4162
  });
3849
4163
  sendSse(input.controller, input.encoder, input.secureSession, {
3850
4164
  type: notification.method === "item/completed" && message.role === "assistant" ? "thread.message.completed" : "thread.message.created",
@@ -3863,13 +4177,14 @@ async function runAppServerPromptStreamed(input) {
3863
4177
  state: "streaming",
3864
4178
  turnId: firstString(params, ["turnId"]) ?? activeTurnId
3865
4179
  });
3866
- assistantMessageId = itemId;
4180
+ const isAsyncAgentMessage = asyncAgentMessageIds.has(itemId);
4181
+ if (!isAsyncAgentMessage) assistantMessageId = itemId;
3867
4182
  const patch = appendMessageDelta(input.messagesByThreadId, activeThreadId, itemId, delta);
3868
4183
  const message = patch.message;
3869
- producedTurnOutput = true;
4184
+ if (!isAsyncAgentMessage) producedTurnOutput = true;
3870
4185
  updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
3871
4186
  state: "running",
3872
- lastResult: message.content
4187
+ ...isAsyncAgentMessage ? {} : { lastResult: message.content }
3873
4188
  });
3874
4189
  if (patch.delta) sendSse(input.controller, input.encoder, input.secureSession, {
3875
4190
  type: "thread.message.delta",
@@ -3903,14 +4218,19 @@ async function runAppServerPromptStreamed(input) {
3903
4218
  case "turn/completed":
3904
4219
  case "turn/failed": {
3905
4220
  const state = terminalTurnState(notification.method, params);
4221
+ const terminalTurn = appServerTurnFromParams(params);
3906
4222
  const terminalTurnId = firstString(params, ["turnId"]) ?? turnIdFromParams(params) ?? activeTurnId;
3907
4223
  input.runThreadOperation(activeThreadId, async () => {
3908
- await input.runAppServerMutation(activeThreadId, () => finishTerminalTurn({
3909
- lastError: state === "failed" ? turnErrorMessage(params) : void 0,
3910
- method: notification.method,
3911
- state,
3912
- turnId: terminalTurnId
3913
- }));
4224
+ await input.runAppServerMutation(activeThreadId, async () => {
4225
+ if (terminalTurnId && finalizedTurnIds.has(terminalTurnId) || terminalTurnId && activeTurnId && terminalTurnId !== activeTurnId) return;
4226
+ if (terminalTurn && terminalTurn.id === terminalTurnId) processTurnItems(terminalTurn);
4227
+ await finishTerminalTurn({
4228
+ lastError: state === "failed" ? turnErrorMessage(params) : void 0,
4229
+ method: notification.method,
4230
+ state,
4231
+ turnId: terminalTurnId
4232
+ });
4233
+ });
3914
4234
  }).catch((error) => {
3915
4235
  cleanupNotificationHandler();
3916
4236
  rejectCompleted(error);
@@ -3926,36 +4246,34 @@ async function runAppServerPromptStreamed(input) {
3926
4246
  });
3927
4247
  try {
3928
4248
  debugStream(`wait idle gate first=${(input.messagesByThreadId.get(activeThreadId) ?? []).every((message) => message.id === userMessage.id) ? "1" : "0"} active=${activeTurnId ?? "none"}`, activeThreadId);
3929
- if (activeTurnId) {
3930
- debugStream("wait idle begin", activeThreadId);
3931
- await waitForAppServerThreadIdle({
3932
- appServer: input.appServer,
3933
- onWaiting() {
3934
- if (waitingForActiveTurnMessageId) return;
3935
- const message = appendMessage(input.messagesByThreadId, activeThreadId, {
3936
- role: "status",
3937
- content: "Waiting for the current Codex turn to finish.",
3938
- state: "streaming"
3939
- });
3940
- waitingForActiveTurnMessageId = message.id;
3941
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
3942
- sendSse(input.controller, input.encoder, input.secureSession, {
3943
- type: "thread.message.created",
3944
- thread: threadSummary,
3945
- message
3946
- });
3947
- },
3948
- onWaitingHeartbeat() {
3949
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
3950
- sendSse(input.controller, input.encoder, input.secureSession, {
3951
- type: "thread.state.changed",
3952
- thread: threadSummary
3953
- });
3954
- },
3955
- threadId: activeThreadId
3956
- });
3957
- debugStream("wait idle complete", activeThreadId);
3958
- }
4249
+ debugStream("wait idle begin", activeThreadId);
4250
+ await waitForAppServerThreadIdle({
4251
+ appServer: input.appServer,
4252
+ onWaiting() {
4253
+ if (waitingForActiveTurnMessageId) return;
4254
+ const message = appendMessage(input.messagesByThreadId, activeThreadId, {
4255
+ role: "status",
4256
+ content: "Waiting for the current Codex turn to finish.",
4257
+ state: "streaming"
4258
+ });
4259
+ waitingForActiveTurnMessageId = message.id;
4260
+ threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
4261
+ sendSse(input.controller, input.encoder, input.secureSession, {
4262
+ type: "thread.message.created",
4263
+ thread: threadSummary,
4264
+ message
4265
+ });
4266
+ },
4267
+ onWaitingHeartbeat() {
4268
+ threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
4269
+ sendSse(input.controller, input.encoder, input.secureSession, {
4270
+ type: "thread.state.changed",
4271
+ thread: threadSummary
4272
+ });
4273
+ },
4274
+ threadId: activeThreadId
4275
+ });
4276
+ debugStream("wait idle complete", activeThreadId);
3959
4277
  if (waitingForActiveTurnMessageId) {
3960
4278
  const message = updateMessage(input.messagesByThreadId, activeThreadId, waitingForActiveTurnMessageId, {
3961
4279
  content: "Current Codex turn finished. Starting your reply.",
@@ -3973,7 +4291,7 @@ async function runAppServerPromptStreamed(input) {
3973
4291
  try {
3974
4292
  turn = await startAndProcessAppServerTurn(activeThreadId, {
3975
4293
  attachments: input.attachments,
3976
- id: randomUUID(),
4294
+ id: userMessage.id,
3977
4295
  prompt,
3978
4296
  runOptions: input.runOptions,
3979
4297
  skills: input.skills,
@@ -4007,7 +4325,7 @@ async function runAppServerPromptStreamed(input) {
4007
4325
  });
4008
4326
  turn = await startAndProcessAppServerTurn(activeThreadId, {
4009
4327
  attachments: input.attachments,
4010
- id: randomUUID(),
4328
+ id: userMessage.id,
4011
4329
  prompt,
4012
4330
  runOptions: input.runOptions,
4013
4331
  skills: input.skills,
@@ -4120,11 +4438,10 @@ async function recoverMissingAppServerThread(input) {
4120
4438
  const thread = await input.appServer.startThread({
4121
4439
  approvalPolicy: runtime.approvalPolicy,
4122
4440
  cwd: input.workspacePath,
4123
- experimentalRawEvents: false,
4124
4441
  model: input.runOptions.model ?? null,
4125
- persistExtendedHistory: true,
4126
4442
  sandbox: runtime.sandbox,
4127
- serviceTier: input.runOptions.serviceTier ?? null
4443
+ serviceTier: input.runOptions.serviceTier ?? null,
4444
+ threadSource: "codex-relay"
4128
4445
  });
4129
4446
  const recoveredThread = mapAppServerThread({
4130
4447
  ...thread,
@@ -4277,7 +4594,11 @@ function appendMessageWithId(messagesByThreadId, threadId, id, input) {
4277
4594
  function upsertAppServerItemMessage(messagesByThreadId, threadId, turnId, item) {
4278
4595
  const message = mapAppServerItem(threadId, appServerTurnShell(turnId), item);
4279
4596
  if (!message) return;
4280
- if (messagesByThreadId.get(threadId)?.some((candidate) => candidate.id === item.id)) return updateMessage(messagesByThreadId, threadId, item.id, message);
4597
+ const existing = messagesByThreadId.get(threadId)?.find((candidate) => candidate.id === item.id);
4598
+ if (existing) return updateMessage(messagesByThreadId, threadId, item.id, {
4599
+ ...message,
4600
+ createdAt: existing.createdAt
4601
+ });
4281
4602
  return appendMessageWithId(messagesByThreadId, threadId, item.id, {
4282
4603
  role: message.role,
4283
4604
  kind: message.kind,
@@ -4295,6 +4616,7 @@ function replaceDuplicateInitialUserMessage(messagesByThreadId, threadId, turnId
4295
4616
  }
4296
4617
  function isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, localMessageId, prompt) {
4297
4618
  if (item.type !== "userMessage" || !("content" in item) || !Array.isArray(item.content)) return false;
4619
+ if (item.clientId) return item.clientId === localMessageId;
4298
4620
  const localMessage = messagesByThreadId.get(threadId)?.find((message) => message.id === localMessageId);
4299
4621
  const skills = appServerUserMessageSkills(item);
4300
4622
  const normalizeContent = (content) => stripPromptSkillMentions(normalizeImageMessageContent(content), skills);
@@ -4966,6 +5288,7 @@ function hasExplicitRunOptions(options) {
4966
5288
  function mapAppServerThread(thread, fallbackMessageCount) {
4967
5289
  const createdAt = fromUnixSeconds(thread.createdAt);
4968
5290
  const updatedAt = fromUnixSeconds(thread.updatedAt);
5291
+ const lastActivityAt = fromUnixSeconds(thread.recencyAt ?? thread.updatedAt);
4969
5292
  const messageCount = Math.max(thread.turns ? countThreadMessages(thread) : 0, fallbackMessageCount ?? 0);
4970
5293
  const mappedState = mapAppServerThreadState(thread.status, thread.turns);
4971
5294
  return ThreadSummarySchema.parse({
@@ -4979,7 +5302,7 @@ function mapAppServerThread(thread, fallbackMessageCount) {
4979
5302
  source: thread.source,
4980
5303
  messageCount,
4981
5304
  lastMessagePreview: thread.preview ? preview(thread.preview) : void 0,
4982
- lastActivityAt: updatedAt
5305
+ lastActivityAt
4983
5306
  });
4984
5307
  }
4985
5308
  function rememberAppServerThread(threads, thread, options = {}) {
@@ -5032,6 +5355,12 @@ function mapAppServerMessages(thread) {
5032
5355
  }
5033
5356
  return messages;
5034
5357
  }
5358
+ function appServerThreadHasCompleteHistory(thread) {
5359
+ return (thread.turns ?? []).every((turn) => turn.itemsView === void 0 || turn.itemsView === "full");
5360
+ }
5361
+ function isAsyncAppServerAgentMessage(item) {
5362
+ return item.type === "agentMessage" && "delivery" in item && item.delivery === "async";
5363
+ }
5035
5364
  function mergeAppServerMessagesWithLocalStatus(appMessages, localMessages) {
5036
5365
  const appMessageIds = new Set(appMessages.map((message) => message.id));
5037
5366
  const localStatusMessages = localMessages.filter((message) => message.role === "status" && !appMessageIds.has(message.id));
@@ -5162,13 +5491,18 @@ function readSessionIndexThreadTitle(threadId) {
5162
5491
  } catch {}
5163
5492
  }
5164
5493
  }
5165
- function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePath) {
5166
- const rolloutPath = findRolloutFileForThread(threadId);
5494
+ function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePath, selectedRolloutPath) {
5495
+ const rolloutPath = selectedRolloutPath && selectedRolloutPath.endsWith(".jsonl") && existsSync(selectedRolloutPath) ? selectedRolloutPath : findRolloutFileForThread(threadId);
5167
5496
  if (!rolloutPath) return {
5168
5497
  messageCountLowerBound: 0,
5169
5498
  messages: [],
5170
5499
  rolloutPath
5171
5500
  };
5501
+ if (isPaginatedRolloutFile(rolloutPath)) return {
5502
+ messageCountLowerBound: 0,
5503
+ messages: [],
5504
+ rolloutPath
5505
+ };
5172
5506
  const collected = [];
5173
5507
  const applyPatchInputs = /* @__PURE__ */ new Map();
5174
5508
  const handledApplyPatchCallIds = /* @__PURE__ */ new Set();
@@ -5208,6 +5542,25 @@ function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePat
5208
5542
  rolloutPath
5209
5543
  };
5210
5544
  }
5545
+ function isPaginatedRolloutFile(rolloutPath) {
5546
+ let descriptor;
5547
+ try {
5548
+ descriptor = openSync(rolloutPath, "r");
5549
+ const buffer = Buffer.allocUnsafe(64 * 1024);
5550
+ const bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
5551
+ const prefix = buffer.toString("utf8", 0, bytesRead);
5552
+ for (const line of prefix.split("\n")) {
5553
+ if (!line.trim()) continue;
5554
+ const record = JSON.parse(line);
5555
+ if (record.type === "session_meta") return record.payload?.history_mode === "paginated";
5556
+ }
5557
+ } catch {
5558
+ return false;
5559
+ } finally {
5560
+ if (descriptor !== void 0) closeSync(descriptor);
5561
+ }
5562
+ return false;
5563
+ }
5211
5564
  function findRolloutFileForThread(threadId) {
5212
5565
  const sessionsRoot = join(process.env.CODEX_HOME || join(homedir(), ".codex"), "sessions");
5213
5566
  if (!existsSync(sessionsRoot)) return;
@@ -5572,12 +5925,16 @@ function mapAppServerItem(threadId, turn, item) {
5572
5925
  const agentItem = item;
5573
5926
  const planContent = proposedPlanContent(agentItem.text);
5574
5927
  const messageParts = appServerAgentMessageParts(agentItem);
5928
+ const details = planContent ? { raw: agentItem.text } : agentItem.delivery || messageParts.details ? {
5929
+ ...messageParts.details,
5930
+ delivery: agentItem.delivery ?? void 0
5931
+ } : void 0;
5575
5932
  return ChatMessageSchema.parse({
5576
5933
  ...base,
5577
5934
  role: "assistant",
5578
5935
  kind: planContent ? "plan" : void 0,
5579
5936
  content: planContent ?? messageParts.content,
5580
- details: planContent ? { raw: agentItem.text } : messageParts.details
5937
+ details
5581
5938
  });
5582
5939
  }
5583
5940
  case "reasoning": {
@@ -5977,6 +6334,21 @@ function structuredStreamMessage(role, event, fallbackContent) {
5977
6334
  type
5978
6335
  }
5979
6336
  };
6337
+ case "todo_list": {
6338
+ const items = Array.isArray(item?.items) ? item.items : [];
6339
+ return {
6340
+ kind: "plan",
6341
+ content: items.flatMap((entry) => {
6342
+ if (!entry || typeof entry !== "object") return [];
6343
+ const todo = entry;
6344
+ return typeof todo.text === "string" ? [`[${todo.completed === true ? "x" : " "}] ${todo.text}`] : [];
6345
+ }).join("\n") || fallbackContent,
6346
+ details: {
6347
+ items,
6348
+ type
6349
+ }
6350
+ };
6351
+ }
5980
6352
  default: return {
5981
6353
  kind: kindFromProtocolType(type) ?? (role === "tool" ? "toolActivity" : "unknown"),
5982
6354
  content: fallbackContent,
@@ -5984,6 +6356,17 @@ function structuredStreamMessage(role, event, fallbackContent) {
5984
6356
  };
5985
6357
  }
5986
6358
  }
6359
+ function isCompletedAgentMessageEvent(event) {
6360
+ if (!event || typeof event !== "object") return false;
6361
+ return event.type === "item.completed" && eventItem(event)?.type === "agent_message";
6362
+ }
6363
+ function isRenderableStructuredStreamEvent(event) {
6364
+ const type = eventItem(event)?.type;
6365
+ return type === "file_change" || type === "mcp_tool_call" || type === "todo_list" || type === "web_search";
6366
+ }
6367
+ function streamItemState(event) {
6368
+ return event?.type === "item.completed" ? "completed" : "streaming";
6369
+ }
5987
6370
  function isApprovalServerRequest(method) {
5988
6371
  return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval" || method === "item/permissions/requestApproval" || method === "item/tool/requestUserInput" || method === "mcpServer/elicitation/request" || method === "execCommandApproval" || method === "applyPatchApproval";
5989
6372
  }
@@ -6046,7 +6429,7 @@ function approvalMessageFromRequest(request) {
6046
6429
  const threadId = firstString(params, ["threadId", "conversationId"]);
6047
6430
  const turnId = firstString(params, ["turnId"]) ?? void 0;
6048
6431
  if (!threadId) return;
6049
- const approvalId = `approval-${request.id}`;
6432
+ const approvalId = appServerApprovalId(threadId, request.id);
6050
6433
  switch (request.method) {
6051
6434
  case "item/commandExecution/requestApproval": {
6052
6435
  const command = firstString(params, ["command"]) ?? "Command execution";
@@ -6134,6 +6517,7 @@ function approvalMessageFromRequest(request) {
6134
6517
  questions
6135
6518
  },
6136
6519
  kind: "structuredUserInput",
6520
+ isBlocking: typeof params?.isBlocking === "boolean" ? params.isBlocking : true,
6137
6521
  questions,
6138
6522
  threadId,
6139
6523
  turnId
@@ -6188,6 +6572,7 @@ function pendingInputOptions(value) {
6188
6572
  function pendingInputRequestFromApproval(approval, threadId) {
6189
6573
  return {
6190
6574
  id: approval.approvalId,
6575
+ isBlocking: approval.isBlocking ?? true,
6191
6576
  questions: approval.questions ?? [],
6192
6577
  threadId,
6193
6578
  turnId: approval.turnId
@@ -6198,6 +6583,7 @@ function pendingInputRequestsForThread(pendingApprovals, threadId) {
6198
6583
  if (pending.kind !== "structuredUserInput" || pending.threadId !== threadId) return [];
6199
6584
  return [{
6200
6585
  id: approvalId,
6586
+ isBlocking: pending.isBlocking ?? true,
6201
6587
  questions: pending.questions ?? [],
6202
6588
  threadId,
6203
6589
  turnId: pending.turnId
@@ -6293,6 +6679,20 @@ function turnIdFromParams(params) {
6293
6679
  const turn = params?.turn;
6294
6680
  return turn && typeof turn === "object" ? firstString(turn, ["id"]) : void 0;
6295
6681
  }
6682
+ function appServerRequestIdFromParams(params) {
6683
+ const requestId = params?.requestId;
6684
+ return typeof requestId === "number" || typeof requestId === "string" ? requestId : void 0;
6685
+ }
6686
+ function appServerApprovalId(threadId, requestId) {
6687
+ return `approval-${createHash("sha256").update(threadId).update("\0").update(String(requestId)).digest("hex").slice(0, 24)}`;
6688
+ }
6689
+ function appServerTurnFromParams(params) {
6690
+ const turn = params?.turn;
6691
+ if (!turn || typeof turn !== "object") return;
6692
+ const record = turn;
6693
+ if (typeof record.id !== "string" || !Array.isArray(record.items)) return;
6694
+ return turn;
6695
+ }
6296
6696
  function turnStatus(params) {
6297
6697
  if (typeof params?.status === "string") return params.status;
6298
6698
  const turn = params?.turn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-relay",
3
- "version": "1.4.9",
3
+ "version": "1.4.11",
4
4
  "description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -41,6 +41,7 @@
41
41
  "@noble/ciphers": "2.2.0",
42
42
  "@noble/curves": "2.2.0",
43
43
  "@noble/hashes": "2.2.0",
44
+ "@openai/codex": "0.149.1",
44
45
  "@openai/codex-sdk": "0.149.1",
45
46
  "base64-js": "1.5.1",
46
47
  "commander": "^14.0.3",
package/src/api-schema.ts CHANGED
@@ -284,6 +284,7 @@ export const PendingInputRequestQuestionSchema = z.object({
284
284
 
285
285
  export const PendingInputRequestSchema = z.object({
286
286
  id: z.string().min(1),
287
+ isBlocking: z.boolean().default(true),
287
288
  questions: z.array(PendingInputRequestQuestionSchema),
288
289
  threadId: z.string().min(1),
289
290
  turnId: z.string().optional(),