Skip to content

feat: include email attachments in agent.generate + tool calls#250

Merged
sweetmantech merged 7 commits intotestfrom
sweetmantech/myc-4368-inbound-email-include-attachments-in-agentgenerate-tool
Feb 28, 2026
Merged

feat: include email attachments in agent.generate + tool calls#250
sweetmantech merged 7 commits intotestfrom
sweetmantech/myc-4368-inbound-email-include-attachments-in-agentgenerate-tool

Conversation

@sweetmantech
Copy link
Contributor

@sweetmantech sweetmantech commented Feb 28, 2026

Summary

  • Fetch attachment download URLs from Resend's receiving attachments API when processing inbound emails
  • Append attachment download URLs to email text so sandbox/tools can download any file type
  • Inject image attachment parts into the last user message so the LLM can visually process images via agent.generate()
  • Add attachments field to ChatRequestBody to pass attachment metadata through the pipeline

Test plan

  • Unit tests for getEmailAttachments (4 tests) — Resend API mapping, empty/null responses, filename defaults
  • Unit tests for formatAttachmentsText (3 tests) — empty array, single, multiple attachments
  • Unit tests for generateEmailResponse (5 tests) — no attachments, image parts injection, non-image skipping, multi-message conversations
  • Updated validateNewEmailMemory tests (3 new tests) — attachment fetching, URL appending, empty attachments
  • All 157 test files pass (1257 tests), zero lint errors
  • Manual test: send email with SVG attachment, verify agent sees image and sandbox can download via URL

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Incoming email attachments are now fetched and summarized (filename, content type, download link) and appended to the processed message content.
    • Attachment summaries are presented as a clear "Attached files:" list to improve context when viewing or replying to inbound emails.

Fetch attachment download URLs from Resend, append them to email text
so sandbox/tools can download files, and inject image parts into agent
messages so the LLM can visually process images.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Contributor

vercel bot commented Feb 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Feb 28, 2026 5:48pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 34 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 8b958ed and d5c0df6.

⛔ Files ignored due to path filters (3)
  • lib/emails/inbound/__tests__/formatAttachmentsText.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/inbound/__tests__/getEmailAttachments.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/emails/inbound/formatAttachmentsText.ts
  • lib/emails/inbound/getEmailAttachments.ts
📝 Walkthrough

Walkthrough

Adds email attachment support: new Resend-based attachment fetcher, a formatter that renders attachments as text, integration into inbound email validation to append formatted attachment info, and extra logging plus a SANDBOX prompt note expansion.

Changes

Cohort / File(s) Summary
Attachment Retrieval
lib/emails/inbound/getEmailAttachments.ts
New module exporting EmailAttachment and getEmailAttachments(emailId); uses Resend client to list receiving attachments, normalizes fields, defaults missing filename to "attachment".
Attachment Formatting
lib/emails/inbound/formatAttachmentsText.ts
New module exporting formatAttachmentsText(attachments); returns empty string for no attachments or a text block starting with two newlines and header Attached files: followed by lines - filename (contentType): downloadUrl.
Email Validation Integration
lib/emails/inbound/validateNewEmailMemory.ts
Now imports and calls getEmailAttachments, appends formatAttachmentsText(attachments) to the constructed email text used for chat requests.
Logging / Instrumentation
lib/emails/inbound/respondToInboundEmail.ts
Added emailId local variable and multiple console logs around processing stages (start, validation, CC check, generation, send) referencing emailId.
Prompt Note Update
lib/chat/tools/createPromptSandboxStreamingTool.ts
Expanded SANDBOX_PROMPT_NOTE to instruct downloading attached file URLs (via curl), warn about 1-hour expiry, and advise not to store download URLs; keeps original commit guidance.

Sequence Diagram

sequenceDiagram
    participant Inbound as Inbound Service
    participant Resend as Resend API
    participant Formatter as Format Module
    participant Chat as Chat Service

    Inbound->>Resend: getEmailAttachments(emailId)
    Resend-->>Inbound: [EmailAttachment...]
    Inbound->>Formatter: formatAttachmentsText(attachments)
    Formatter-->>Inbound: "Attached files:\n- name (type): url"
    Inbound->>Chat: append formatted text to email body and send chatRequestBody
    Chat-->>Inbound: response/result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

📎 New attachments come to light,
URLs gleam for just an hour's flight,
Fetched and formatted, neat and true,
Pinned to memory, shown to you —
Logs whisper progress through the night.

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning New modules follow SOLID principles well, but respondToInboundEmail.ts violates SRP with scattered logging, DRY with repeated patterns, and clean code with raw PII logging and premature persistence before error checking. Extract logging into a utility function with PII redaction, move saveChatCompletion after error check on line 72, and implement email masking for all logging statements to restore proper separation of concerns and data consistency.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sweetmantech/myc-4368-inbound-email-include-attachments-in-agentgenerate-tool

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
lib/emails/inbound/validateNewEmailMemory.ts (1)

34-36: Consider graceful handling if attachment fetch fails.

If getEmailAttachments throws (network error, API failure), the entire email processing will fail. Depending on business requirements, you may want to continue processing the email without attachments rather than failing completely:

🛡️ Proposed graceful fallback
-  const attachments = await getEmailAttachments(emailId);
+  let attachments: Awaited<ReturnType<typeof getEmailAttachments>> = [];
+  try {
+    attachments = await getEmailAttachments(emailId);
+  } catch (error) {
+    console.error(`Failed to fetch attachments for email ${emailId}, continuing without:`, error);
+  }
   const emailText =
     trimRepliedContext(emailContent.html || "") + formatAttachmentsText(attachments);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/emails/inbound/validateNewEmailMemory.ts` around lines 34 - 36, Wrap the
call to getEmailAttachments(emailId) in a try/catch inside
validateNewEmailMemory (or the function containing this diff) so that if
getEmailAttachments throws (network/API error) you log the error and proceed
with attachments = [] instead of letting the entire flow fail; then build
emailText using trimRepliedContext(...) + formatAttachmentsText(attachments) as
before. Optionally emit a telemetry/metric or processLogger.warn mentioning the
emailId and the attachment fetch failure for visibility.
lib/emails/inbound/generateEmailResponse.ts (1)

32-54: Consider extracting attachment injection logic for better testability.

The image attachment injection logic (lines 32-54) could be extracted into a dedicated helper function like injectImageAttachments(messages, attachments). This would improve testability and align with SRP, allowing this function to focus on orchestration while the helper handles the message transformation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/emails/inbound/generateEmailResponse.ts` around lines 32 - 54, Extract
the image-attachment injection block into a helper function named
injectImageAttachments(messages, attachments) that takes the existing messages
array and attachments list, filters image attachments (imageAttachments), finds
the last user message index (lastUserIdx), builds the parts array (preserving
the original textContent and pushing image entries with new URL(att.downloadUrl)
and mimeType att.contentType), and replaces messages[lastUserIdx].content with
the parts cast to ModelMessage["content"]; then replace the inline block in
generateEmailResponse.ts with a call to injectImageAttachments(messages,
attachments) and export the helper (or keep it top-level) so it can be unit
tested while maintaining identical behavior and types.
lib/emails/inbound/getEmailAttachments.ts (1)

24-29: Missing validation for required response fields.

The mapping assumes att.content_type and att.download_url are always present. If the API returns unexpected data, this could result in undefined values in the returned objects.

🛡️ Proposed defensive filtering
-  return data.data.map(att => ({
-    id: att.id,
-    filename: att.filename || "attachment",
-    contentType: att.content_type,
-    downloadUrl: att.download_url,
-  }));
+  return data.data
+    .filter(att => att.id && att.content_type && att.download_url)
+    .map(att => ({
+      id: att.id,
+      filename: att.filename || "attachment",
+      contentType: att.content_type,
+      downloadUrl: att.download_url,
+    }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/emails/inbound/getEmailAttachments.ts` around lines 24 - 29, The mapping
in getEmailAttachments currently assumes att.content_type and att.download_url
exist, which can yield undefined; update the logic that processes data.data (the
att items) to defensively filter out or skip any att entries where
att.content_type or att.download_url are falsy before mapping, and/or supply a
safe default only for filename (keep filename fallback "attachment"), ensure the
returned objects (contentType and downloadUrl) are never undefined by either
excluding those attachments or throwing/logging a clear warning inside the same
function (referencing the att.id, att.content_type, att.download_url and the
produced keys contentType/downloadUrl to locate the change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/emails/inbound/generateEmailResponse.ts`:
- Around line 39-51: The code in generateEmailResponse.ts overwrites existing
multipart message content by coercing msg.content to an empty string; instead,
detect whether msg.content is already an array and preserve/merge it: if
msg.content is an array use that as the initial parts (or concat new image
parts) else create a text part from the string as currently done; update the
logic around messages[lastUserIdx] and the parts construction (referencing msg,
messages, lastUserIdx, parts, imageAttachments, and ModelMessage["content"]) so
existing multipart content is retained and new image parts are appended.
- Line 47: The code constructs an image URL with image: new URL(att.downloadUrl)
which can throw on malformed URLs; update the logic in generateEmailResponse (or
the surrounding function where att is processed) to defensively validate/parse
att.downloadUrl: attempt to create the URL inside a try/catch (or pre-validate
with a safe URL-check helper), and on failure set the image field to
undefined/null or skip attaching the image and log the bad URL (include
att.downloadUrl and context) instead of allowing the exception to propagate.
- Around line 41-51: The current manual parts array and cast around
messages[lastUserIdx] use a loose shape and then force-cast to
ModelMessage["content"]; instead import the ai library discriminated types (e.g.
ModelMessage plus TextPart, ImagePart, FilePart) and type the parts as
Array<TextPart | ImagePart | FilePart>, then build each part using the exact
property names required by those types (use the ImagePart shape for image
attachments and TextPart for textContent) and assign directly to
messages[lastUserIdx].content without any cast so the compiler validates the
shapes (reference symbols: ModelMessage, TextPart, ImagePart, the parts
variable, and messages[lastUserIdx] / msg).

In `@lib/emails/inbound/getEmailAttachments.ts`:
- Around line 18-22: The call to resend.emails.receiving.attachments.list inside
getEmailAttachments is missing error handling; wrap the API call in a try/catch
around getResendClient() and the list call (in function getEmailAttachments) and
handle failures by either returning an empty array on recoverable errors or
re-throwing a new Error with contextual information (including emailId and the
original error) so callers can distinguish Resend failures; ensure the catch
uses the resend client reference (getResendClient) and the method name
(resend.emails.receiving.attachments.list) in the logged/thrown message for
easier debugging.

---

Nitpick comments:
In `@lib/emails/inbound/generateEmailResponse.ts`:
- Around line 32-54: Extract the image-attachment injection block into a helper
function named injectImageAttachments(messages, attachments) that takes the
existing messages array and attachments list, filters image attachments
(imageAttachments), finds the last user message index (lastUserIdx), builds the
parts array (preserving the original textContent and pushing image entries with
new URL(att.downloadUrl) and mimeType att.contentType), and replaces
messages[lastUserIdx].content with the parts cast to ModelMessage["content"];
then replace the inline block in generateEmailResponse.ts with a call to
injectImageAttachments(messages, attachments) and export the helper (or keep it
top-level) so it can be unit tested while maintaining identical behavior and
types.

In `@lib/emails/inbound/getEmailAttachments.ts`:
- Around line 24-29: The mapping in getEmailAttachments currently assumes
att.content_type and att.download_url exist, which can yield undefined; update
the logic that processes data.data (the att items) to defensively filter out or
skip any att entries where att.content_type or att.download_url are falsy before
mapping, and/or supply a safe default only for filename (keep filename fallback
"attachment"), ensure the returned objects (contentType and downloadUrl) are
never undefined by either excluding those attachments or throwing/logging a
clear warning inside the same function (referencing the att.id,
att.content_type, att.download_url and the produced keys contentType/downloadUrl
to locate the change).

In `@lib/emails/inbound/validateNewEmailMemory.ts`:
- Around line 34-36: Wrap the call to getEmailAttachments(emailId) in a
try/catch inside validateNewEmailMemory (or the function containing this diff)
so that if getEmailAttachments throws (network/API error) you log the error and
proceed with attachments = [] instead of letting the entire flow fail; then
build emailText using trimRepliedContext(...) +
formatAttachmentsText(attachments) as before. Optionally emit a telemetry/metric
or processLogger.warn mentioning the emailId and the attachment fetch failure
for visibility.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a81063 and 4ee7ae7.

⛔ Files ignored due to path filters (4)
  • lib/emails/inbound/__tests__/formatAttachmentsText.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/inbound/__tests__/generateEmailResponse.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/inbound/__tests__/getEmailAttachments.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (5)
  • lib/chat/validateChatRequest.ts
  • lib/emails/inbound/formatAttachmentsText.ts
  • lib/emails/inbound/generateEmailResponse.ts
  • lib/emails/inbound/getEmailAttachments.ts
  • lib/emails/inbound/validateNewEmailMemory.ts

Comment on lines +39 to +51
const msg = messages[lastUserIdx];
const textContent = typeof msg.content === "string" ? msg.content : "";
const parts: Array<{ type: string; text?: string; image?: URL; mimeType?: string }> = [
{ type: "text", text: textContent },
];
for (const att of imageAttachments) {
parts.push({
type: "image",
image: new URL(att.downloadUrl),
mimeType: att.contentType,
});
}
messages[lastUserIdx] = { ...msg, content: parts as ModelMessage["content"] };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential data loss when message content is already multipart.

If msg.content is already an array of content parts (not a string), line 40 converts it to an empty string, losing the original content. Consider handling array content:

 const msg = messages[lastUserIdx];
-const textContent = typeof msg.content === "string" ? msg.content : "";
+const existingParts = typeof msg.content === "string" 
+  ? [{ type: "text" as const, text: msg.content }]
+  : Array.isArray(msg.content) ? msg.content : [];
 const parts: Array<{ type: string; text?: string; image?: URL; mimeType?: string }> = [
-  { type: "text", text: textContent },
+  ...existingParts,
 ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/emails/inbound/generateEmailResponse.ts` around lines 39 - 51, The code
in generateEmailResponse.ts overwrites existing multipart message content by
coercing msg.content to an empty string; instead, detect whether msg.content is
already an array and preserve/merge it: if msg.content is an array use that as
the initial parts (or concat new image parts) else create a text part from the
string as currently done; update the logic around messages[lastUserIdx] and the
parts construction (referencing msg, messages, lastUserIdx, parts,
imageAttachments, and ModelMessage["content"]) so existing multipart content is
retained and new image parts are appended.

Construct a proper UserModelMessage instead of spreading a generic
ModelMessage, fixing the discriminated union type incompatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
lib/emails/inbound/generateEmailResponse.ts (2)

39-40: ⚠️ Potential issue | 🟡 Minor

Potential data loss when msg.content is already multipart.

If msg.content is already an array of content parts (not a string), line 40 converts it to an empty string, silently discarding the original content. While getEmailRoomMessages currently returns string content, the ModelMessage type allows arrays—this creates fragile coupling.

🛡️ Proposed defensive handling
 const msg = messages[lastUserIdx];
-const textContent = typeof msg.content === "string" ? msg.content : "";
-const parts: UserModelMessage["content"] = [
-  { type: "text", text: textContent },
+const existingParts: UserModelMessage["content"] =
+  typeof msg.content === "string"
+    ? [{ type: "text", text: msg.content }]
+    : Array.isArray(msg.content)
+      ? msg.content
+      : [];
+const parts: UserModelMessage["content"] = [
+  ...existingParts,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/emails/inbound/generateEmailResponse.ts` around lines 39 - 40, The
current generateEmailResponse logic grabs the last user message (msg from
messages[lastUserIdx]) but forces non-string msg.content to an empty string,
risking data loss if content is an array per the ModelMessage type; update the
assignment of textContent in generateEmailResponse to defensively handle arrays
and other non-string types returned by getEmailRoomMessages — e.g., if
msg.content is an array, join parts (or JSON.stringify when parts are objects),
if it's an object serialize it appropriately, and otherwise fallback to an empty
string — so that msg, messages, lastUserIdx, and textContent are preserved
without silently discarding content.

43-47: ⚠️ Potential issue | 🟡 Minor

new URL() can throw on malformed URLs.

If att.downloadUrl is malformed, this will throw an unhandled exception, potentially breaking the entire email response flow. Consider defensive handling to skip invalid attachments rather than crashing.

🛡️ Proposed defensive handling
-        ...imageAttachments.map(att => ({
-          type: "image" as const,
-          image: new URL(att.downloadUrl),
-          mimeType: att.contentType,
-        })),
+        ...imageAttachments.flatMap(att => {
+          try {
+            return [{
+              type: "image" as const,
+              image: new URL(att.downloadUrl),
+              mimeType: att.contentType,
+            }];
+          } catch {
+            console.warn(`Skipping attachment with invalid URL: ${att.downloadUrl}`);
+            return [];
+          }
+        }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/emails/inbound/generateEmailResponse.ts` around lines 43 - 47, The
mapping that constructs image attachments uses new URL(att.downloadUrl) which
can throw on malformed URLs; in the imageAttachments -> map block (where objects
with type "image" and image: new URL(...) are created) wrap the URL construction
in defensive logic: attempt to parse/construct the URL inside a try/catch (or
validate it first) and if it fails simply skip that attachment (omit it from the
returned array) and optionally log a warning; ensure the rest of
generateEmailResponse continues without throwing for invalid att.downloadUrl
values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@lib/emails/inbound/generateEmailResponse.ts`:
- Around line 39-40: The current generateEmailResponse logic grabs the last user
message (msg from messages[lastUserIdx]) but forces non-string msg.content to an
empty string, risking data loss if content is an array per the ModelMessage
type; update the assignment of textContent in generateEmailResponse to
defensively handle arrays and other non-string types returned by
getEmailRoomMessages — e.g., if msg.content is an array, join parts (or
JSON.stringify when parts are objects), if it's an object serialize it
appropriately, and otherwise fallback to an empty string — so that msg,
messages, lastUserIdx, and textContent are preserved without silently discarding
content.
- Around line 43-47: The mapping that constructs image attachments uses new
URL(att.downloadUrl) which can throw on malformed URLs; in the imageAttachments
-> map block (where objects with type "image" and image: new URL(...) are
created) wrap the URL construction in defensive logic: attempt to
parse/construct the URL inside a try/catch (or validate it first) and if it
fails simply skip that attachment (omit it from the returned array) and
optionally log a warning; ensure the rest of generateEmailResponse continues
without throwing for invalid att.downloadUrl values.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ee7ae7 and a957496.

📒 Files selected for processing (1)
  • lib/emails/inbound/generateEmailResponse.ts

Remove image part injection into agent messages. Attachment download
URLs in the email text are sufficient — OpenClaw in the sandbox can
fetch any file type directly. This avoids signed URL access issues
and unsupported format errors (e.g. SVG) with LLM vision APIs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs at each step to identify where the email response pipeline
stops: memory validation, CC check, response generation, and send.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Attachment download URLs from email are temporary (expire in 1 hour).
Add sandbox prompt note telling OpenClaw to always download files
locally via curl instead of storing the raw URL in repo files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/emails/inbound/respondToInboundEmail.ts (1)

35-40: Extract a logging helper to reduce duplication and shrink function size.

The repeated stage logs are clear, but duplicated string templates across these lines make maintenance harder and push this domain function further past the size guideline. Consider a tiny helper for stage-based logging.

Suggested refactor
+function logInboundStage(emailId: string, stage: string, details?: string): void {
+  console.log(
+    `[respondToInboundEmail] Email ${emailId} - ${stage}${details ? ` (${details})` : ""}`,
+  );
+}

- console.log(`[respondToInboundEmail] Email ${emailId} - early return from validateNewEmailMemory`);
+ logInboundStage(emailId, "early return from validateNewEmailMemory");

- console.log(`[respondToInboundEmail] Email ${emailId} - memory validated, roomId=${chatRequestBody.roomId}, emailText length=${emailText.length}`);
+ logInboundStage(
+   emailId,
+   "memory validated",
+   `roomId=${chatRequestBody.roomId}, emailTextLength=${emailText.length}`,
+ );

- console.log(`[respondToInboundEmail] Email ${emailId} - generating response...`);
+ logInboundStage(emailId, "generating response");

As per coding guidelines, **/*.{ts,tsx}: “Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle” and lib/**/*.ts: “For domain functions, ensure ... Keep functions under 50 lines”.

Also applies to: 45-53, 73-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/emails/inbound/respondToInboundEmail.ts` around lines 35 - 40, The
function respondToInboundEmail has duplicated stage log templates (e.g., the
console.log lines using emailId, validationResult, chatRequestBody.roomId, and
emailText.length at multiple spots including the shown block and lines 45-53,
73-77); extract a small helper (e.g., logEmailStage or formatEmailStageLog) that
accepts (stage: string, emailId: string, opts?: {roomId?: string, emailText?:
string, extra?: any}) and builds/prints the standardized message, then replace
the repeated console.log calls in respondToInboundEmail (and the other noted
blocks) to call that helper so the domain function shrinks and duplicated string
templates are centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/emails/inbound/respondToInboundEmail.ts`:
- Around line 30-31: The logs in respondToInboundEmail are outputting raw PII
(email addresses via variables to and from); change the logging to not emit raw
addresses by introducing and using a redaction helper (e.g., redactEmail or
hashEmail) and replace direct uses of to and from in the console.log (and the
other log at the second occurrence) with the redacted/hashed value; update the
two places where emailId and to/from are logged (identify calls inside
respondToInboundEmail) to log emailId plus redactEmail(to) or redactEmail(from)
and ensure the helper is deterministic and non-reversible (hash or mask) and
reused for any other email logging in this file.
- Around line 67-74: In respondToInboundEmail, saveChatCompletion is being
called before confirming send success; move the call so it runs only after
sendEmailWithResend returns a non-error result (i.e., after the check that
result instanceof NextResponse fails). Specifically, ensure
sendEmailWithResend(payload) is awaited, then check if result is an error
(result instanceof NextResponse) and return on error, and only after that call
saveChatCompletion({ text, roomId }) so assistant replies are persisted only
when outbound delivery succeeded.

---

Nitpick comments:
In `@lib/emails/inbound/respondToInboundEmail.ts`:
- Around line 35-40: The function respondToInboundEmail has duplicated stage log
templates (e.g., the console.log lines using emailId, validationResult,
chatRequestBody.roomId, and emailText.length at multiple spots including the
shown block and lines 45-53, 73-77); extract a small helper (e.g., logEmailStage
or formatEmailStageLog) that accepts (stage: string, emailId: string, opts?:
{roomId?: string, emailText?: string, extra?: any}) and builds/prints the
standardized message, then replace the repeated console.log calls in
respondToInboundEmail (and the other noted blocks) to call that helper so the
domain function shrinks and duplicated string templates are centralized.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a5fb33 and 8b958ed.

📒 Files selected for processing (2)
  • lib/chat/tools/createPromptSandboxStreamingTool.ts
  • lib/emails/inbound/respondToInboundEmail.ts
✅ Files skipped from review due to trivial changes (1)
  • lib/chat/tools/createPromptSandboxStreamingTool.ts

Comment on lines 67 to 74
const result = await sendEmailWithResend(payload);

// Save the assistant response message
await saveChatCompletion({ text, roomId });

if (result instanceof NextResponse) {
console.log(`[respondToInboundEmail] Email ${emailId} - sendEmailWithResend returned error response`);
return result;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Persist completion only after send success is confirmed.

saveChatCompletion currently runs before the error check (result instanceof NextResponse). If send fails, the assistant reply is still persisted, which can desync memory from actual outbound delivery.

Suggested fix
 const result = await sendEmailWithResend(payload);

-// Save the assistant response message
-await saveChatCompletion({ text, roomId });
-
 if (result instanceof NextResponse) {
   console.log(`[respondToInboundEmail] Email ${emailId} - sendEmailWithResend returned error response`);
   return result;
 }
 
+// Save only after successful send
+await saveChatCompletion({ text, roomId });
+
 console.log(`[respondToInboundEmail] Email ${emailId} - reply sent successfully`);
 return NextResponse.json(result);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = await sendEmailWithResend(payload);
// Save the assistant response message
await saveChatCompletion({ text, roomId });
if (result instanceof NextResponse) {
console.log(`[respondToInboundEmail] Email ${emailId} - sendEmailWithResend returned error response`);
return result;
const result = await sendEmailWithResend(payload);
if (result instanceof NextResponse) {
console.log(`[respondToInboundEmail] Email ${emailId} - sendEmailWithResend returned error response`);
return result;
}
// Save only after successful send
await saveChatCompletion({ text, roomId });
console.log(`[respondToInboundEmail] Email ${emailId} - reply sent successfully`);
return NextResponse.json(result);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/emails/inbound/respondToInboundEmail.ts` around lines 67 - 74, In
respondToInboundEmail, saveChatCompletion is being called before confirming send
success; move the call so it runs only after sendEmailWithResend returns a
non-error result (i.e., after the check that result instanceof NextResponse
fails). Specifically, ensure sendEmailWithResend(payload) is awaited, then check
if result is an error (result instanceof NextResponse) and return on error, and
only after that call saveChatCompletion({ text, roomId }) so assistant replies
are persisted only when outbound delivery succeeded.

sweetmantech and others added 2 commits February 28, 2026 12:46
…nterface

Replace the custom EmailAttachment interface with
ListAttachmentsResponseSuccess["data"][number] from the Resend SDK.
Remove the camelCase remapping layer — return SDK objects directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sweetmantech sweetmantech merged commit 2265e8e into test Feb 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant