codex-relay 1.0.7 → 1.1.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 (3) hide show
  1. package/README.md +10 -0
  2. package/dist/src.js +65 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Codex Relay runs a local bridge server for the Codex Relay mobile app. Keep Codex on your computer, then use your phone to pair with that local session, send prompts, watch streamed output, and respond to approval requests.
4
4
 
5
+ Codex Relay is an independent project. It is not affiliated with, endorsed by, or sponsored by OpenAI or the OpenAI Codex team.
6
+
5
7
  ## Requirements
6
8
 
7
9
  - Node.js 22.14 or newer
@@ -145,3 +147,11 @@ kill -TERM <pid>
145
147
  ```
146
148
 
147
149
  If the mobile app cannot connect, confirm that the phone can reach the printed `Mobile:` URL and that the chosen port is not blocked by a firewall.
150
+
151
+ Connection checklist:
152
+
153
+ - Are the phone and computer on the same Wi-Fi or LAN?
154
+ - If keeping the same network is difficult, are both devices connected through Tailscale or another reachable private network?
155
+ - Can the phone open the exact `Mobile:` URL printed by the relay?
156
+ - Does the computer firewall allow inbound traffic on the relay port, usually `8787`?
157
+ - If the printed URL is not reachable, did you set `CODEX_RELAY_PUBLIC_URL` to a reachable LAN, Tailscale, or tunnel URL?
package/dist/src.js CHANGED
@@ -2229,16 +2229,17 @@ async function runPromptStreamed(input) {
2229
2229
  if (kind === "error") throw new Error(text ?? "Codex run failed.");
2230
2230
  if (!text) continue;
2231
2231
  if (kind === "assistant") {
2232
- assistantMessage = appendMessageDelta(input.messagesByThreadId, activeThreadId, assistantMessage.id, text);
2232
+ const assistantPatch = appendMessageDelta(input.messagesByThreadId, activeThreadId, assistantMessage.id, text);
2233
+ assistantMessage = assistantPatch.message;
2233
2234
  updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
2234
2235
  state: "running",
2235
2236
  lastResult: assistantMessage.content
2236
2237
  });
2237
- sendSse(input.controller, input.encoder, input.secureSession, {
2238
+ if (assistantPatch.delta) sendSse(input.controller, input.encoder, input.secureSession, {
2238
2239
  type: "thread.message.delta",
2239
2240
  threadId: activeThreadId,
2240
2241
  messageId: assistantMessage.id,
2241
- delta: text
2242
+ delta: assistantPatch.delta
2242
2243
  });
2243
2244
  } else {
2244
2245
  const structured = structuredStreamMessage(kind, event, text);
@@ -2494,16 +2495,17 @@ async function streamRunningAppServerThread(input) {
2494
2495
  turnId: firstString(params, ["turnId"]) ?? activeTurnId
2495
2496
  });
2496
2497
  assistantMessageId = itemId;
2497
- const message = appendMessageDelta(input.messagesByThreadId, input.threadId, itemId, delta);
2498
+ const patch = appendMessageDelta(input.messagesByThreadId, input.threadId, itemId, delta);
2499
+ const message = patch.message;
2498
2500
  updateThread(input.threads, input.messagesByThreadId, input.threadId, {
2499
2501
  state: "running",
2500
2502
  lastResult: message.content
2501
2503
  });
2502
- sendSse(input.controller, input.encoder, input.secureSession, {
2504
+ if (patch.delta) sendSse(input.controller, input.encoder, input.secureSession, {
2503
2505
  type: "thread.message.delta",
2504
2506
  threadId: input.threadId,
2505
2507
  messageId: itemId,
2506
- delta
2508
+ delta: patch.delta
2507
2509
  });
2508
2510
  return;
2509
2511
  }
@@ -2705,7 +2707,7 @@ async function runAppServerPromptStreamed(input) {
2705
2707
  case "item/completed": {
2706
2708
  const item = params?.item;
2707
2709
  if (!item || typeof item !== "object") return;
2708
- const canonicalUserMessage = replaceDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, firstString(params, ["turnId"]) ?? activeTurnId, item, userMessage.id, prompt);
2710
+ const canonicalUserMessage = replaceDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, firstString(params, ["turnId"]) ?? activeTurnId, item, userMessage.id, displayPrompt);
2709
2711
  if (canonicalUserMessage) {
2710
2712
  userMessage = canonicalUserMessage;
2711
2713
  return;
@@ -2737,17 +2739,18 @@ async function runAppServerPromptStreamed(input) {
2737
2739
  turnId: firstString(params, ["turnId"]) ?? activeTurnId
2738
2740
  });
2739
2741
  assistantMessageId = itemId;
2740
- const message = appendMessageDelta(input.messagesByThreadId, activeThreadId, itemId, delta);
2742
+ const patch = appendMessageDelta(input.messagesByThreadId, activeThreadId, itemId, delta);
2743
+ const message = patch.message;
2741
2744
  producedTurnOutput = true;
2742
2745
  updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
2743
2746
  state: "running",
2744
2747
  lastResult: message.content
2745
2748
  });
2746
- sendSse(input.controller, input.encoder, input.secureSession, {
2749
+ if (patch.delta) sendSse(input.controller, input.encoder, input.secureSession, {
2747
2750
  type: "thread.message.delta",
2748
2751
  threadId: activeThreadId,
2749
2752
  messageId: itemId,
2750
- delta
2753
+ delta: patch.delta
2751
2754
  });
2752
2755
  return;
2753
2756
  }
@@ -3269,8 +3272,9 @@ function isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, local
3269
3272
  if (item.type !== "userMessage" || !("content" in item) || !Array.isArray(item.content)) return false;
3270
3273
  const localMessage = messagesByThreadId.get(threadId)?.find((message) => message.id === localMessageId);
3271
3274
  const skills = appServerUserMessageSkills(item);
3272
- const normalizedPrompt = stripPromptSkillMentions(prompt, skills);
3273
- return stripPromptSkillMentions(localMessage?.content ?? "", skills) === normalizedPrompt && stripPromptSkillMentions(appServerUserMessageText(item), skills) === normalizedPrompt;
3275
+ const normalizeContent = (content) => stripPromptSkillMentions(normalizeImageMessageContent(content), skills);
3276
+ const normalizedPrompt = normalizeContent(prompt);
3277
+ return normalizeContent(localMessage?.content ?? "") === normalizedPrompt && normalizeContent(appServerUserMessageText(item)) === normalizedPrompt;
3274
3278
  }
3275
3279
  function messageWithReplacementDetail(message, replacesMessageId) {
3276
3280
  return ChatMessageSchema.parse({
@@ -3526,10 +3530,18 @@ function isAppServerSkillInput(input) {
3526
3530
  }
3527
3531
  function appendMessageDelta(messagesByThreadId, threadId, messageId, delta) {
3528
3532
  const existing = messagesByThreadId.get(threadId)?.find((message) => message.id === messageId);
3529
- return updateMessage(messagesByThreadId, threadId, messageId, {
3530
- content: `${existing?.content ?? ""}${delta}`,
3531
- state: "streaming"
3532
- });
3533
+ const normalizedDelta = normalizeStreamDelta(existing?.content ?? "", delta);
3534
+ return {
3535
+ delta: normalizedDelta,
3536
+ message: updateMessage(messagesByThreadId, threadId, messageId, {
3537
+ content: `${existing?.content ?? ""}${normalizedDelta}`,
3538
+ state: "streaming"
3539
+ })
3540
+ };
3541
+ }
3542
+ function normalizeStreamDelta(existingContent, incomingDelta) {
3543
+ if (!existingContent || !incomingDelta.startsWith(existingContent)) return incomingDelta;
3544
+ return incomingDelta.slice(existingContent.length);
3533
3545
  }
3534
3546
  function markApprovalMessageResolved(messagesByThreadId, threadId, messageId, decision) {
3535
3547
  const message = (messagesByThreadId.get(threadId) ?? []).find((candidate) => candidate.id === messageId);
@@ -4092,6 +4104,7 @@ function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePat
4092
4104
  };
4093
4105
  const collected = [];
4094
4106
  const applyPatchInputs = /* @__PURE__ */ new Map();
4107
+ const handledApplyPatchCallIds = /* @__PURE__ */ new Set();
4095
4108
  const pendingApplyPatchChanges = [];
4096
4109
  const lines = readFileSync(rolloutPath, "utf8").split("\n");
4097
4110
  for (let index = 0; index < lines.length; index += 1) {
@@ -4102,7 +4115,7 @@ function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePat
4102
4115
  try {
4103
4116
  const record = JSON.parse(line);
4104
4117
  rememberRolloutApplyPatchInput(record, applyPatchInputs);
4105
- collectRolloutApplyPatchOutput(record, workspacePath, applyPatchInputs, pendingApplyPatchChanges);
4118
+ collectRolloutApplyPatchOutput(record, workspacePath, applyPatchInputs, handledApplyPatchCallIds, pendingApplyPatchChanges);
4106
4119
  if (isRolloutTaskComplete(record) && pendingApplyPatchChanges.length > 0) {
4107
4120
  collected.push(rolloutApplyPatchSummaryMessage(threadId, record, `rollout:${lineNumber}:apply_patch`, pendingApplyPatchChanges));
4108
4121
  pendingApplyPatchChanges.length = 0;
@@ -4111,6 +4124,11 @@ function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePat
4111
4124
  const message = rolloutRecordMessage(threadId, record, `rollout:${lineNumber}`, workspacePath);
4112
4125
  if (!message) continue;
4113
4126
  collected.push(message);
4127
+ const patchApplyEndCallId = rolloutPatchApplyEndCallId(record);
4128
+ if (patchApplyEndCallId) {
4129
+ handledApplyPatchCallIds.add(patchApplyEndCallId);
4130
+ pendingApplyPatchChanges.splice(0, pendingApplyPatchChanges.length, ...pendingApplyPatchChanges.filter((change) => change.callId !== patchApplyEndCallId));
4131
+ }
4114
4132
  } catch {}
4115
4133
  }
4116
4134
  return {
@@ -4172,7 +4190,7 @@ function rolloutRecordMessage(threadId, record, messageKey, workspacePath = defa
4172
4190
  if (record.type === "event_msg" && payload.type === "patch_apply_end") {
4173
4191
  const changes = rolloutPatchApplyChanges(payload.changes, workspacePath);
4174
4192
  if (changes.length === 0) return;
4175
- const patchPreview = largeTextPreview(changes.map((change) => change.patch).filter((patch) => Boolean(patch)).join("\n"));
4193
+ const patchPreview = largeTextPreview(rolloutPatchPreview(changes));
4176
4194
  return ChatMessageSchema.parse({
4177
4195
  id: `${messageKey}:patch:${firstString(payload, ["call_id"]) ?? ""}`,
4178
4196
  threadId,
@@ -4182,7 +4200,7 @@ function rolloutRecordMessage(threadId, record, messageKey, workspacePath = defa
4182
4200
  createdAt: timestamp,
4183
4201
  state: "completed",
4184
4202
  details: {
4185
- changes: changes.map(({ patch: _patch, ...change }) => change),
4203
+ changes: changes.map(publicRolloutPatchChange),
4186
4204
  patch: patchPreview?.text,
4187
4205
  patchOriginalLength: patchPreview?.originalLength,
4188
4206
  patchTruncated: patchPreview?.truncated
@@ -4250,15 +4268,16 @@ function rememberRolloutApplyPatchInput(record, applyPatchInputs) {
4250
4268
  const input = firstString(payload, ["input"]);
4251
4269
  if (callId && input) applyPatchInputs.set(callId, input);
4252
4270
  }
4253
- function collectRolloutApplyPatchOutput(record, workspacePath, applyPatchInputs, pendingApplyPatchChanges) {
4271
+ function collectRolloutApplyPatchOutput(record, workspacePath, applyPatchInputs, handledApplyPatchCallIds, pendingApplyPatchChanges) {
4254
4272
  const payload = record.payload;
4255
- if (record.type !== "response_item" || payload?.type !== "custom_tool_call_output" || !firstString(payload, ["call_id"])) return;
4273
+ const callId = firstString(payload, ["call_id"]);
4274
+ if (record.type !== "response_item" || payload?.type !== "custom_tool_call_output" || !callId || handledApplyPatchCallIds.has(callId)) return;
4256
4275
  const changes = rolloutApplyPatchOutputChanges(rolloutCustomToolOutputText(payload), workspacePath);
4257
4276
  if (changes.length === 0) return;
4258
- const callId = firstString(payload, ["call_id"]);
4259
4277
  const patch = callId ? applyPatchInputs.get(callId) : void 0;
4260
4278
  for (const change of changes) pendingApplyPatchChanges.push({
4261
4279
  ...change,
4280
+ callId,
4262
4281
  patch
4263
4282
  });
4264
4283
  }
@@ -4274,13 +4293,36 @@ function rolloutApplyPatchSummaryMessage(threadId, record, messageKey, pendingAp
4274
4293
  createdAt: timestamp,
4275
4294
  state: "completed",
4276
4295
  details: {
4277
- changes: pendingApplyPatchChanges.map(({ patch: _patch, ...change }) => change),
4296
+ changes: pendingApplyPatchChanges.map(publicRolloutPatchChange),
4278
4297
  patch: patchPreview?.text,
4279
4298
  patchOriginalLength: patchPreview?.originalLength,
4280
4299
  patchTruncated: patchPreview?.truncated
4281
4300
  }
4282
4301
  });
4283
4302
  }
4303
+ function rolloutPatchApplyEndCallId(record) {
4304
+ if (record.type !== "event_msg" || record.payload?.type !== "patch_apply_end") return;
4305
+ return firstString(record.payload, ["call_id"]);
4306
+ }
4307
+ function publicRolloutPatchChange(change) {
4308
+ const { callId: _callId, patch: _patch, ...publicChange } = change;
4309
+ return publicChange;
4310
+ }
4311
+ function rolloutPatchPreview(changes) {
4312
+ return changes.flatMap((change) => {
4313
+ if (!change.patch) return [];
4314
+ if (hasPatchFileHeader(change.patch)) return [change.patch];
4315
+ return [`*** ${patchHeaderChangeKind(change.kind)} File: ${change.path}\n${change.patch}`];
4316
+ }).join("\n");
4317
+ }
4318
+ function hasPatchFileHeader(patch) {
4319
+ return /^(?:diff --git |\*\*\* (?:Add|Update|Delete) File: )/m.test(patch);
4320
+ }
4321
+ function patchHeaderChangeKind(kind) {
4322
+ if (kind === "added") return "Add";
4323
+ if (kind === "deleted") return "Delete";
4324
+ return "Update";
4325
+ }
4284
4326
  function rolloutCustomToolOutputText(payload) {
4285
4327
  const rawOutput = firstString(payload, ["output"]);
4286
4328
  if (!rawOutput) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-relay",
3
- "version": "1.0.7",
3
+ "version": "1.1.1",
4
4
  "description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {