Skip to content

Comments

Fast Message Polling For Webhooks#420

Open
khaliqgant wants to merge 9 commits intomainfrom
feature/fast-message-polling
Open

Fast Message Polling For Webhooks#420
khaliqgant wants to merge 9 commits intomainfrom
feature/fast-message-polling

Conversation

@khaliqgant
Copy link
Collaborator

@khaliqgant khaliqgant commented Feb 13, 2026

khaliqgant and others added 4 commits February 13, 2026 15:09
The daemon previously only fetched messages from cloud during its 60-second
heartbeat cycle, causing unacceptable delays for Slack-initiated messages.
This adds a separate 5-second message poll timer so cloud messages (like
Slack mentions routed to agents) are delivered within seconds instead of up
to a minute.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the 5-second setInterval message polling with a long-poll loop
that holds the HTTP connection open until messages arrive or a 25s timeout
expires. This reduces latency from up to 5s to near-instant delivery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Daemon now injects RELAY_CLOUD_URL, WORKSPACE_TOKEN, and WORKSPACE_ID
into spawned agents' environments (same pattern as GH_TOKEN for gh CLI).
For messages routed to existing agents, the daemon enriches the message
with the correct inline slack CLI command using its known cloud URL.

Also adds __cloud__ relay target support so agents can forward messages
to the cloud server via the daemon's connection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@khaliqgant khaliqgant changed the title Fast Message Polling For Slack Fast Message Polling For Webhooks Feb 13, 2026
*/
async spawn(request: SpawnRequest): Promise<SpawnResult> {
const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions, interactive, model: modelOverride } = request;
const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions, interactive, model: modelOverride, extraEnv } = request;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Copilot Autofix

AI 11 days ago

In general: replace uses of Math.random() for generating these agent names with a cryptographically secure source of randomness (crypto.randomInt or crypto.getRandomValues) so that the selection of adjectives/nouns and the numeric suffix are not predictable, while keeping the resulting format (AdjectiveNoun or AdjectiveNounNNN) and all call sites unchanged.

Concretely for this project, the only insecure randomness is in packages/utils/src/name-generator.ts. We can fix it by:

  • Importing Node’s crypto module in that file.
  • Implementing a small helper function secureRandomInt(max: number) using crypto.randomInt (which is available in Node and avoids modulo bias).
  • Using secureRandomInt instead of Math.random() when indexing into ADJECTIVES and NOUNS, and when generating the fallback numeric suffix.

This change is local to name-generator.ts; no callers need to change, and it preserves the types and return values. The spawner and CLI code will continue to work as before, but the generated names will now be derived from a CSPRNG.


Suggested changeset 1
packages/utils/src/name-generator.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/utils/src/name-generator.ts b/packages/utils/src/name-generator.ts
--- a/packages/utils/src/name-generator.ts
+++ b/packages/utils/src/name-generator.ts
@@ -3,6 +3,8 @@
  * Inspired by mcp_agent_mail's approach.
  */
 
+import crypto from 'node:crypto';
+
 const ADJECTIVES = [
   'Blue', 'Green', 'Red', 'Purple', 'Golden', 'Silver', 'Crystal', 'Amber',
   'Coral', 'Jade', 'Ruby', 'Sapphire', 'Emerald', 'Onyx', 'Pearl', 'Copper',
@@ -25,12 +27,17 @@
   'Star', 'Moon', 'Sun', 'Comet', 'Cloud', 'Storm', 'Thunder', 'Lightning',
 ];
 
+function secureRandomInt(max: number): number {
+  // crypto.randomInt generates a cryptographically secure integer in [0, max)
+  return crypto.randomInt(0, max);
+}
+
 /**
  * Generate a random agent name (AdjectiveNoun format).
  */
 export function generateAgentName(): string {
-  const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
-  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
+  const adjective = ADJECTIVES[secureRandomInt(ADJECTIVES.length)];
+  const noun = NOUNS[secureRandomInt(NOUNS.length)];
   return `${adjective}${noun}`;
 }
 
@@ -45,7 +48,7 @@
     }
   }
   // Fallback: append random suffix
-  return `${generateAgentName()}${Math.floor(Math.random() * 1000)}`;
+  return `${generateAgentName()}${secureRandomInt(1000)}`;
 }
 
 /**
EOF
@@ -3,6 +3,8 @@
* Inspired by mcp_agent_mail's approach.
*/

import crypto from 'node:crypto';

const ADJECTIVES = [
'Blue', 'Green', 'Red', 'Purple', 'Golden', 'Silver', 'Crystal', 'Amber',
'Coral', 'Jade', 'Ruby', 'Sapphire', 'Emerald', 'Onyx', 'Pearl', 'Copper',
@@ -25,12 +27,17 @@
'Star', 'Moon', 'Sun', 'Comet', 'Cloud', 'Storm', 'Thunder', 'Lightning',
];

function secureRandomInt(max: number): number {
// crypto.randomInt generates a cryptographically secure integer in [0, max)
return crypto.randomInt(0, max);
}

/**
* Generate a random agent name (AdjectiveNoun format).
*/
export function generateAgentName(): string {
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
const adjective = ADJECTIVES[secureRandomInt(ADJECTIVES.length)];
const noun = NOUNS[secureRandomInt(NOUNS.length)];
return `${adjective}${noun}`;
}

@@ -45,7 +48,7 @@
}
}
// Fallback: append random suffix
return `${generateAgentName()}${Math.floor(Math.random() * 1000)}`;
return `${generateAgentName()}${secureRandomInt(1000)}`;
}

/**
Copilot is powered by AI and may make mistakes. Always verify output.
*/
async spawn(request: SpawnRequest): Promise<SpawnResult> {
const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions, interactive, model: modelOverride } = request;
const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions, interactive, model: modelOverride, extraEnv } = request;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Copilot Autofix

AI 9 days ago

General fix: replace all uses of Math.random() in this agent name generation utility with a cryptographically secure pseudorandom number generator (CSPRNG). In Node.js, this is best done via crypto.randomInt (for uniform integer selection in a range) or crypto.randomBytes, which are both cryptographically secure. We should avoid introducing bias when mapping bytes/numbers to indices.

Best concrete fix: in packages/utils/src/name-generator.ts, import Node’s crypto module and implement a small helper that uses crypto.randomInt(max) to select a random index for arrays and suffixes. Then:

  • Change generateAgentName() to use secureRandomInt(ADJECTIVES.length) and secureRandomInt(NOUNS.length) instead of Math.random().
  • Change the fallback in generateUniqueAgentName() to use secureRandomInt(1000) instead of Math.random().

This confines the change to the randomness implementation while leaving public function signatures and overall behavior intact (still adjective+noun optionally with numeric suffix). No other files (src/cli/index.ts, packages/config/src/shadow-config.ts, packages/bridge/src/spawner.ts) need code changes; they will automatically benefit from the improved randomness.

Concretely:

  • Edit packages/utils/src/name-generator.ts to:
    • Add import crypto from 'node:crypto'; at the top (without altering existing exports).
    • Add a small helper function secureRandomInt(max: number): number that calls crypto.randomInt(max).
    • Replace each Math.floor(Math.random() * ADJECTIVES.length) / NOUNS.length / 1000 with secureRandomInt(...).

No changes are needed to imports or code in the other snippets, since they do not call Math.random() directly.


Suggested changeset 1
packages/utils/src/name-generator.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/utils/src/name-generator.ts b/packages/utils/src/name-generator.ts
--- a/packages/utils/src/name-generator.ts
+++ b/packages/utils/src/name-generator.ts
@@ -3,6 +3,8 @@
  * Inspired by mcp_agent_mail's approach.
  */
 
+import crypto from 'node:crypto';
+
 const ADJECTIVES = [
   'Blue', 'Green', 'Red', 'Purple', 'Golden', 'Silver', 'Crystal', 'Amber',
   'Coral', 'Jade', 'Ruby', 'Sapphire', 'Emerald', 'Onyx', 'Pearl', 'Copper',
@@ -25,12 +27,17 @@
   'Star', 'Moon', 'Sun', 'Comet', 'Cloud', 'Storm', 'Thunder', 'Lightning',
 ];
 
+function secureRandomInt(max: number): number {
+  // crypto.randomInt generates a uniform integer in [0, max)
+  return crypto.randomInt(max);
+}
+
 /**
  * Generate a random agent name (AdjectiveNoun format).
  */
 export function generateAgentName(): string {
-  const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
-  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
+  const adjective = ADJECTIVES[secureRandomInt(ADJECTIVES.length)];
+  const noun = NOUNS[secureRandomInt(NOUNS.length)];
   return `${adjective}${noun}`;
 }
 
@@ -45,7 +48,7 @@
     }
   }
   // Fallback: append random suffix
-  return `${generateAgentName()}${Math.floor(Math.random() * 1000)}`;
+  return `${generateAgentName()}${secureRandomInt(1000)}`;
 }
 
 /**
EOF
@@ -3,6 +3,8 @@
* Inspired by mcp_agent_mail's approach.
*/

import crypto from 'node:crypto';

const ADJECTIVES = [
'Blue', 'Green', 'Red', 'Purple', 'Golden', 'Silver', 'Crystal', 'Amber',
'Coral', 'Jade', 'Ruby', 'Sapphire', 'Emerald', 'Onyx', 'Pearl', 'Copper',
@@ -25,12 +27,17 @@
'Star', 'Moon', 'Sun', 'Comet', 'Cloud', 'Storm', 'Thunder', 'Lightning',
];

function secureRandomInt(max: number): number {
// crypto.randomInt generates a uniform integer in [0, max)
return crypto.randomInt(max);
}

/**
* Generate a random agent name (AdjectiveNoun format).
*/
export function generateAgentName(): string {
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
const adjective = ADJECTIVES[secureRandomInt(ADJECTIVES.length)];
const noun = NOUNS[secureRandomInt(NOUNS.length)];
return `${adjective}${noun}`;
}

@@ -45,7 +48,7 @@
}
}
// Fallback: append random suffix
return `${generateAgentName()}${Math.floor(Math.random() * 1000)}`;
return `${generateAgentName()}${secureRandomInt(1000)}`;
}

/**
Copilot is powered by AI and may make mistakes. Always verify output.
*/
async spawn(request: SpawnRequest): Promise<SpawnResult> {
const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions, interactive, model: modelOverride } = request;
const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions, interactive, model: modelOverride, extraEnv } = request;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Copilot Autofix

AI 11 days ago

To fix the problem, we need to replace the use of Math.random() in the name generation utilities with a cryptographically secure source of randomness. On Node.js, the recommended way is to use crypto.randomInt (or crypto.randomBytes), which is backed by a CSPRNG. We should keep the external behavior the same (still choosing a random adjective and noun and, in the fallback, a numeric suffix), but base all randomness on crypto instead of Math.random.

The best targeted fix is in packages/utils/src/name-generator.ts:

  • Import Node’s crypto module (e.g., import crypto from 'node:crypto';).
  • Add a small helper secureRandomIndex(max: number): number that returns an integer in [0, max) using crypto.randomInt.
  • Change generateAgentName() to select indices using secureRandomIndex(ADJECTIVES.length) and secureRandomIndex(NOUNS.length) instead of Math.floor(Math.random() * ...).
  • Change the fallback in generateUniqueAgentName to use crypto.randomInt(1000) instead of Math.floor(Math.random() * 1000).

No changes are needed in src/cli/index.ts or packages/bridge/src/spawner.ts, because once generateAgentName() and generateUniqueAgentName() are secure, all downstream uses (including the task sent into spawn) will be derived from secure randomness.

Concretely:

  • In packages/utils/src/name-generator.ts, add the crypto import near the top with the other declarations.
  • Insert the helper function secureRandomIndex.
  • Update the two lines using Math.random() (line 32 and line 48) to use crypto.randomInt via the helper.

Suggested changeset 1
packages/utils/src/name-generator.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/utils/src/name-generator.ts b/packages/utils/src/name-generator.ts
--- a/packages/utils/src/name-generator.ts
+++ b/packages/utils/src/name-generator.ts
@@ -3,6 +3,8 @@
  * Inspired by mcp_agent_mail's approach.
  */
 
+import crypto from 'node:crypto';
+
 const ADJECTIVES = [
   'Blue', 'Green', 'Red', 'Purple', 'Golden', 'Silver', 'Crystal', 'Amber',
   'Coral', 'Jade', 'Ruby', 'Sapphire', 'Emerald', 'Onyx', 'Pearl', 'Copper',
@@ -26,11 +28,21 @@
 ];
 
 /**
+ * Get a cryptographically secure random integer in the range [0, max).
+ */
+function secureRandomIndex(max: number): number {
+  if (max <= 0) {
+    throw new Error('secureRandomIndex: max must be positive');
+  }
+  return crypto.randomInt(0, max);
+}
+
+/**
  * Generate a random agent name (AdjectiveNoun format).
  */
 export function generateAgentName(): string {
-  const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
-  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
+  const adjective = ADJECTIVES[secureRandomIndex(ADJECTIVES.length)];
+  const noun = NOUNS[secureRandomIndex(NOUNS.length)];
   return `${adjective}${noun}`;
 }
 
@@ -45,7 +54,7 @@
     }
   }
   // Fallback: append random suffix
-  return `${generateAgentName()}${Math.floor(Math.random() * 1000)}`;
+  return `${generateAgentName()}${crypto.randomInt(0, 1000)}`;
 }
 
 /**
EOF
@@ -3,6 +3,8 @@
* Inspired by mcp_agent_mail's approach.
*/

import crypto from 'node:crypto';

const ADJECTIVES = [
'Blue', 'Green', 'Red', 'Purple', 'Golden', 'Silver', 'Crystal', 'Amber',
'Coral', 'Jade', 'Ruby', 'Sapphire', 'Emerald', 'Onyx', 'Pearl', 'Copper',
@@ -26,11 +28,21 @@
];

/**
* Get a cryptographically secure random integer in the range [0, max).
*/
function secureRandomIndex(max: number): number {
if (max <= 0) {
throw new Error('secureRandomIndex: max must be positive');
}
return crypto.randomInt(0, max);
}

/**
* Generate a random agent name (AdjectiveNoun format).
*/
export function generateAgentName(): string {
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
const adjective = ADJECTIVES[secureRandomIndex(ADJECTIVES.length)];
const noun = NOUNS[secureRandomIndex(NOUNS.length)];
return `${adjective}${noun}`;
}

@@ -45,7 +54,7 @@
}
}
// Fallback: append random suffix
return `${generateAgentName()}${Math.floor(Math.random() * 1000)}`;
return `${generateAgentName()}${crypto.randomInt(0, 1000)}`;
}

/**
Copilot is powered by AI and may make mistakes. Always verify output.
const text = typeof body === 'string' ? body : JSON.stringify(body);

try {
const response = await fetch(`${this.cloudUrl}/api/daemons/slack-reply`, {

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
Comment on lines +961 to +964
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.cloudApiKey}`,
},

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

View 7 additional findings in Devin Review.

Open in Devin Review

agent-relay-staging bot pushed a commit that referenced this pull request Feb 14, 2026
Comprehensive investigation and fixes for:
1. Thread context dropout - agents spawn without thread_ts
2. Missing message acknowledgment - spawn commands can be silently lost

Proposes 4-phase solution:
- Phase 1: Thread context propagation (P0)
- Phase 2: Message ACK protocol with retry logic (P0)
- Phase 3: Request correlation and tracing (P1)
- Phase 4: Thread timestamp validation (P2)

Includes:
- Root cause analysis with affected file locations
- Detailed implementation specs with code examples
- Database schema changes for message tracking
- ACK protocol implementation
- Success metrics and monitoring strategy
- Testing and rollout plans

Repository branches:
- relay: feature/fast-message-polling (PR #420)
- relay-dashboard: feat/slack-integration-v2 (PR #47)
- relay-cloud: feature/slack-cli (PR #84)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Comment on lines +321 to +328
body: JSON.stringify({
agents,
metrics: {
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
},
...(syncMessages ? { syncMessages, repoFullName: this.repoFullName } : {}),
}),

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 11 additional findings in Devin Review.

Open in Devin Review

Comment on lines +809 to +816
// Store Slack context for reply routing
if (msg.metadata?.slackReply && msg.metadata?.channelId && msg.metadata?.threadTs && msg.metadata?.workspaceId) {
this.slackContexts.set(msg.to, {
channelId: msg.metadata.channelId as string,
threadTs: msg.metadata.threadTs as string,
workspaceId: msg.metadata.workspaceId as string,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Slack context stored under original target name, but fallback agent replies under its own name

When a cross-machine Slack message arrives, the Slack context (channelId, threadTs, workspaceId) is stored using msg.to as the key. However, the router's fallback logic may deliver the message to a different agent (e.g., "Lead"). When that agent replies to __cloud__, handleCloudMessage looks up the Slack context using the agent's own name, which won't match.

Root Cause and Impact

At packages/daemon/src/server.ts:811, the Slack context is stored as this.slackContexts.set(msg.to, ...) where msg.to is the original target agent name (e.g., "SomeAgent").

However, the router's cross-machine fallback at packages/daemon/src/router.ts:730-748 may reroute the message to "Lead" or the first available agent if the original target doesn't exist locally.

When the fallback agent ("Lead") later sends a reply to __cloud__, handleCloudMessage at packages/daemon/src/server.ts:945 calls this.slackContexts.get(from) where from is "Lead". Since the context was stored under the original target name (not "Lead"), the lookup returns undefined, and the reply is silently dropped with a warning log.

Impact: Slack replies from agents that received messages via the fallback routing will never be delivered back to Slack.

Prompt for agents
The Slack context is stored using msg.to as the key (the original intended target), but the router may deliver the message to a different agent via fallback (e.g., Lead). The fix should ensure the Slack context is stored under the name of the agent that actually receives the message, not the original target. One approach: after router.route() delivers the message, store the Slack context under the actual recipient's name. Alternatively, store the context under both the original target AND the fallback target. Another approach is to include the Slack context in the message data/metadata so the receiving agent can reference it when replying, rather than relying on a daemon-side lookup by agent name.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- router.ts: Update `to` parameter to `actualTo` after fallback for correct
  downstream tracking (recordReceive, setProcessing)
- router.ts: Add .catch() handler for cloud message storage
- server.ts: Fix metadata spread order to prevent _crossMachine overwrite
- server.ts: Add Slack context fallback extraction from message data
- server.ts: Add slackContexts.delete() cleanup in removeStaleAgent,
  onClose, onError, and stop() to prevent memory leaks

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

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 13 additional findings in Devin Review.

Open in Devin Review

Accept both 'channelId' and 'channel' field names in handleCloudSpawnCommand
for compatibility with different cloud server metadata formats.

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

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 16 additional findings in Devin Review.

Open in Devin Review

Instead of dropping messages when no connections exist, persist them
to storage for later delivery when an agent connects. This prevents
message loss during brief periods when no agents are connected.

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

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 20 additional findings in Devin Review.

Open in Devin Review

Comment on lines +737 to +743
// Fallback 2: Try first available agent (excluding human users)
for (const [name, agent] of this.agents) {
agentTarget = agent;
actualTo = name;
routerLog.info(`Lead not found, falling back to first available agent "${name}"`, { from, requestedTarget: to });
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Cross-machine fallback routing can redirect messages to unrelated agents

The cross-machine message fallback chain in sendDirect falls back to the first available agent in the this.agents Map when neither the target nor "Lead" is found. This can route messages to completely unrelated agents that have no context about the incoming task.

Detailed Explanation

At packages/daemon/src/router.ts:738-743, when a cross-machine message's target is not found and no "Lead" agent exists:

for (const [name, agent] of this.agents) {
  agentTarget = agent;
  actualTo = name;
  break;
}

This picks the first agent from the Map iteration order, which is insertion order. This could be any agent - a Dashboard connection, a worker doing something completely different, etc. The message gets silently redirected without the sender knowing.

While the code excludes human users (only iterates this.agents, not this.users), it doesn't filter out internal agents (like those starting with __). An internal agent like __cli_sender__ could receive the message.

Impact: Cross-machine messages (e.g., from Slack) could be delivered to the wrong agent, causing confusion and potentially triggering unintended actions. The original sender has no indication that their message was rerouted.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Add fallback values for from/to fields since SendEnvelope types
have these as optional, but saveMessage requires strings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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