codex-relay 1.4.0 → 1.4.1

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.
Files changed (2) hide show
  1. package/dist/src.js +83 -45
  2. package/package.json +1 -1
package/dist/src.js CHANGED
@@ -1418,6 +1418,7 @@ function createApp(options = {}) {
1418
1418
  const pendingApprovals = /* @__PURE__ */ new Map();
1419
1419
  const resolvedApprovals = /* @__PURE__ */ new Map();
1420
1420
  const queuedInputsByThreadId = /* @__PURE__ */ new Map();
1421
+ const threadOperationTails = /* @__PURE__ */ new Map();
1421
1422
  const workspaceTerminalSessions = /* @__PURE__ */ new Map();
1422
1423
  const activeAppServerTurnIdsByThreadId = /* @__PURE__ */ new Map();
1423
1424
  const appServerHistoryLoadsByThreadId = /* @__PURE__ */ new Map();
@@ -1425,6 +1426,15 @@ function createApp(options = {}) {
1425
1426
  const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
1426
1427
  const activeStreamControllers = /* @__PURE__ */ new Set();
1427
1428
  const threadOptions = { workingDirectory: workspacePath };
1429
+ function runThreadOperation(threadId, operation) {
1430
+ const result = (threadOperationTails.get(threadId) ?? Promise.resolve()).then(operation, operation);
1431
+ const tail = result.then(() => void 0, () => void 0);
1432
+ threadOperationTails.set(threadId, tail);
1433
+ tail.finally(() => {
1434
+ if (threadOperationTails.get(threadId) === tail) threadOperationTails.delete(threadId);
1435
+ });
1436
+ return result;
1437
+ }
1428
1438
  const pushNotificationDispatcher = options.pairing ? createPushNotificationDispatcher({
1429
1439
  sender: options.pushNotificationSender ?? createExpoPushNotificationSender(),
1430
1440
  sessions: options.pairing.sessions
@@ -2392,44 +2402,46 @@ function createApp(options = {}) {
2392
2402
  });
2393
2403
  app.post("/v1/threads/:threadId/input", async (c) => {
2394
2404
  const threadId = c.req.param("threadId");
2395
- const knownThread = await ensureKnownThread({
2396
- appServer,
2397
- threadId,
2398
- messagesByThreadId,
2399
- threads
2400
- });
2401
- if (!knownThread) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
2402
- if (!appServer) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("unsupported", "Running-thread input requires the Codex app-server."), 409);
2403
- if (knownThread.state !== "running") return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("thread_not_running", `Thread ${threadId} is not currently running.`), 409);
2404
- const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, RunThreadRequestSchema);
2405
- if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
2406
- const runOptions = withRuntimePreferences(await preferences.read(knownThread.cwd ?? workspacePath), parsed.data);
2407
- const skills = runOptions.skills ?? [];
2408
- const prompt = runOptions.prompt;
2409
- const queuedInputs = queuedInputsByThreadId.get(threadId) ?? [];
2410
- const queuedInput = {
2411
- attachments: runOptions.attachments ?? [],
2412
- id: randomUUID(),
2413
- prompt,
2414
- runOptions,
2415
- skills,
2416
- workspacePath: knownThread.cwd ?? workspacePath
2417
- };
2418
- queuedInputs.push(queuedInput);
2419
- queuedInputsByThreadId.set(threadId, queuedInputs);
2420
- const thread = updateThread(threads, messagesByThreadId, threadId, {
2421
- state: "running",
2422
- lastPrompt: promptWithAttachmentReferences(prompt, runOptions.attachments ?? []),
2423
- lastError: void 0,
2424
- ...runtimeMetadataFromOptions(runOptions)
2425
- });
2426
- const response = SubmitThreadInputResponseSchema.parse({
2427
- acceptedAs: "queued",
2428
- input: queuedThreadInputSummary(queuedInput),
2429
- queueLength: queuedInputsByThreadId.get(threadId)?.length ?? 0,
2430
- thread
2405
+ return runThreadOperation(threadId, async () => {
2406
+ const knownThread = await ensureKnownThread({
2407
+ appServer,
2408
+ threadId,
2409
+ messagesByThreadId,
2410
+ threads
2411
+ });
2412
+ if (!knownThread) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
2413
+ if (!appServer) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("unsupported", "Running-thread input requires the Codex app-server."), 409);
2414
+ if (knownThread.state !== "running") return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("thread_not_running", `Thread ${threadId} is not currently running.`), 409);
2415
+ const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, RunThreadRequestSchema);
2416
+ if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
2417
+ const runOptions = withRuntimePreferences(await preferences.read(knownThread.cwd ?? workspacePath), parsed.data);
2418
+ const skills = runOptions.skills ?? [];
2419
+ const prompt = runOptions.prompt;
2420
+ const queuedInputs = queuedInputsByThreadId.get(threadId) ?? [];
2421
+ const queuedInput = {
2422
+ attachments: runOptions.attachments ?? [],
2423
+ id: randomUUID(),
2424
+ prompt,
2425
+ runOptions,
2426
+ skills,
2427
+ workspacePath: knownThread.cwd ?? workspacePath
2428
+ };
2429
+ queuedInputs.push(queuedInput);
2430
+ queuedInputsByThreadId.set(threadId, queuedInputs);
2431
+ const thread = updateThread(threads, messagesByThreadId, threadId, {
2432
+ state: "running",
2433
+ lastPrompt: promptWithAttachmentReferences(prompt, runOptions.attachments ?? []),
2434
+ lastError: void 0,
2435
+ ...runtimeMetadataFromOptions(runOptions)
2436
+ });
2437
+ const response = SubmitThreadInputResponseSchema.parse({
2438
+ acceptedAs: "queued",
2439
+ input: queuedThreadInputSummary(queuedInput),
2440
+ queueLength: queuedInputsByThreadId.get(threadId)?.length ?? 0,
2441
+ thread
2442
+ });
2443
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, response, 202);
2431
2444
  });
2432
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response, 202);
2433
2445
  });
2434
2446
  app.delete("/v1/threads/:threadId/input/:inputId", async (c) => {
2435
2447
  const threadId = c.req.param("threadId");
@@ -2650,6 +2662,7 @@ function createApp(options = {}) {
2650
2662
  messagesByThreadId,
2651
2663
  pendingApprovals,
2652
2664
  queuedInputsByThreadId,
2665
+ runThreadOperation,
2653
2666
  prompt: rawPrompt,
2654
2667
  attachments: runOptions.attachments ?? [],
2655
2668
  secureSession,
@@ -2862,6 +2875,7 @@ async function runPromptStreamed(input) {
2862
2875
  messagesByThreadId: input.messagesByThreadId,
2863
2876
  pendingApprovals: input.pendingApprovals,
2864
2877
  queuedInputsByThreadId: input.queuedInputsByThreadId,
2878
+ runThreadOperation: input.runThreadOperation,
2865
2879
  prompt: input.prompt,
2866
2880
  runOptions: input.runOptions,
2867
2881
  secureSession: input.secureSession,
@@ -3693,11 +3707,16 @@ async function runAppServerPromptStreamed(input) {
3693
3707
  case "turn/failed": {
3694
3708
  const state = terminalTurnState(notification.method, params);
3695
3709
  const terminalTurnId = firstString(params, ["turnId"]) ?? turnIdFromParams(params) ?? activeTurnId;
3696
- finishTerminalTurn({
3697
- lastError: state === "failed" ? turnErrorMessage(params) : void 0,
3698
- method: notification.method,
3699
- state,
3700
- turnId: terminalTurnId
3710
+ input.runThreadOperation(activeThreadId, async () => {
3711
+ finishTerminalTurn({
3712
+ lastError: state === "failed" ? turnErrorMessage(params) : void 0,
3713
+ method: notification.method,
3714
+ state,
3715
+ turnId: terminalTurnId
3716
+ });
3717
+ }).catch((error) => {
3718
+ cleanupNotificationHandler();
3719
+ rejectCompleted(error);
3701
3720
  });
3702
3721
  return;
3703
3722
  }
@@ -5721,10 +5740,14 @@ function isApprovalServerRequest(method) {
5721
5740
  }
5722
5741
  function observeAppServerPushNotifications(appServer, dispatcher) {
5723
5742
  const dispatchedEventIds = /* @__PURE__ */ new Set();
5724
- const dispatch = (event, eventId) => {
5725
- if (dispatchedEventIds.has(eventId)) return;
5743
+ const subagentThreadIds = /* @__PURE__ */ new Set();
5744
+ const rememberEventId = (eventId) => {
5726
5745
  if (dispatchedEventIds.size >= 1e3) dispatchedEventIds.clear();
5727
5746
  dispatchedEventIds.add(eventId);
5747
+ };
5748
+ const dispatch = (event, eventId) => {
5749
+ if (dispatchedEventIds.has(eventId)) return;
5750
+ rememberEventId(eventId);
5728
5751
  dispatcher.dispatch(event).catch((error) => {
5729
5752
  relayDebugLog("push_notification.dispatch_failed", {
5730
5753
  error: errorMessage(error),
@@ -5735,9 +5758,15 @@ function observeAppServerPushNotifications(appServer, dispatcher) {
5735
5758
  });
5736
5759
  };
5737
5760
  appServer.onNotification((notification) => {
5761
+ rememberSubagentThreadIds(notification, subagentThreadIds);
5738
5762
  const event = pushNotificationEventFromTerminalNotification(notification);
5739
5763
  if (!event) return;
5740
- dispatch(event, `${event.intent}:${event.threadId}:${event.turnId ?? ""}`);
5764
+ const eventId = `${event.intent}:${event.threadId}:${event.turnId ?? ""}`;
5765
+ if (subagentThreadIds.delete(event.threadId)) {
5766
+ rememberEventId(eventId);
5767
+ return;
5768
+ }
5769
+ dispatch(event, eventId);
5741
5770
  });
5742
5771
  appServer.onRequest((request) => {
5743
5772
  const event = pushNotificationEventFromApprovalRequest(request);
@@ -5745,6 +5774,15 @@ function observeAppServerPushNotifications(appServer, dispatcher) {
5745
5774
  dispatch(event, `${event.intent}:${event.threadId}:${event.turnId ?? ""}:${request.id}`);
5746
5775
  });
5747
5776
  }
5777
+ function rememberSubagentThreadIds(notification, subagentThreadIds) {
5778
+ const item = objectRecord(recordParams(notification)?.item);
5779
+ if (item?.type !== "collabAgentToolCall" || ![
5780
+ "spawnAgent",
5781
+ "sendInput",
5782
+ "resumeAgent"
5783
+ ].includes(String(item.tool))) return;
5784
+ for (const threadId of stringArray(item.receiverThreadIds)) subagentThreadIds.add(threadId);
5785
+ }
5748
5786
  function pushNotificationEventFromTerminalNotification(notification) {
5749
5787
  if (notification.method !== "turn/completed" && notification.method !== "turn/failed") return;
5750
5788
  const params = recordParams(notification);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-relay",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {