codex-relay 1.4.8 → 1.4.10

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,30 @@ 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
+ cursor,
196
+ limit,
197
+ sortDirection: "desc",
198
+ sortKey: "recency_at",
199
+ sourceKinds: [
200
+ "cli",
201
+ "vscode",
202
+ "exec",
203
+ "appServer"
204
+ ]
205
+ });
206
+ threads.push(...response.data);
207
+ const nextCursor = response.nextCursor ?? void 0;
208
+ if (!nextCursor || seenCursors.has(nextCursor)) break;
209
+ seenCursors.add(nextCursor);
210
+ cursor = nextCursor;
211
+ } while (cursor);
212
+ return threads;
181
213
  }
182
214
  async readThread(threadId, options = {}) {
183
215
  return (await this.request("thread/read", {
@@ -195,19 +227,39 @@ var CodexAppServerClient = class {
195
227
  return this.request("account/rateLimits/read", null);
196
228
  }
197
229
  async startThread(params) {
198
- return (await this.request("thread/start", params)).thread;
230
+ const response = await this.request("thread/start", params);
231
+ this.subscribedThreadIds.add(response.thread.id);
232
+ return response.thread;
199
233
  }
200
234
  async resumeThread(params) {
201
- return (await this.request("thread/resume", params)).thread;
235
+ const response = await this.request("thread/resume", params.excludeTurns ? {
236
+ ...params,
237
+ initialTurnsPage: {
238
+ itemsView: "summary",
239
+ limit: 1,
240
+ sortDirection: "desc"
241
+ }
242
+ } : params);
243
+ this.subscribedThreadIds.add(response.thread.id);
244
+ this.rememberActiveTurn({
245
+ ...response.thread,
246
+ turns: response.initialTurnsPage?.data ?? response.thread.turns
247
+ });
248
+ return response.thread;
202
249
  }
203
250
  async startTurn(params) {
204
- return (await this.request("turn/start", params)).turn;
251
+ const response = await this.request("turn/start", params);
252
+ this.rememberTurn(params.threadId, response.turn);
253
+ return response.turn;
205
254
  }
206
255
  async interruptTurn(params) {
207
256
  await this.request("turn/interrupt", params);
208
257
  }
209
258
  async archiveThread(params) {
210
259
  await this.request("thread/archive", params);
260
+ this.subscribedThreadIds.delete(params.threadId);
261
+ this.activeTurnIdsByThreadId.delete(params.threadId);
262
+ this.terminalTurnIdsByThreadId.delete(params.threadId);
211
263
  }
212
264
  async setThreadName(params) {
213
265
  await this.request("thread/name/set", params);
@@ -215,6 +267,9 @@ var CodexAppServerClient = class {
215
267
  async rollbackThread(params) {
216
268
  return (await this.request("thread/rollback", params)).thread;
217
269
  }
270
+ async revertThread(params) {
271
+ return (await this.request("thread/revert", params)).thread;
272
+ }
218
273
  async getThreadGoal(params) {
219
274
  return (await this.request("thread/goal/get", params)).goal;
220
275
  }
@@ -255,6 +310,9 @@ var CodexAppServerClient = class {
255
310
  this.stopStdioCodexAppServer();
256
311
  this.stopSharedCodexAppServer();
257
312
  this.initialized = void 0;
313
+ this.activeTurnIdsByThreadId.clear();
314
+ this.terminalTurnIdsByThreadId.clear();
315
+ this.subscribedThreadIds.clear();
258
316
  }
259
317
  async request(method, params) {
260
318
  await this.ensureInitialized();
@@ -322,8 +380,90 @@ var CodexAppServerClient = class {
322
380
  title: "Codex Relay Mobile Server",
323
381
  version: "1.2.0"
324
382
  },
325
- capabilities: { experimentalApi: true }
383
+ capabilities: {
384
+ experimentalApi: true,
385
+ requestAttestation: false
386
+ }
326
387
  });
388
+ await this.writeJson({ method: "initialized" });
389
+ for (const threadId of this.subscribedThreadIds) try {
390
+ const disconnectedTurnId = this.activeTurnIdsByThreadId.get(threadId);
391
+ const response = await this.requestRaw("thread/resume", {
392
+ excludeTurns: true,
393
+ initialTurnsPage: {
394
+ itemsView: "summary",
395
+ limit: 1,
396
+ sortDirection: "desc"
397
+ },
398
+ threadId
399
+ });
400
+ this.reconcileResumedThread({
401
+ ...response.thread,
402
+ turns: response.initialTurnsPage?.data ?? response.thread.turns
403
+ }, disconnectedTurnId);
404
+ } catch (error) {
405
+ relayDebugLog("app_server.thread.resubscribe_failed", {
406
+ message: error instanceof Error ? error.message : String(error),
407
+ threadId
408
+ });
409
+ }
410
+ }
411
+ reconcileResumedThread(thread, disconnectedTurnId) {
412
+ if (disconnectedTurnId) {
413
+ const disconnectedTurn = thread.turns?.find((turn) => turn.id === disconnectedTurnId);
414
+ if (disconnectedTurn && !isAppServerTurnInProgress(disconnectedTurn)) this.dispatchNotification({
415
+ method: "turn/completed",
416
+ params: {
417
+ threadId: thread.id,
418
+ turn: disconnectedTurn
419
+ }
420
+ });
421
+ else this.dispatchNotification({
422
+ method: "thread/status/changed",
423
+ params: {
424
+ status: thread.status,
425
+ threadId: thread.id
426
+ }
427
+ });
428
+ }
429
+ this.rememberActiveTurn(thread);
430
+ }
431
+ rememberActiveTurn(thread) {
432
+ const activeTurn = [...thread.turns ?? []].reverse().find((turn) => isAppServerTurnInProgress(turn));
433
+ if (activeTurn && this.terminalTurnIdsByThreadId.get(thread.id) !== activeTurn.id) {
434
+ this.activeTurnIdsByThreadId.set(thread.id, activeTurn.id);
435
+ this.terminalTurnIdsByThreadId.delete(thread.id);
436
+ } else this.activeTurnIdsByThreadId.delete(thread.id);
437
+ }
438
+ rememberTurn(threadId, turn) {
439
+ if (isAppServerTurnInProgress(turn)) {
440
+ if (this.terminalTurnIdsByThreadId.get(threadId) !== turn.id) {
441
+ this.activeTurnIdsByThreadId.set(threadId, turn.id);
442
+ this.terminalTurnIdsByThreadId.delete(threadId);
443
+ }
444
+ } else if (this.activeTurnIdsByThreadId.get(threadId) === turn.id) this.activeTurnIdsByThreadId.delete(threadId);
445
+ }
446
+ dispatchNotification(notification) {
447
+ const params = notification.params;
448
+ const record = params && typeof params === "object" ? params : {};
449
+ const threadId = typeof record.threadId === "string" ? record.threadId : void 0;
450
+ const turn = record.turn && typeof record.turn === "object" ? record.turn : void 0;
451
+ const turnId = typeof record.turnId === "string" ? record.turnId : typeof turn?.id === "string" ? turn.id : void 0;
452
+ if (notification.method === "turn/started" && threadId && turnId) {
453
+ if (this.terminalTurnIdsByThreadId.get(threadId) !== turnId) {
454
+ this.activeTurnIdsByThreadId.set(threadId, turnId);
455
+ this.terminalTurnIdsByThreadId.delete(threadId);
456
+ }
457
+ } else if ([
458
+ "turn/completed",
459
+ "turn/failed",
460
+ "turn/aborted",
461
+ "turn/cancelled"
462
+ ].includes(notification.method) && threadId) {
463
+ if (turnId) this.terminalTurnIdsByThreadId.set(threadId, turnId);
464
+ if (!turnId || this.activeTurnIdsByThreadId.get(threadId) === turnId) this.activeTurnIdsByThreadId.delete(threadId);
465
+ }
466
+ for (const handler of this.notificationHandlers) handler(notification);
327
467
  }
328
468
  startStdioCodexAppServer() {
329
469
  const spawnConfig = resolveCodexAppServerSpawn();
@@ -528,7 +668,7 @@ var CodexAppServerClient = class {
528
668
  } catch {
529
669
  return;
530
670
  }
531
- if (typeof message.method === "string" && typeof message.id === "number") {
671
+ if (typeof message.method === "string" && (typeof message.id === "number" || typeof message.id === "string")) {
532
672
  debugAppServer("server-request", message.method, message.id);
533
673
  const request = {
534
674
  id: message.id,
@@ -545,7 +685,7 @@ var CodexAppServerClient = class {
545
685
  method: message.method,
546
686
  params: message.params
547
687
  };
548
- for (const handler of this.notificationHandlers) handler(notification);
688
+ this.dispatchNotification(notification);
549
689
  return;
550
690
  }
551
691
  if (typeof message.id !== "number") return;
@@ -644,6 +784,15 @@ function connectRemoteAddress(remoteAddress) {
644
784
  function sharedCodexAppServerSocketPath() {
645
785
  return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "app-server-control", "app-server-control.sock");
646
786
  }
787
+ function isAppServerTurnInProgress(turn) {
788
+ if (turn.completedAt !== null && turn.completedAt !== void 0) return false;
789
+ const status = typeof turn.status === "string" ? turn.status : turn.status && typeof turn.status === "object" && "type" in turn.status ? String(turn.status.type) : "";
790
+ return [
791
+ "active",
792
+ "inprogress",
793
+ "running"
794
+ ].includes(status.toLowerCase().replace(/[^a-z0-9]/g, ""));
795
+ }
647
796
  function asError(error) {
648
797
  return error instanceof Error ? error : new Error(String(error));
649
798
  }
@@ -672,7 +821,8 @@ function extractStreamText(event) {
672
821
  if (!event || typeof event !== "object") return;
673
822
  const record = event;
674
823
  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;
824
+ const error = record.error && typeof record.error === "object" ? record.error : void 0;
825
+ const candidate = record.delta ?? record.text ?? record.message ?? error?.message ?? item?.text ?? item?.message ?? item?.aggregated_output ?? item?.command;
676
826
  return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
677
827
  }
678
828
  function classifyStreamEvent(event) {
@@ -1472,11 +1622,47 @@ function createApp(options = {}) {
1472
1622
  const appServerMutationTails = /* @__PURE__ */ new Map();
1473
1623
  const workspaceTerminalSessions = /* @__PURE__ */ new Map();
1474
1624
  const activeAppServerTurnIdsByThreadId = /* @__PURE__ */ new Map();
1625
+ const appServerHistoryGenerationsByThreadId = /* @__PURE__ */ new Map();
1475
1626
  const appServerHistoryLoadsByThreadId = /* @__PURE__ */ new Map();
1627
+ const appServerRolloutPathsByThreadId = /* @__PURE__ */ new Map();
1476
1628
  const steeringThreads = /* @__PURE__ */ new Set();
1477
1629
  const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
1478
1630
  const activeStreamControllers = /* @__PURE__ */ new Map();
1479
- const threadOptions = { workingDirectory: workspacePath };
1631
+ const threadOptions = {
1632
+ threadSource: "codex-relay",
1633
+ workingDirectory: workspacePath
1634
+ };
1635
+ const advanceAppServerHistoryGeneration = (threadId) => {
1636
+ appServerHistoryGenerationsByThreadId.set(threadId, (appServerHistoryGenerationsByThreadId.get(threadId) ?? 0) + 1);
1637
+ };
1638
+ const invalidateAppServerHistory = (threadId) => {
1639
+ advanceAppServerHistoryGeneration(threadId);
1640
+ appServerHistoryLoadsByThreadId.delete(threadId);
1641
+ appServerRolloutPathsByThreadId.delete(threadId);
1642
+ messagesByThreadId.delete(threadId);
1643
+ };
1644
+ if (appServer && typeof appServer.onNotification === "function") appServer.onNotification((notification) => {
1645
+ const params = recordParams(notification);
1646
+ const threadId = firstString(params, ["threadId"]);
1647
+ if (!threadId) return;
1648
+ if (notification.method === "thread/reverted") {
1649
+ invalidateAppServerHistory(threadId);
1650
+ return;
1651
+ }
1652
+ if (notification.method === "item/completed") {
1653
+ advanceAppServerHistoryGeneration(threadId);
1654
+ const item = params?.item;
1655
+ if (item && typeof item === "object" && item.type !== "userMessage") upsertAppServerItemMessage(messagesByThreadId, threadId, firstString(params, ["turnId"]), item);
1656
+ return;
1657
+ }
1658
+ if (notification.method === "turn/completed") {
1659
+ advanceAppServerHistoryGeneration(threadId);
1660
+ const turn = appServerTurnFromParams(params);
1661
+ if (turn) {
1662
+ for (const item of turn.items) if (item.type !== "userMessage") upsertAppServerItemMessage(messagesByThreadId, threadId, turn.id, item);
1663
+ }
1664
+ }
1665
+ });
1480
1666
  function runThreadOperation(threadId, operation) {
1481
1667
  const result = (threadOperationTails.get(threadId) ?? Promise.resolve()).then(operation, operation);
1482
1668
  const tail = result.then(() => void 0, () => void 0);
@@ -1503,13 +1689,18 @@ function createApp(options = {}) {
1503
1689
  const scheduleAppServerHistoryLoad = (threadId, cachedMessages) => {
1504
1690
  if (!appServer || appServerHistoryLoadsByThreadId.has(threadId)) return;
1505
1691
  const startedAt = Date.now();
1692
+ const historyGeneration = appServerHistoryGenerationsByThreadId.get(threadId) ?? 0;
1506
1693
  relayDebugLog("thread.detail.history.background_started", {
1507
1694
  cachedMessageCount: cachedMessages.length,
1508
1695
  threadId
1509
1696
  });
1510
1697
  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);
1698
+ if ((appServerHistoryGenerationsByThreadId.get(threadId) ?? 0) !== historyGeneration) return;
1699
+ const hasCompleteHistory = appServerThreadHasCompleteHistory(threadWithTurns);
1700
+ const mappedThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: hasCompleteHistory });
1701
+ const appMessages = mapAppServerMessages(threadWithTurns);
1702
+ const localMessages = messagesByThreadId.get(threadId) ?? cachedMessages;
1703
+ const messages = hasCompleteHistory ? mergeAppServerMessagesWithLocalStatus(appMessages, localMessages) : mergeThreadMessagePages(appMessages, localMessages);
1513
1704
  messagesByThreadId.set(threadId, messages);
1514
1705
  relayDebugLog("thread.detail.history.background_completed", {
1515
1706
  durationMs: Date.now() - startedAt,
@@ -1524,7 +1715,7 @@ function createApp(options = {}) {
1524
1715
  threadId
1525
1716
  });
1526
1717
  }).finally(() => {
1527
- appServerHistoryLoadsByThreadId.delete(threadId);
1718
+ if (appServerHistoryLoadsByThreadId.get(threadId) === load) appServerHistoryLoadsByThreadId.delete(threadId);
1528
1719
  });
1529
1720
  appServerHistoryLoadsByThreadId.set(threadId, load);
1530
1721
  };
@@ -2203,13 +2394,27 @@ function createApp(options = {}) {
2203
2394
  const turns = threadBeforeRewind.turns ?? [];
2204
2395
  const turnIndex = turns.findIndex((turn) => turn.id === parsed.data.turnId);
2205
2396
  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({
2397
+ let rolledBackThread;
2398
+ if (threadBeforeRewind.historyMode === "paginated" && typeof appServer.revertThread === "function") try {
2399
+ rolledBackThread = await appServer.revertThread({
2400
+ beforeTurnId: parsed.data.turnId,
2401
+ threadId
2402
+ });
2403
+ } catch (error) {
2404
+ if (!/method not found|unsupported|-32601/i.test(errorMessage(error))) throw error;
2405
+ rolledBackThread = await appServer.rollbackThread({
2406
+ threadId,
2407
+ numTurns: turns.length - turnIndex
2408
+ });
2409
+ }
2410
+ else rolledBackThread = await appServer.rollbackThread({
2207
2411
  threadId,
2208
2412
  numTurns: turns.length - turnIndex
2209
2413
  });
2210
2414
  const threadWithTurns = rolledBackThread.turns === void 0 ? await appServer.readThread(threadId, { includeTurns: true }) : rolledBackThread;
2211
2415
  const thread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: true });
2212
2416
  const messages = mapAppServerMessages(threadWithTurns);
2417
+ invalidateAppServerHistory(threadId);
2213
2418
  messagesByThreadId.set(threadId, messages);
2214
2419
  liveThreads.delete(threadId);
2215
2420
  activeAppServerTurnIdsByThreadId.delete(threadId);
@@ -2239,18 +2444,29 @@ function createApp(options = {}) {
2239
2444
  const thread = await appServer.readThread(threadId, { includeTurns: false });
2240
2445
  if (isSubagentThread(thread)) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
2241
2446
  const mappedThread = rememberAppServerThread(threads, thread);
2242
- const cachedMessages = messagesByThreadId.get(threadId) ?? [];
2447
+ let cachedMessages = messagesByThreadId.get(threadId) ?? [];
2448
+ if (thread.path) {
2449
+ const previousRolloutPath = appServerRolloutPathsByThreadId.get(threadId);
2450
+ if (previousRolloutPath && previousRolloutPath !== thread.path) {
2451
+ invalidateAppServerHistory(threadId);
2452
+ cachedMessages = [];
2453
+ }
2454
+ appServerRolloutPathsByThreadId.set(threadId, thread.path);
2455
+ }
2243
2456
  let loadedMessages = false;
2244
2457
  let messages = cachedMessages;
2245
2458
  let responseThread = preserveKnownRunningThreadState(mappedThread, wasKnownRunning);
2246
2459
  if (forceRefresh && responseThread.state !== "running") {
2247
2460
  const threadWithTurns = await appServer.readThread(threadId, { includeTurns: true });
2248
- responseThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: true });
2249
- messages = mapAppServerMessages(threadWithTurns);
2461
+ const hasCompleteHistory = appServerThreadHasCompleteHistory(threadWithTurns);
2462
+ responseThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: hasCompleteHistory });
2463
+ const appMessages = mapAppServerMessages(threadWithTurns);
2464
+ messages = hasCompleteHistory ? mergeAppServerMessagesWithLocalStatus(appMessages, cachedMessages) : mergeThreadMessagePages(appMessages, cachedMessages);
2250
2465
  messagesByThreadId.set(threadId, messages);
2251
2466
  loadedMessages = true;
2252
2467
  } else {
2253
- const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath);
2468
+ const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath, thread.path ?? void 0);
2469
+ if (rolloutHistory.rolloutPath) appServerRolloutPathsByThreadId.set(threadId, rolloutHistory.rolloutPath);
2254
2470
  if (rolloutHistory.messages.length > 0) {
2255
2471
  messages = mergeThreadMessagePages(rolloutHistory.messages, cachedMessages);
2256
2472
  responseThread = rememberRolloutThreadMessages(threads, responseThread, messages, rolloutHistory.messageCountLowerBound);
@@ -2265,8 +2481,10 @@ function createApp(options = {}) {
2265
2481
  }
2266
2482
  if (!loadedMessages && responseThread.state !== "running") {
2267
2483
  const threadWithTurns = await appServer.readThread(threadId, { includeTurns: true });
2268
- responseThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: true });
2269
- messages = mergeAppServerMessagesWithLocalStatus(mapAppServerMessages(threadWithTurns), cachedMessages);
2484
+ const hasCompleteHistory = appServerThreadHasCompleteHistory(threadWithTurns);
2485
+ responseThread = rememberAppServerThread(threads, threadWithTurns, { authoritativeMessageCount: hasCompleteHistory });
2486
+ const appMessages = mapAppServerMessages(threadWithTurns);
2487
+ messages = hasCompleteHistory ? mergeAppServerMessagesWithLocalStatus(appMessages, cachedMessages) : mergeThreadMessagePages(appMessages, cachedMessages);
2270
2488
  messagesByThreadId.set(threadId, messages);
2271
2489
  loadedMessages = true;
2272
2490
  } else if (!loadedMessages) scheduleAppServerHistoryLoad(threadId, cachedMessages);
@@ -2927,11 +3145,10 @@ async function createAppServerThreadRecord(input) {
2927
3145
  const thread = await input.appServer.startThread({
2928
3146
  approvalPolicy: runtime.approvalPolicy,
2929
3147
  cwd: input.workspacePath,
2930
- experimentalRawEvents: false,
2931
3148
  model: input.options.model ?? null,
2932
- persistExtendedHistory: true,
2933
3149
  sandbox: runtime.sandbox,
2934
- serviceTier: input.options.serviceTier ?? null
3150
+ serviceTier: input.options.serviceTier ?? null,
3151
+ threadSource: "codex-relay"
2935
3152
  });
2936
3153
  const metadata = ThreadSummarySchema.parse({
2937
3154
  ...mapAppServerThread({
@@ -3101,11 +3318,25 @@ async function runPromptStreamed(input) {
3101
3318
  message: assistantMessage
3102
3319
  });
3103
3320
  for await (const event of streamed.events) {
3321
+ activeThreadId = replaceLocalThreadId(input.threads, input.messagesByThreadId, input.liveThreads, activeThreadId, getThreadId(thread));
3104
3322
  const kind = classifyStreamEvent(event);
3105
3323
  const text = extractStreamText(event);
3106
3324
  if (kind === "error") throw new Error(text ?? "Codex run failed.");
3107
- if (!text) continue;
3108
3325
  if (kind === "assistant") {
3326
+ if (!text) continue;
3327
+ if (isCompletedAgentMessageEvent(event)) {
3328
+ assistantMessage = updateMessage(input.messagesByThreadId, activeThreadId, assistantMessage.id, { content: text });
3329
+ threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
3330
+ state: "running",
3331
+ lastResult: assistantMessage.content
3332
+ });
3333
+ sendSse(input.controller, input.encoder, input.secureSession, {
3334
+ type: "thread.message.created",
3335
+ thread: threadSummary,
3336
+ message: assistantMessage
3337
+ });
3338
+ continue;
3339
+ }
3109
3340
  const assistantPatch = appendMessageDelta(input.messagesByThreadId, activeThreadId, assistantMessage.id, text);
3110
3341
  assistantMessage = assistantPatch.message;
3111
3342
  updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
@@ -3119,14 +3350,18 @@ async function runPromptStreamed(input) {
3119
3350
  delta: assistantPatch.delta
3120
3351
  });
3121
3352
  } else {
3122
- const structured = structuredStreamMessage(kind, event, text);
3123
- const statusMessage = appendMessage(input.messagesByThreadId, activeThreadId, {
3353
+ if (!text && !isRenderableStructuredStreamEvent(event)) continue;
3354
+ const structured = structuredStreamMessage(kind, event, text ?? "Codex activity");
3355
+ const itemId = firstString(eventItem(event), ["id"]);
3356
+ const existingMessage = itemId ? input.messagesByThreadId.get(activeThreadId)?.find((message) => message.id === itemId) : void 0;
3357
+ const message = {
3124
3358
  role: kind,
3125
3359
  kind: structured.kind,
3126
3360
  content: structured.content,
3127
3361
  details: structured.details,
3128
- state: "completed"
3129
- });
3362
+ state: streamItemState(event)
3363
+ };
3364
+ const statusMessage = existingMessage ? updateMessage(input.messagesByThreadId, activeThreadId, existingMessage.id, message) : itemId ? appendMessageWithId(input.messagesByThreadId, activeThreadId, itemId, message) : appendMessage(input.messagesByThreadId, activeThreadId, message);
3130
3365
  threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
3131
3366
  sendSse(input.controller, input.encoder, input.secureSession, {
3132
3367
  type: "thread.message.created",
@@ -3179,6 +3414,7 @@ async function startAppServerTurn(appServer, threadId, input) {
3179
3414
  const runtime = resolveAppServerRuntime(input.runOptions, input.workspacePath);
3180
3415
  const params = {
3181
3416
  approvalPolicy: runtime.approvalPolicy,
3417
+ clientUserMessageId: input.id,
3182
3418
  collaborationMode: appServerCollaborationMode(input.runOptions),
3183
3419
  cwd: input.workspacePath,
3184
3420
  effort: input.runOptions.reasoningEffort ?? null,
@@ -3213,9 +3449,8 @@ async function resumeAppServerThread(appServer, threadId, input, runtime) {
3213
3449
  await appServer.resumeThread({
3214
3450
  approvalPolicy: runtime.approvalPolicy,
3215
3451
  cwd: input.workspacePath,
3216
- excludeTurns: false,
3452
+ excludeTurns: true,
3217
3453
  model: input.runOptions.model ?? null,
3218
- persistExtendedHistory: true,
3219
3454
  sandbox: runtime.sandbox,
3220
3455
  serviceTier: input.runOptions.serviceTier ?? null,
3221
3456
  threadId
@@ -3262,10 +3497,12 @@ async function streamRunningAppServerThread(input) {
3262
3497
  let observedInputRequest = false;
3263
3498
  let observedTurnActivity = false;
3264
3499
  let producedTurnOutput = false;
3500
+ const asyncAgentMessageIds = /* @__PURE__ */ new Set();
3265
3501
  let threadSummary = input.threads.get(input.threadId);
3266
3502
  let cleanupNotificationHandler = () => void 0;
3267
3503
  let cleanupRequestHandler = () => void 0;
3268
3504
  let handlersCleaned = false;
3505
+ let streamFinished = false;
3269
3506
  const cleanupHandlers = () => {
3270
3507
  if (handlersCleaned) return;
3271
3508
  handlersCleaned = true;
@@ -3286,6 +3523,7 @@ async function streamRunningAppServerThread(input) {
3286
3523
  if (approval.kind === "structuredUserInput") {
3287
3524
  const pending = {
3288
3525
  appServer: input.appServer,
3526
+ isBlocking: approval.isBlocking,
3289
3527
  kind: approval.kind,
3290
3528
  method: request.method,
3291
3529
  questions: approval.questions,
@@ -3330,6 +3568,7 @@ async function streamRunningAppServerThread(input) {
3330
3568
  const finish = (error) => {
3331
3569
  if (settled) return;
3332
3570
  settled = true;
3571
+ streamFinished = true;
3333
3572
  cleanupHandlers();
3334
3573
  if (error === void 0) resolve();
3335
3574
  else reject(error);
@@ -3340,6 +3579,20 @@ async function streamRunningAppServerThread(input) {
3340
3579
  if (notificationThreadId && notificationThreadId !== input.threadId) return;
3341
3580
  try {
3342
3581
  switch (notification.method) {
3582
+ case "serverRequest/resolved": {
3583
+ const requestId = appServerRequestIdFromParams(params);
3584
+ if (requestId === void 0) return;
3585
+ const approvalId = appServerApprovalId(input.threadId, requestId);
3586
+ const pending = input.pendingApprovals.get(approvalId);
3587
+ if (!pending || pending.threadId !== input.threadId) return;
3588
+ input.pendingApprovals.delete(approvalId);
3589
+ if (pending.kind === "structuredUserInput") sendSse(input.controller, input.encoder, input.secureSession, {
3590
+ type: "thread.input_request.resolved",
3591
+ requestId: approvalId,
3592
+ threadId: input.threadId
3593
+ });
3594
+ return;
3595
+ }
3343
3596
  case "thread/status/changed": {
3344
3597
  const state = mapAppServerThreadState(params?.status);
3345
3598
  if (state !== "running" && observedTurnActivity) return;
@@ -3375,11 +3628,13 @@ async function streamRunningAppServerThread(input) {
3375
3628
  if (!item || typeof item !== "object") return;
3376
3629
  const message = upsertAppServerItemMessage(input.messagesByThreadId, input.threadId, firstString(params, ["turnId"]) ?? activeTurnId, item);
3377
3630
  if (!message) return;
3378
- if (message.role !== "user") producedTurnOutput = true;
3379
- if (message.role === "assistant") assistantMessageId = message.id;
3631
+ const isAsyncAgentMessage = isAsyncAppServerAgentMessage(item);
3632
+ if (isAsyncAgentMessage) asyncAgentMessageIds.add(message.id);
3633
+ if (message.role !== "user" && !isAsyncAgentMessage) producedTurnOutput = true;
3634
+ if (message.role === "assistant" && !isAsyncAgentMessage) assistantMessageId = message.id;
3380
3635
  threadSummary = updateThread(input.threads, input.messagesByThreadId, input.threadId, {
3381
3636
  state: "running",
3382
- lastResult: message.role === "assistant" ? message.content : void 0
3637
+ ...message.role === "assistant" && !isAsyncAgentMessage ? { lastResult: message.content } : {}
3383
3638
  });
3384
3639
  sendSse(input.controller, input.encoder, input.secureSession, {
3385
3640
  type: notification.method === "item/completed" && message.role === "assistant" ? "thread.message.completed" : "thread.message.created",
@@ -3399,13 +3654,16 @@ async function streamRunningAppServerThread(input) {
3399
3654
  state: "streaming",
3400
3655
  turnId: firstString(params, ["turnId"]) ?? activeTurnId
3401
3656
  });
3402
- assistantMessageId = itemId;
3403
- producedTurnOutput = true;
3657
+ const isAsyncAgentMessage = asyncAgentMessageIds.has(itemId);
3658
+ if (!isAsyncAgentMessage) {
3659
+ assistantMessageId = itemId;
3660
+ producedTurnOutput = true;
3661
+ }
3404
3662
  const patch = appendMessageDelta(input.messagesByThreadId, input.threadId, itemId, delta);
3405
3663
  const message = patch.message;
3406
3664
  updateThread(input.threads, input.messagesByThreadId, input.threadId, {
3407
3665
  state: "running",
3408
- lastResult: message.content
3666
+ ...isAsyncAgentMessage ? {} : { lastResult: message.content }
3409
3667
  });
3410
3668
  if (patch.delta) sendSse(input.controller, input.encoder, input.secureSession, {
3411
3669
  type: "thread.message.delta",
@@ -3441,6 +3699,27 @@ async function streamRunningAppServerThread(input) {
3441
3699
  case "turn/failed": {
3442
3700
  observedTurnActivity = true;
3443
3701
  const state = terminalTurnState(notification.method, params);
3702
+ const terminalTurn = appServerTurnFromParams(params);
3703
+ if (terminalTurn) {
3704
+ activeTurnId = terminalTurn.id;
3705
+ for (const item of terminalTurn.items) {
3706
+ const message = upsertAppServerItemMessage(input.messagesByThreadId, input.threadId, terminalTurn.id, item);
3707
+ if (!message) continue;
3708
+ const isAsyncAgentMessage = isAsyncAppServerAgentMessage(item);
3709
+ if (isAsyncAgentMessage) asyncAgentMessageIds.add(message.id);
3710
+ if (message.role !== "user" && !isAsyncAgentMessage) producedTurnOutput = true;
3711
+ if (message.role === "assistant" && !isAsyncAgentMessage) assistantMessageId = message.id;
3712
+ threadSummary = updateThread(input.threads, input.messagesByThreadId, input.threadId, {
3713
+ state: "running",
3714
+ ...message.role === "assistant" && !isAsyncAgentMessage ? { lastResult: message.content } : {}
3715
+ });
3716
+ sendSse(input.controller, input.encoder, input.secureSession, {
3717
+ type: message.role === "assistant" ? "thread.message.completed" : "thread.message.created",
3718
+ thread: threadSummary,
3719
+ message
3720
+ });
3721
+ }
3722
+ }
3444
3723
  relayDebugLog("app_server.turn.terminal", {
3445
3724
  method: notification.method,
3446
3725
  state,
@@ -3460,7 +3739,8 @@ async function streamRunningAppServerThread(input) {
3460
3739
  finish();
3461
3740
  return;
3462
3741
  }
3463
- if (assistantMessageId) {
3742
+ const assistantMessage = assistantMessageId ? input.messagesByThreadId.get(input.threadId)?.find((message) => message.id === assistantMessageId) : void 0;
3743
+ if (assistantMessageId && assistantMessage?.state !== "completed") {
3464
3744
  const completedMessage = updateMessage(input.messagesByThreadId, input.threadId, assistantMessageId, { state: "completed" });
3465
3745
  sendSse(input.controller, input.encoder, input.secureSession, {
3466
3746
  type: "thread.message.completed",
@@ -3518,8 +3798,15 @@ async function streamRunningAppServerThread(input) {
3518
3798
  removeAbortListener = () => input.signal.removeEventListener("abort", onAbort);
3519
3799
  });
3520
3800
  try {
3521
- const appServerThread = await Promise.race([input.appServer.readThread(input.threadId, { includeTurns: false }), aborted.then(() => void 0)]);
3801
+ const appServerThread = await Promise.race([typeof input.appServer.resumeThread === "function" ? input.appServer.resumeThread({
3802
+ excludeTurns: true,
3803
+ threadId: input.threadId
3804
+ }) : input.appServer.readThread(input.threadId, { includeTurns: false }), aborted.then(() => void 0)]);
3522
3805
  if (!appServerThread || input.signal.aborted) return;
3806
+ if (streamFinished) {
3807
+ await completed;
3808
+ return;
3809
+ }
3523
3810
  threadSummary = rememberAppServerThread(input.threads, appServerThread);
3524
3811
  sendSse(input.controller, input.encoder, input.secureSession, {
3525
3812
  type: "thread.state.changed",
@@ -3553,6 +3840,7 @@ async function runAppServerPromptStreamed(input) {
3553
3840
  let handedOffToQueuedTurn = false;
3554
3841
  let observedInputRequest = false;
3555
3842
  let producedTurnOutput = false;
3843
+ const asyncAgentMessageIds = /* @__PURE__ */ new Set();
3556
3844
  let userMessage = appendMessage(input.messagesByThreadId, activeThreadId, {
3557
3845
  role: "user",
3558
3846
  content: displayPrompt,
@@ -3589,6 +3877,7 @@ async function runAppServerPromptStreamed(input) {
3589
3877
  if (approval.kind === "structuredUserInput") {
3590
3878
  const pending = {
3591
3879
  appServer: input.appServer,
3880
+ isBlocking: approval.isBlocking,
3592
3881
  kind: approval.kind,
3593
3882
  method: request.method,
3594
3883
  questions: approval.questions,
@@ -3668,7 +3957,8 @@ async function runAppServerPromptStreamed(input) {
3668
3957
  resolveCompleted();
3669
3958
  return;
3670
3959
  }
3671
- if (assistantMessageId) {
3960
+ const assistantMessage = assistantMessageId ? input.messagesByThreadId.get(activeThreadId)?.find((message) => message.id === assistantMessageId) : void 0;
3961
+ if (assistantMessageId && assistantMessage?.state !== "completed") {
3672
3962
  const completedMessage = updateMessage(input.messagesByThreadId, activeThreadId, assistantMessageId, { state: "completed" });
3673
3963
  sendSse(input.controller, input.encoder, input.secureSession, {
3674
3964
  type: "thread.message.completed",
@@ -3723,8 +4013,7 @@ async function runAppServerPromptStreamed(input) {
3723
4013
  cleanupNotificationHandler();
3724
4014
  resolveCompleted();
3725
4015
  }
3726
- async function processReturnedTurn(turn) {
3727
- if (finalizedTurnIds.has(turn.id)) return;
4016
+ function processTurnItems(turn) {
3728
4017
  activeTurnId = turn.id;
3729
4018
  input.activeAppServerTurnIdsByThreadId.set(activeThreadId, turn.id);
3730
4019
  const turnIsRunning = isAppServerTurnRunning(turn);
@@ -3750,11 +4039,13 @@ async function runAppServerPromptStreamed(input) {
3750
4039
  }
3751
4040
  const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, turn.id, item);
3752
4041
  if (!message) continue;
3753
- if (message.role !== "user") producedTurnOutput = true;
3754
- if (message.role === "assistant" && turnIsRunning) assistantMessageId = message.id;
4042
+ const isAsyncAgentMessage = isAsyncAppServerAgentMessage(item);
4043
+ if (isAsyncAgentMessage) asyncAgentMessageIds.add(message.id);
4044
+ if (message.role !== "user" && !isAsyncAgentMessage) producedTurnOutput = true;
4045
+ if (message.role === "assistant" && !isAsyncAgentMessage) assistantMessageId = message.id;
3755
4046
  threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
3756
4047
  state: "running",
3757
- lastResult: message.role === "assistant" ? message.content : void 0
4048
+ ...message.role === "assistant" && !isAsyncAgentMessage ? { lastResult: message.content } : {}
3758
4049
  });
3759
4050
  sendSse(input.controller, input.encoder, input.secureSession, {
3760
4051
  type: message.role === "assistant" ? "thread.message.completed" : "thread.message.created",
@@ -3762,7 +4053,11 @@ async function runAppServerPromptStreamed(input) {
3762
4053
  message
3763
4054
  });
3764
4055
  }
3765
- if (turnIsRunning) return;
4056
+ return turnIsRunning;
4057
+ }
4058
+ async function processReturnedTurn(turn) {
4059
+ if (finalizedTurnIds.has(turn.id)) return;
4060
+ if (processTurnItems(turn)) return;
3766
4061
  const params = { turn };
3767
4062
  const state = terminalTurnState("turn/completed", params);
3768
4063
  await finishTerminalTurn({
@@ -3788,6 +4083,20 @@ async function runAppServerPromptStreamed(input) {
3788
4083
  if (threadId && threadId !== activeThreadId) return;
3789
4084
  try {
3790
4085
  switch (notification.method) {
4086
+ case "serverRequest/resolved": {
4087
+ const requestId = appServerRequestIdFromParams(params);
4088
+ if (requestId === void 0) return;
4089
+ const approvalId = appServerApprovalId(activeThreadId, requestId);
4090
+ const pending = input.pendingApprovals.get(approvalId);
4091
+ if (!pending || pending.threadId !== activeThreadId) return;
4092
+ input.pendingApprovals.delete(approvalId);
4093
+ if (pending.kind === "structuredUserInput") sendSse(input.controller, input.encoder, input.secureSession, {
4094
+ type: "thread.input_request.resolved",
4095
+ requestId: approvalId,
4096
+ threadId: activeThreadId
4097
+ });
4098
+ return;
4099
+ }
3791
4100
  case "thread/status/changed": {
3792
4101
  const status = params?.status;
3793
4102
  const state = mapAppServerThreadState(status);
@@ -3840,11 +4149,13 @@ async function runAppServerPromptStreamed(input) {
3840
4149
  const turnId = firstString(params, ["turnId"]) ?? activeTurnId;
3841
4150
  const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, turnId, item);
3842
4151
  if (!message) return;
3843
- if (message.role !== "user") producedTurnOutput = true;
3844
- if (message.role === "assistant") assistantMessageId = message.id;
4152
+ const isAsyncAgentMessage = isAsyncAppServerAgentMessage(item);
4153
+ if (isAsyncAgentMessage) asyncAgentMessageIds.add(message.id);
4154
+ if (message.role !== "user" && !isAsyncAgentMessage) producedTurnOutput = true;
4155
+ if (message.role === "assistant" && !isAsyncAgentMessage) assistantMessageId = message.id;
3845
4156
  threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
3846
4157
  state: "running",
3847
- lastResult: message.role === "assistant" ? message.content : void 0
4158
+ ...message.role === "assistant" && !isAsyncAgentMessage ? { lastResult: message.content } : {}
3848
4159
  });
3849
4160
  sendSse(input.controller, input.encoder, input.secureSession, {
3850
4161
  type: notification.method === "item/completed" && message.role === "assistant" ? "thread.message.completed" : "thread.message.created",
@@ -3863,13 +4174,14 @@ async function runAppServerPromptStreamed(input) {
3863
4174
  state: "streaming",
3864
4175
  turnId: firstString(params, ["turnId"]) ?? activeTurnId
3865
4176
  });
3866
- assistantMessageId = itemId;
4177
+ const isAsyncAgentMessage = asyncAgentMessageIds.has(itemId);
4178
+ if (!isAsyncAgentMessage) assistantMessageId = itemId;
3867
4179
  const patch = appendMessageDelta(input.messagesByThreadId, activeThreadId, itemId, delta);
3868
4180
  const message = patch.message;
3869
- producedTurnOutput = true;
4181
+ if (!isAsyncAgentMessage) producedTurnOutput = true;
3870
4182
  updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
3871
4183
  state: "running",
3872
- lastResult: message.content
4184
+ ...isAsyncAgentMessage ? {} : { lastResult: message.content }
3873
4185
  });
3874
4186
  if (patch.delta) sendSse(input.controller, input.encoder, input.secureSession, {
3875
4187
  type: "thread.message.delta",
@@ -3903,14 +4215,19 @@ async function runAppServerPromptStreamed(input) {
3903
4215
  case "turn/completed":
3904
4216
  case "turn/failed": {
3905
4217
  const state = terminalTurnState(notification.method, params);
4218
+ const terminalTurn = appServerTurnFromParams(params);
3906
4219
  const terminalTurnId = firstString(params, ["turnId"]) ?? turnIdFromParams(params) ?? activeTurnId;
3907
4220
  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
- }));
4221
+ await input.runAppServerMutation(activeThreadId, async () => {
4222
+ if (terminalTurnId && finalizedTurnIds.has(terminalTurnId) || terminalTurnId && activeTurnId && terminalTurnId !== activeTurnId) return;
4223
+ if (terminalTurn && terminalTurn.id === terminalTurnId) processTurnItems(terminalTurn);
4224
+ await finishTerminalTurn({
4225
+ lastError: state === "failed" ? turnErrorMessage(params) : void 0,
4226
+ method: notification.method,
4227
+ state,
4228
+ turnId: terminalTurnId
4229
+ });
4230
+ });
3914
4231
  }).catch((error) => {
3915
4232
  cleanupNotificationHandler();
3916
4233
  rejectCompleted(error);
@@ -3926,36 +4243,34 @@ async function runAppServerPromptStreamed(input) {
3926
4243
  });
3927
4244
  try {
3928
4245
  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
- }
4246
+ debugStream("wait idle begin", activeThreadId);
4247
+ await waitForAppServerThreadIdle({
4248
+ appServer: input.appServer,
4249
+ onWaiting() {
4250
+ if (waitingForActiveTurnMessageId) return;
4251
+ const message = appendMessage(input.messagesByThreadId, activeThreadId, {
4252
+ role: "status",
4253
+ content: "Waiting for the current Codex turn to finish.",
4254
+ state: "streaming"
4255
+ });
4256
+ waitingForActiveTurnMessageId = message.id;
4257
+ threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
4258
+ sendSse(input.controller, input.encoder, input.secureSession, {
4259
+ type: "thread.message.created",
4260
+ thread: threadSummary,
4261
+ message
4262
+ });
4263
+ },
4264
+ onWaitingHeartbeat() {
4265
+ threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
4266
+ sendSse(input.controller, input.encoder, input.secureSession, {
4267
+ type: "thread.state.changed",
4268
+ thread: threadSummary
4269
+ });
4270
+ },
4271
+ threadId: activeThreadId
4272
+ });
4273
+ debugStream("wait idle complete", activeThreadId);
3959
4274
  if (waitingForActiveTurnMessageId) {
3960
4275
  const message = updateMessage(input.messagesByThreadId, activeThreadId, waitingForActiveTurnMessageId, {
3961
4276
  content: "Current Codex turn finished. Starting your reply.",
@@ -3973,7 +4288,7 @@ async function runAppServerPromptStreamed(input) {
3973
4288
  try {
3974
4289
  turn = await startAndProcessAppServerTurn(activeThreadId, {
3975
4290
  attachments: input.attachments,
3976
- id: randomUUID(),
4291
+ id: userMessage.id,
3977
4292
  prompt,
3978
4293
  runOptions: input.runOptions,
3979
4294
  skills: input.skills,
@@ -4007,7 +4322,7 @@ async function runAppServerPromptStreamed(input) {
4007
4322
  });
4008
4323
  turn = await startAndProcessAppServerTurn(activeThreadId, {
4009
4324
  attachments: input.attachments,
4010
- id: randomUUID(),
4325
+ id: userMessage.id,
4011
4326
  prompt,
4012
4327
  runOptions: input.runOptions,
4013
4328
  skills: input.skills,
@@ -4120,11 +4435,10 @@ async function recoverMissingAppServerThread(input) {
4120
4435
  const thread = await input.appServer.startThread({
4121
4436
  approvalPolicy: runtime.approvalPolicy,
4122
4437
  cwd: input.workspacePath,
4123
- experimentalRawEvents: false,
4124
4438
  model: input.runOptions.model ?? null,
4125
- persistExtendedHistory: true,
4126
4439
  sandbox: runtime.sandbox,
4127
- serviceTier: input.runOptions.serviceTier ?? null
4440
+ serviceTier: input.runOptions.serviceTier ?? null,
4441
+ threadSource: "codex-relay"
4128
4442
  });
4129
4443
  const recoveredThread = mapAppServerThread({
4130
4444
  ...thread,
@@ -4277,7 +4591,11 @@ function appendMessageWithId(messagesByThreadId, threadId, id, input) {
4277
4591
  function upsertAppServerItemMessage(messagesByThreadId, threadId, turnId, item) {
4278
4592
  const message = mapAppServerItem(threadId, appServerTurnShell(turnId), item);
4279
4593
  if (!message) return;
4280
- if (messagesByThreadId.get(threadId)?.some((candidate) => candidate.id === item.id)) return updateMessage(messagesByThreadId, threadId, item.id, message);
4594
+ const existing = messagesByThreadId.get(threadId)?.find((candidate) => candidate.id === item.id);
4595
+ if (existing) return updateMessage(messagesByThreadId, threadId, item.id, {
4596
+ ...message,
4597
+ createdAt: existing.createdAt
4598
+ });
4281
4599
  return appendMessageWithId(messagesByThreadId, threadId, item.id, {
4282
4600
  role: message.role,
4283
4601
  kind: message.kind,
@@ -4295,6 +4613,7 @@ function replaceDuplicateInitialUserMessage(messagesByThreadId, threadId, turnId
4295
4613
  }
4296
4614
  function isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, localMessageId, prompt) {
4297
4615
  if (item.type !== "userMessage" || !("content" in item) || !Array.isArray(item.content)) return false;
4616
+ if (item.clientId) return item.clientId === localMessageId;
4298
4617
  const localMessage = messagesByThreadId.get(threadId)?.find((message) => message.id === localMessageId);
4299
4618
  const skills = appServerUserMessageSkills(item);
4300
4619
  const normalizeContent = (content) => stripPromptSkillMentions(normalizeImageMessageContent(content), skills);
@@ -4966,6 +5285,7 @@ function hasExplicitRunOptions(options) {
4966
5285
  function mapAppServerThread(thread, fallbackMessageCount) {
4967
5286
  const createdAt = fromUnixSeconds(thread.createdAt);
4968
5287
  const updatedAt = fromUnixSeconds(thread.updatedAt);
5288
+ const lastActivityAt = fromUnixSeconds(thread.recencyAt ?? thread.updatedAt);
4969
5289
  const messageCount = Math.max(thread.turns ? countThreadMessages(thread) : 0, fallbackMessageCount ?? 0);
4970
5290
  const mappedState = mapAppServerThreadState(thread.status, thread.turns);
4971
5291
  return ThreadSummarySchema.parse({
@@ -4979,7 +5299,7 @@ function mapAppServerThread(thread, fallbackMessageCount) {
4979
5299
  source: thread.source,
4980
5300
  messageCount,
4981
5301
  lastMessagePreview: thread.preview ? preview(thread.preview) : void 0,
4982
- lastActivityAt: updatedAt
5302
+ lastActivityAt
4983
5303
  });
4984
5304
  }
4985
5305
  function rememberAppServerThread(threads, thread, options = {}) {
@@ -5032,6 +5352,12 @@ function mapAppServerMessages(thread) {
5032
5352
  }
5033
5353
  return messages;
5034
5354
  }
5355
+ function appServerThreadHasCompleteHistory(thread) {
5356
+ return (thread.turns ?? []).every((turn) => turn.itemsView === void 0 || turn.itemsView === "full");
5357
+ }
5358
+ function isAsyncAppServerAgentMessage(item) {
5359
+ return item.type === "agentMessage" && "delivery" in item && item.delivery === "async";
5360
+ }
5035
5361
  function mergeAppServerMessagesWithLocalStatus(appMessages, localMessages) {
5036
5362
  const appMessageIds = new Set(appMessages.map((message) => message.id));
5037
5363
  const localStatusMessages = localMessages.filter((message) => message.role === "status" && !appMessageIds.has(message.id));
@@ -5162,13 +5488,18 @@ function readSessionIndexThreadTitle(threadId) {
5162
5488
  } catch {}
5163
5489
  }
5164
5490
  }
5165
- function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePath) {
5166
- const rolloutPath = findRolloutFileForThread(threadId);
5491
+ function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePath, selectedRolloutPath) {
5492
+ const rolloutPath = selectedRolloutPath && selectedRolloutPath.endsWith(".jsonl") && existsSync(selectedRolloutPath) ? selectedRolloutPath : findRolloutFileForThread(threadId);
5167
5493
  if (!rolloutPath) return {
5168
5494
  messageCountLowerBound: 0,
5169
5495
  messages: [],
5170
5496
  rolloutPath
5171
5497
  };
5498
+ if (isPaginatedRolloutFile(rolloutPath)) return {
5499
+ messageCountLowerBound: 0,
5500
+ messages: [],
5501
+ rolloutPath
5502
+ };
5172
5503
  const collected = [];
5173
5504
  const applyPatchInputs = /* @__PURE__ */ new Map();
5174
5505
  const handledApplyPatchCallIds = /* @__PURE__ */ new Set();
@@ -5208,6 +5539,25 @@ function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePat
5208
5539
  rolloutPath
5209
5540
  };
5210
5541
  }
5542
+ function isPaginatedRolloutFile(rolloutPath) {
5543
+ let descriptor;
5544
+ try {
5545
+ descriptor = openSync(rolloutPath, "r");
5546
+ const buffer = Buffer.allocUnsafe(64 * 1024);
5547
+ const bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
5548
+ const prefix = buffer.toString("utf8", 0, bytesRead);
5549
+ for (const line of prefix.split("\n")) {
5550
+ if (!line.trim()) continue;
5551
+ const record = JSON.parse(line);
5552
+ if (record.type === "session_meta") return record.payload?.history_mode === "paginated";
5553
+ }
5554
+ } catch {
5555
+ return false;
5556
+ } finally {
5557
+ if (descriptor !== void 0) closeSync(descriptor);
5558
+ }
5559
+ return false;
5560
+ }
5211
5561
  function findRolloutFileForThread(threadId) {
5212
5562
  const sessionsRoot = join(process.env.CODEX_HOME || join(homedir(), ".codex"), "sessions");
5213
5563
  if (!existsSync(sessionsRoot)) return;
@@ -5572,12 +5922,16 @@ function mapAppServerItem(threadId, turn, item) {
5572
5922
  const agentItem = item;
5573
5923
  const planContent = proposedPlanContent(agentItem.text);
5574
5924
  const messageParts = appServerAgentMessageParts(agentItem);
5925
+ const details = planContent ? { raw: agentItem.text } : agentItem.delivery || messageParts.details ? {
5926
+ ...messageParts.details,
5927
+ delivery: agentItem.delivery ?? void 0
5928
+ } : void 0;
5575
5929
  return ChatMessageSchema.parse({
5576
5930
  ...base,
5577
5931
  role: "assistant",
5578
5932
  kind: planContent ? "plan" : void 0,
5579
5933
  content: planContent ?? messageParts.content,
5580
- details: planContent ? { raw: agentItem.text } : messageParts.details
5934
+ details
5581
5935
  });
5582
5936
  }
5583
5937
  case "reasoning": {
@@ -5977,6 +6331,21 @@ function structuredStreamMessage(role, event, fallbackContent) {
5977
6331
  type
5978
6332
  }
5979
6333
  };
6334
+ case "todo_list": {
6335
+ const items = Array.isArray(item?.items) ? item.items : [];
6336
+ return {
6337
+ kind: "plan",
6338
+ content: items.flatMap((entry) => {
6339
+ if (!entry || typeof entry !== "object") return [];
6340
+ const todo = entry;
6341
+ return typeof todo.text === "string" ? [`[${todo.completed === true ? "x" : " "}] ${todo.text}`] : [];
6342
+ }).join("\n") || fallbackContent,
6343
+ details: {
6344
+ items,
6345
+ type
6346
+ }
6347
+ };
6348
+ }
5980
6349
  default: return {
5981
6350
  kind: kindFromProtocolType(type) ?? (role === "tool" ? "toolActivity" : "unknown"),
5982
6351
  content: fallbackContent,
@@ -5984,6 +6353,17 @@ function structuredStreamMessage(role, event, fallbackContent) {
5984
6353
  };
5985
6354
  }
5986
6355
  }
6356
+ function isCompletedAgentMessageEvent(event) {
6357
+ if (!event || typeof event !== "object") return false;
6358
+ return event.type === "item.completed" && eventItem(event)?.type === "agent_message";
6359
+ }
6360
+ function isRenderableStructuredStreamEvent(event) {
6361
+ const type = eventItem(event)?.type;
6362
+ return type === "file_change" || type === "mcp_tool_call" || type === "todo_list" || type === "web_search";
6363
+ }
6364
+ function streamItemState(event) {
6365
+ return event?.type === "item.completed" ? "completed" : "streaming";
6366
+ }
5987
6367
  function isApprovalServerRequest(method) {
5988
6368
  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
6369
  }
@@ -6046,7 +6426,7 @@ function approvalMessageFromRequest(request) {
6046
6426
  const threadId = firstString(params, ["threadId", "conversationId"]);
6047
6427
  const turnId = firstString(params, ["turnId"]) ?? void 0;
6048
6428
  if (!threadId) return;
6049
- const approvalId = `approval-${request.id}`;
6429
+ const approvalId = appServerApprovalId(threadId, request.id);
6050
6430
  switch (request.method) {
6051
6431
  case "item/commandExecution/requestApproval": {
6052
6432
  const command = firstString(params, ["command"]) ?? "Command execution";
@@ -6134,6 +6514,7 @@ function approvalMessageFromRequest(request) {
6134
6514
  questions
6135
6515
  },
6136
6516
  kind: "structuredUserInput",
6517
+ isBlocking: typeof params?.isBlocking === "boolean" ? params.isBlocking : true,
6137
6518
  questions,
6138
6519
  threadId,
6139
6520
  turnId
@@ -6188,6 +6569,7 @@ function pendingInputOptions(value) {
6188
6569
  function pendingInputRequestFromApproval(approval, threadId) {
6189
6570
  return {
6190
6571
  id: approval.approvalId,
6572
+ isBlocking: approval.isBlocking ?? true,
6191
6573
  questions: approval.questions ?? [],
6192
6574
  threadId,
6193
6575
  turnId: approval.turnId
@@ -6198,6 +6580,7 @@ function pendingInputRequestsForThread(pendingApprovals, threadId) {
6198
6580
  if (pending.kind !== "structuredUserInput" || pending.threadId !== threadId) return [];
6199
6581
  return [{
6200
6582
  id: approvalId,
6583
+ isBlocking: pending.isBlocking ?? true,
6201
6584
  questions: pending.questions ?? [],
6202
6585
  threadId,
6203
6586
  turnId: pending.turnId
@@ -6293,6 +6676,20 @@ function turnIdFromParams(params) {
6293
6676
  const turn = params?.turn;
6294
6677
  return turn && typeof turn === "object" ? firstString(turn, ["id"]) : void 0;
6295
6678
  }
6679
+ function appServerRequestIdFromParams(params) {
6680
+ const requestId = params?.requestId;
6681
+ return typeof requestId === "number" || typeof requestId === "string" ? requestId : void 0;
6682
+ }
6683
+ function appServerApprovalId(threadId, requestId) {
6684
+ return `approval-${createHash("sha256").update(threadId).update("\0").update(String(requestId)).digest("hex").slice(0, 24)}`;
6685
+ }
6686
+ function appServerTurnFromParams(params) {
6687
+ const turn = params?.turn;
6688
+ if (!turn || typeof turn !== "object") return;
6689
+ const record = turn;
6690
+ if (typeof record.id !== "string" || !Array.isArray(record.items)) return;
6691
+ return turn;
6692
+ }
6296
6693
  function turnStatus(params) {
6297
6694
  if (typeof params?.status === "string") return params.status;
6298
6695
  const turn = params?.turn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-relay",
3
- "version": "1.4.8",
3
+ "version": "1.4.10",
4
4
  "description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -41,7 +41,8 @@
41
41
  "@noble/ciphers": "2.2.0",
42
42
  "@noble/curves": "2.2.0",
43
43
  "@noble/hashes": "2.2.0",
44
- "@openai/codex-sdk": "0.145.0",
44
+ "@openai/codex": "0.149.1",
45
+ "@openai/codex-sdk": "0.149.1",
45
46
  "base64-js": "1.5.1",
46
47
  "commander": "^14.0.3",
47
48
  "es-git": "^0.6.0",
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(),