codex-relay 1.0.2 → 1.0.3

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 +86 -7
  2. package/package.json +1 -1
package/dist/src.js CHANGED
@@ -1217,14 +1217,24 @@ function createApp(options = {}) {
1217
1217
  return c.json(apiError("not_found", "Image attachment not found."), 404);
1218
1218
  }
1219
1219
  const fileStat = statSync(filePath);
1220
+ if (!fileStat.isFile() || fileStat.size === 0) {
1221
+ relayDebugLog("image_attachment.rejected", {
1222
+ attachmentId,
1223
+ reason: fileStat.isFile() ? "empty_file" : "not_file",
1224
+ userAgent: c.req.header("user-agent")
1225
+ });
1226
+ return c.json(apiError("not_found", "Image attachment not found."), 404);
1227
+ }
1228
+ const imageBytes = await readFile(filePath);
1220
1229
  relayDebugLog("image_attachment.served", {
1221
1230
  attachmentId,
1222
1231
  mimeType: imageMimeType(filePath),
1223
- size: fileStat.size,
1232
+ size: imageBytes.length,
1224
1233
  userAgent: c.req.header("user-agent")
1225
1234
  });
1226
- return new Response(await readFile(filePath), { headers: {
1235
+ return new Response(imageBytes, { headers: {
1227
1236
  "cache-control": "private, max-age=31536000, immutable",
1237
+ "content-length": String(imageBytes.length),
1228
1238
  "content-type": imageMimeType(filePath)
1229
1239
  } });
1230
1240
  });
@@ -2517,7 +2527,11 @@ async function runAppServerPromptStreamed(input) {
2517
2527
  case "item/completed": {
2518
2528
  const item = params?.item;
2519
2529
  if (!item || typeof item !== "object") return;
2520
- if (isDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, item, userMessage.id, prompt)) return;
2530
+ const canonicalUserMessage = replaceDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, firstString(params, ["turnId"]) ?? activeTurnId, item, userMessage.id, prompt);
2531
+ if (canonicalUserMessage) {
2532
+ userMessage = canonicalUserMessage;
2533
+ return;
2534
+ }
2521
2535
  const turnId = firstString(params, ["turnId"]) ?? activeTurnId;
2522
2536
  const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, turnId, item);
2523
2537
  if (!message) return;
@@ -2750,7 +2764,11 @@ async function runAppServerPromptStreamed(input) {
2750
2764
  debugStream("start turn complete", activeThreadId, activeTurnId);
2751
2765
  let streamedReturnedItem = false;
2752
2766
  for (const item of turn.items) {
2753
- if (isDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, item, userMessage.id, displayPrompt)) continue;
2767
+ const canonicalUserMessage = replaceDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, activeTurnId, item, userMessage.id, displayPrompt);
2768
+ if (canonicalUserMessage) {
2769
+ userMessage = canonicalUserMessage;
2770
+ continue;
2771
+ }
2754
2772
  const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, activeTurnId, item);
2755
2773
  if (!message) continue;
2756
2774
  streamedReturnedItem = true;
@@ -3062,6 +3080,12 @@ function upsertAppServerItemMessage(messagesByThreadId, threadId, turnId, item)
3062
3080
  turnId: message.turnId
3063
3081
  });
3064
3082
  }
3083
+ function replaceDuplicateInitialUserMessage(messagesByThreadId, threadId, turnId, item, localMessageId, prompt) {
3084
+ if (!isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, localMessageId, prompt)) return;
3085
+ const message = mapAppServerItem(threadId, appServerTurnShell(turnId), item);
3086
+ if (!message) return;
3087
+ return replaceMessage(messagesByThreadId, threadId, localMessageId, messageWithReplacementDetail(message, localMessageId));
3088
+ }
3065
3089
  function isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, localMessageId, prompt) {
3066
3090
  if (item.type !== "userMessage" || !("content" in item) || !Array.isArray(item.content)) return false;
3067
3091
  const localMessage = messagesByThreadId.get(threadId)?.find((message) => message.id === localMessageId);
@@ -3069,6 +3093,15 @@ function isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, local
3069
3093
  const normalizedPrompt = stripPromptSkillMentions(prompt, skills);
3070
3094
  return stripPromptSkillMentions(localMessage?.content ?? "", skills) === normalizedPrompt && stripPromptSkillMentions(appServerUserMessageText(item), skills) === normalizedPrompt;
3071
3095
  }
3096
+ function messageWithReplacementDetail(message, replacesMessageId) {
3097
+ return ChatMessageSchema.parse({
3098
+ ...message,
3099
+ details: {
3100
+ ...message.details,
3101
+ replacesMessageId
3102
+ }
3103
+ });
3104
+ }
3072
3105
  function appServerUserMessageText(item) {
3073
3106
  const skills = appServerUserMessageSkills(item);
3074
3107
  return promptMarkdownWithSkills(promptWithAppServerImageReferences(item.content.map((content) => {
@@ -3102,6 +3135,7 @@ function appServerUserMessageDetails(item) {
3102
3135
  async function saveUploadedImageAttachment(file) {
3103
3136
  if (!file.type.startsWith("image/")) throw new Error(`Unsupported attachment type: ${file.type || "unknown"}`);
3104
3137
  if (file.size > IMAGE_ATTACHMENT_MAX_BYTES) throw new Error(`Image ${file.name || "attachment"} is too large.`);
3138
+ if (file.size === 0) throw new Error(`Image ${file.name || "attachment"} is empty.`);
3105
3139
  const attachmentId = `${Date.now()}-${randomUUID()}${imageExtension(file.name, file.type)}`;
3106
3140
  const filePath = resolve(imageAttachmentDirectory, attachmentId);
3107
3141
  await mkdir(imageAttachmentDirectory, { recursive: true });
@@ -3152,7 +3186,7 @@ function materializeLocalImageFile(path, name = basename(path)) {
3152
3186
  const filePath = localImageFilePath(path);
3153
3187
  if (!filePath || !existsSync(filePath)) return;
3154
3188
  const fileStat = statSync(filePath);
3155
- if (!fileStat.isFile() || fileStat.size > IMAGE_ATTACHMENT_MAX_BYTES) return;
3189
+ if (!fileStat.isFile() || fileStat.size === 0 || fileStat.size > IMAGE_ATTACHMENT_MAX_BYTES) return;
3156
3190
  const buffer = readFileSync(filePath);
3157
3191
  const mimeType = imageMimeType(filePath);
3158
3192
  const attachmentId = `${createHash("sha256").update(buffer).digest("hex").slice(0, 24)}${imageExtension(name, mimeType)}`;
@@ -3268,6 +3302,17 @@ function updateMessage(messagesByThreadId, threadId, messageId, update) {
3268
3302
  messages[index] = next;
3269
3303
  return next;
3270
3304
  }
3305
+ function replaceMessage(messagesByThreadId, threadId, messageId, replacement) {
3306
+ const messages = messagesByThreadId.get(threadId) ?? [];
3307
+ const index = messages.findIndex((message) => message.id === messageId);
3308
+ if (index === -1) throw new Error(`Unknown message: ${messageId}`);
3309
+ const next = ChatMessageSchema.parse({
3310
+ ...replacement,
3311
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3312
+ });
3313
+ messages[index] = next;
3314
+ return next;
3315
+ }
3271
3316
  function replaceLocalThreadId(threads, messagesByThreadId, liveThreads, currentThreadId, sdkThreadId) {
3272
3317
  if (!sdkThreadId || sdkThreadId === currentThreadId) return currentThreadId;
3273
3318
  const metadata = threads.get(currentThreadId);
@@ -3628,26 +3673,58 @@ function mergeThreadMessagePages(incomingMessages, cachedMessages) {
3628
3673
  return dedupeThreadMessages(Array.from(byId.values()));
3629
3674
  }
3630
3675
  function dedupeThreadMessages(messages) {
3676
+ const byCrossSourceKey = /* @__PURE__ */ new Map();
3631
3677
  const byImageKey = /* @__PURE__ */ new Map();
3632
3678
  const deduped = [];
3633
3679
  const sortedMessages = [...messages].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
3634
3680
  for (const message of sortedMessages) {
3681
+ const crossSourceKey = crossSourceMessageKey(message);
3682
+ const crossSourceIndex = crossSourceKey ? byCrossSourceKey.get(crossSourceKey) : void 0;
3683
+ if (crossSourceIndex !== void 0) {
3684
+ const existingMessage = deduped[crossSourceIndex];
3685
+ if (existingMessage && shouldPreferDuplicateThreadMessage(message, existingMessage)) deduped[crossSourceIndex] = message;
3686
+ continue;
3687
+ }
3688
+ const previous = deduped[deduped.length - 1];
3689
+ if (previous && isDuplicateCrossSourceMessage(previous, message)) {
3690
+ if (shouldPreferDuplicateThreadMessage(message, previous)) deduped[deduped.length - 1] = message;
3691
+ continue;
3692
+ }
3635
3693
  const imageKey = userImageMessageKey(message);
3636
3694
  if (!imageKey) {
3695
+ if (crossSourceKey) byCrossSourceKey.set(crossSourceKey, deduped.length);
3637
3696
  deduped.push(message);
3638
3697
  continue;
3639
3698
  }
3640
3699
  const existingIndex = byImageKey.get(imageKey);
3641
3700
  if (existingIndex === void 0) {
3642
3701
  byImageKey.set(imageKey, deduped.length);
3702
+ if (crossSourceKey) byCrossSourceKey.set(crossSourceKey, deduped.length);
3643
3703
  deduped.push(message);
3644
3704
  continue;
3645
3705
  }
3646
3706
  const existingMessage = deduped[existingIndex];
3647
- if (existingMessage && shouldPreferDuplicateImageMessage(message, existingMessage)) deduped[existingIndex] = message;
3707
+ if (existingMessage && shouldPreferDuplicateThreadMessage(message, existingMessage)) deduped[existingIndex] = message;
3648
3708
  }
3649
3709
  return deduped;
3650
3710
  }
3711
+ function crossSourceMessageKey(message) {
3712
+ if (!isSyntheticHistoryMessageId(message.id)) return;
3713
+ if (message.role !== "user" && message.role !== "assistant") return;
3714
+ return [
3715
+ message.threadId,
3716
+ message.createdAt.slice(0, 19),
3717
+ message.role,
3718
+ message.kind,
3719
+ message.content
3720
+ ].join("\n");
3721
+ }
3722
+ function isDuplicateCrossSourceMessage(previous, next) {
3723
+ return previous.id !== next.id && (isSyntheticHistoryMessageId(previous.id) || isSyntheticHistoryMessageId(next.id)) && previous.threadId === next.threadId && previous.role === next.role && previous.kind === next.kind && previous.content === next.content;
3724
+ }
3725
+ function isSyntheticHistoryMessageId(id) {
3726
+ return id.startsWith("msg-") || id.startsWith("rollout:");
3727
+ }
3651
3728
  function userImageMessageKey(message) {
3652
3729
  if (message.role !== "user") return;
3653
3730
  const imageUris = imageAttachmentUris(message);
@@ -3665,9 +3742,11 @@ function imageAttachmentUris(message) {
3665
3742
  return ["url" in attachment ? attachment.url : void 0, "path" in attachment ? attachment.path : void 0].filter((value) => typeof value === "string" && value.length > 0);
3666
3743
  });
3667
3744
  }
3668
- function shouldPreferDuplicateImageMessage(candidate, existing) {
3745
+ function shouldPreferDuplicateThreadMessage(candidate, existing) {
3669
3746
  if (existing.id.startsWith("rollout") && !candidate.id.startsWith("rollout")) return true;
3670
3747
  if (!existing.id.startsWith("rollout") && candidate.id.startsWith("rollout")) return false;
3748
+ if (!existing.turnId && candidate.turnId) return true;
3749
+ if (existing.turnId && !candidate.turnId) return false;
3671
3750
  return candidate.content.length < existing.content.length;
3672
3751
  }
3673
3752
  function rememberRolloutThreadMessages(threads, thread, messages, messageCountLowerBound = messages.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-relay",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
5
5
  "repository": {
6
6
  "type": "git",