Skip to content

fix(voice-call): accept externally-initiated Twilio outbound-api calls#31181

Merged
steipete merged 3 commits intoopenclaw:mainfrom
scoootscooob:fix/voice-call-external-outbound
Mar 2, 2026
Merged

fix(voice-call): accept externally-initiated Twilio outbound-api calls#31181
steipete merged 3 commits intoopenclaw:mainfrom
scoootscooob:fix/voice-call-external-outbound

Conversation

@scoootscooob
Copy link
Contributor

Problem

When calls are initiated directly via the Twilio REST API (e.g. curl -X POST .../Calls.json -d "Url=https://example.com/voice/webhook"), the voice-call plugin rejects the MediaStream connection with:

[MediaStream] Rejecting stream for unknown call: CAxxxxxxx

The call is immediately terminated after the recipient answers — no audio is played.

Fixes #30900

Root Cause

processEvent() in manager/events.ts only auto-registers untracked calls when event.direction === "inbound". Calls initiated via Twilio REST API arrive with Direction=outbound-api, which parseDirection() normalizes to "outbound". Since "outbound" !== "inbound", the call is never registered in CallManager, and shouldAcceptStream() rejects the media stream.

Fix

Expand the auto-registration condition to also cover direction === "outbound" — these are externally-initiated outbound-api calls arriving at our webhook without a pre-existing tracked call. Inbound policy checks (disabled/allowlist/pairing) still only apply to true inbound calls; external outbound-api calls are implicitly trusted because the caller controls the webhook URL.

Test

  • 2 new tests in events.test.ts:
    • Verifies externally-initiated outbound-api calls are auto-registered in activeCalls and providerCallIdMap
    • Verifies outbound calls bypass disabled inbound policy (no hangup)
  • All 80 existing voice-call tests continue to pass

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle bot added channel: voice-call Channel integration: voice-call size: S labels Mar 2, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6adf336370

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const isUnregisteredWebhookCall =
!call &&
event.providerCallId &&
(event.direction === "inbound" || event.direction === "outbound");

Choose a reason for hiding this comment

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

P2 Badge Preserve outbound direction for auto-registered calls

Including event.direction === "outbound" in this auto-registration branch routes externally initiated outbound-api events through createInboundCall, which hardcodes direction: "inbound" and inbound metadata. That misclassifies these calls and changes downstream behavior that depends on direction (for example, the webhook auto-response gate in extensions/voice-call/src/webhook.ts treats inbound calls as always conversational), so outbound-api calls can be handled as inbound conversations unexpectedly. Register outbound-api calls with outbound direction (or a dedicated helper) instead of the inbound constructor path.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

Correctly expands auto-registration to cover externally-initiated Twilio outbound-api calls, fixing MediaStream rejection issue. Security logic properly bypasses inbound policy for external outbound calls.

Critical Issue:

  • Externally-initiated outbound calls are stored with incorrect direction: "inbound" because createInboundCall() hardcodes the direction field, even though these calls have event.direction === "outbound". This affects data integrity and any downstream code relying on accurate direction information.

Other Issues:

  • Test coverage doesn't verify that call.direction matches the event's direction, which would have caught this bug.

Confidence Score: 2/5

  • Fixes the immediate MediaStream rejection issue but introduces data integrity bug with incorrect direction field storage
  • Core functionality works and security logic is sound, but externally-initiated outbound calls are stored with wrong direction: "inbound" instead of "outbound". This data inconsistency could cause unexpected behavior in code relying on accurate direction information (e.g., analytics, logging, business logic checking call.direction). The fix should preserve the event's actual direction.
  • extensions/voice-call/src/manager/events.ts requires attention to fix the direction field issue in the createInboundCall function

Last reviewed commit: 6adf336

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +253 to +259
// Call should be registered in activeCalls and providerCallIdMap
expect(ctx.activeCalls.size).toBe(1);
expect(ctx.providerCallIdMap.get("CA-external-123")).toBeDefined();
const call = [...ctx.activeCalls.values()][0];
expect(call?.providerCallId).toBe("CA-external-123");
expect(call?.from).toBe("+15550000000");
expect(call?.to).toBe("+15559876543");
Copy link
Contributor

Choose a reason for hiding this comment

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

Test doesn't verify that call.direction matches the event's direction. Add:

Suggested change
// Call should be registered in activeCalls and providerCallIdMap
expect(ctx.activeCalls.size).toBe(1);
expect(ctx.providerCallIdMap.get("CA-external-123")).toBeDefined();
const call = [...ctx.activeCalls.values()][0];
expect(call?.providerCallId).toBe("CA-external-123");
expect(call?.from).toBe("+15550000000");
expect(call?.to).toBe("+15559876543");
// Call should be registered in activeCalls and providerCallIdMap
expect(ctx.activeCalls.size).toBe(1);
expect(ctx.providerCallIdMap.get("CA-external-123")).toBeDefined();
const call = [...ctx.activeCalls.values()][0];
expect(call?.direction).toBe("outbound");
expect(call?.providerCallId).toBe("CA-external-123");
expect(call?.from).toBe("+15550000000");
expect(call?.to).toBe("+15559876543");
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/voice-call/src/manager/events.test.ts
Line: 253-259

Comment:
Test doesn't verify that `call.direction` matches the event's direction. Add:
```suggestion
    // Call should be registered in activeCalls and providerCallIdMap
    expect(ctx.activeCalls.size).toBe(1);
    expect(ctx.providerCallIdMap.get("CA-external-123")).toBeDefined();
    const call = [...ctx.activeCalls.values()][0];
    expect(call?.direction).toBe("outbound");
    expect(call?.providerCallId).toBe("CA-external-123");
    expect(call?.from).toBe("+15550000000");
    expect(call?.to).toBe("+15559876543");
```

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Additional Comments (1)

extensions/voice-call/src/manager/events.ts
createInboundCall() hardcodes direction: "inbound" (line 74), but is now used to create externally-initiated outbound calls. The created CallRecord will have direction: "inbound" even though event.direction === "outbound". This creates data inconsistency.

Consider passing the event's direction to createInboundCall() or renaming it to a more general createWebhookCall(direction) function.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/voice-call/src/manager/events.ts
Line: 145

Comment:
`createInboundCall()` hardcodes `direction: "inbound"` (line 74), but is now used to create externally-initiated outbound calls. The created `CallRecord` will have `direction: "inbound"` even though `event.direction === "outbound"`. This creates data inconsistency.

Consider passing the event's direction to `createInboundCall()` or renaming it to a more general `createWebhookCall(direction)` function.

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 502b1aa3a4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

CHANGELOG.md Outdated
@@ -2,338 +2,97 @@

Docs: https://docs.openclaw.ai

## 2026.3.2 (Unreleased)
## 2026.2.27

Choose a reason for hiding this comment

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

P1 Badge Restore current changelog head before adding new entry

This commit replaces the existing 2026.3.2 (Unreleased) head with an older 2026.2.27 snapshot, effectively dropping a large set of already-recorded entries and PR references from CHANGELOG.md; that is a regression in release metadata and will trip the repo’s changelog merge-hygiene check in scripts/pr (it rejects dropped PR refs that are not re-added). Please rebase this changelog edit onto the current head and add only the new voice-call fix line.

Useful? React with 👍 / 👎.

@scoootscooob scoootscooob force-pushed the fix/voice-call-external-outbound branch from 502b1aa to 76f8a06 Compare March 2, 2026 17:37
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76f8a06d57

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +83 to +86
initialMessage:
params.direction === "inbound"
? params.ctx.config.inboundGreeting || "Hello! How can I help you today?"
: undefined,

Choose a reason for hiding this comment

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

P1 Badge Populate mode for auto-registered outbound webhook calls

When an untracked outbound webhook call is auto-registered here, the new CallRecord does not set metadata.mode, so downstream transcript handling treats it as non-conversational. In extensions/voice-call/src/webhook.ts:127-129, auto-replies only run for inbound calls or mode === "conversation"; externally initiated outbound-api calls created by this path therefore accept the stream but never produce bot responses, leaving the call silent after user speech.

Useful? React with 👍 / 👎.

scoootscooob and others added 3 commits March 2, 2026 23:55
Fixes openclaw#30900 — Calls initiated directly via the Twilio REST API
(Direction=outbound-api) were rejected as "unknown call" because
processEvent only auto-registered calls with direction=inbound.
External outbound-api calls now get registered in the CallManager
so the media stream is accepted. Inbound policy checks still only
apply to true inbound calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ection

Address Greptile review: externally-initiated outbound-api calls were
stored with hardcoded direction: "inbound". Now createWebhookCall accepts
a direction parameter so the CallRecord accurately reflects the event's
actual direction. Also skip inboundGreeting for outbound calls and add a
test asserting inbound direction is preserved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@steipete steipete force-pushed the fix/voice-call-external-outbound branch from 76f8a06 to 6565e9f Compare March 2, 2026 23:56
@steipete steipete merged commit 381bb86 into openclaw:main Mar 2, 2026
@steipete
Copy link
Contributor

steipete commented Mar 2, 2026

Landed via temp rebase onto main.

  • Gate: pnpm lint && pnpm build && pnpm test
  • Land commit: 6565e9f
  • Merge commit: 381bb86

Thanks @scoootscooob!

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6565e9fa96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 82 to +86
metadata: {
initialMessage: params.ctx.config.inboundGreeting || "Hello! How can I help you today?",
initialMessage:
params.direction === "inbound"
? params.ctx.config.inboundGreeting || "Hello! How can I help you today?"
: undefined,

Choose a reason for hiding this comment

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

P1 Badge Set mode for auto-registered outbound webhook calls

Auto-registering an untracked outbound webhook call here leaves metadata.mode unset, which breaks conversational handling for the very outbound-api flow this commit adds. In extensions/voice-call/src/webhook.ts:127-129, auto-response only runs when call.direction === "inbound" or call.metadata.mode === "conversation"; these outbound calls therefore accept media/transcripts but never trigger handleInboundResponse, resulting in silent calls after user speech unless another path manually drives responses.

Useful? React with 👍 / 👎.

dawi369 pushed a commit to dawi369/davis that referenced this pull request Mar 3, 2026
OWALabuy pushed a commit to kcinzgg/openclaw that referenced this pull request Mar 4, 2026
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: voice-call Channel integration: voice-call size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: voice-call plugin rejects externally-initiated Twilio calls as "unknown"

2 participants