#Trying to Setup Twilio and TTS

1 messages · Page 1 of 1 (latest)

jade valve
#

Voice-call plugin returning empty <Response></Response> TwiML instead of <Connect><Stream>.

Setup: Twilio + streaming enabled + publicUrl set explicitly + Tailscale funnel manually configured. Plugin installed via openclaw plugins install but system copy at /usr/lib/node_modules/openclaw/extensions/voice-call/ also loads → duplicate plugin warning on startup → system copy wins → publicUrl never set on provider → empty TwiML.

Fix attempted: removed tailscale block from config, reinstalled plugin. Duplicate persists.

Version: 2026.2.26. Plugin: @openclaw/voice-call 2026.2.25.

void sedgeBOT
#

A couple things here that are easy to mix up:

1) <Response></Response> can be “normal” for the status callback

The Twilio provider intentionally returns empty TwiML when the request URL includes ?type=status (that’s the status-callback webhook, not the “give me TwiML instructions” webhook).

So first: in Twilio’s debugger/logs, check the exact request URL that got the empty response.

  • If it ends in type=status → empty TwiML is expected.
  • The TwiML request URL is the one without that flag (and usually includes callId=... for outbound calls initiated by OpenClaw).

2) If you want <Connect><Stream>…, make sure you’re actually in conversation mode

By default the plugin’s outbound mode is notify, which uses <Say> (inline TwiML) and will not start a media stream.

Either set:

plugins: {
#
  entries: {
    "voice-call": {
      config: {
        outbound: { defaultMode: "conversation" },
        streaming: { enabled: true }
      }
    }
  }
}

…or pass conversation mode on the command (CLI supports it).

3) Duplicate plugin id: the “winner” is purely discovery precedence — verify with plugins list

Per the plugin loader, only the first discovered voice-call wins (config paths → workspace extensions → ~/.openclaw/extensions → bundled /.../openclaw/extensions).

#

So if /usr/lib/node_modules/openclaw/extensions/voice-call is winning, you almost certainly have one of these:

  • a plugins.load.paths entry pointing at the bundled folder (that makes it “config origin”, i.e. highest precedence), or
  • the plugin was installed into a different state dir / user than the running gateway is using.

What I’d do next (this usually makes the root cause obvious):

  1. openclaw plugins list (paste output)
  2. openclaw plugins info voice-call (paste output)
  3. openclaw gateway status (look for config/service mismatch)

If you want a quick cleanup attempt without manual spelunking, try:

  • openclaw plugins uninstall voice-call (this removes entries/installs/load.paths for that id)
  • then either:
    • use bundled: openclaw plugins enable voice-call
    • or use npm: openclaw plugins install @openclaw/voice-call
  • restart the gateway
jade valve
#

Thanks, useful distinction on the status callback.

Checked the logs: the duplicate plugin warning fires on every single startup — "duplicate plugin id detected; later plugin may be overridden (/usr/lib/node_modules/openclaw/extensions/voice-call/index.ts)". The system copy at /usr/lib/node_modules/openclaw/extensions/voice-call/ loads first and wins, so the user-installed copy's config (including publicUrl) never takes effect. That means getStreamUrl() returns null → empty TwiML.

The defaultMode is already set to "conversation" in config, so point 2 is fine.

The core issue: how do I suppress/remove the system copy so only the user-installed plugin loads? Or is there a way to force the user copy to take precedence? Running v2026.2.26.

void sedgeBOT
#

You generally don’t remove /usr/lib/node_modules/openclaw/extensions/voice-call/ — that’s the bundled copy shipped with core, and it’s expected to be discoverable.

Two important clarifications:

1) That warning line actually implies /usr/lib/... is the loser, not the winner

The message:

duplicate plugin id detected; later plugin may be overridden (/usr/lib/.../voice-call/index.ts)

means: “we already saw a voice-call earlier; this later one might be ignored.”

Bundled plugins are discovered last (config load.paths → workspace → ~/.openclaw/extensions → bundled), so if your npm-installed plugin is being discovered, it should win.

So if behavior suggests the bundled copy is winning, it’s usually because the installed copy isn’t being discovered by the running Gateway service (profile/user/state-dir mismatch).

2) Plugin code copy doesn’t change your publicUrl config

publicUrl comes from plugins.entries.voice-call.config.publicUrl in your config. Both copies read the same config. If currentPublicUrl is null at runtime, that almost always means the Gateway process is running with a different config/state dir than the one you edited.

#

What to do

A) Verify what’s actually loaded

Please run and paste:

openclaw plugins list
openclaw plugins info voice-call
openclaw gateway status

In plugins list/info, look for the source and status: loaded entry for voice-call. You should also see the overridden one marked disabled with an “overridden by …” reason.

In gateway status, check whether it shows Config (cli) vs Config (service) pointing at different files (that’s the classic “I changed config but the daemon is using another one” problem).

B) Fix the common root cause: profile / state-dir mismatch

If you’re using profiles (or installed the service under a different user), install the plugin under the same profile the service uses:

#
openclaw --profile <the-one-your-service-uses> plugins install @openclaw/voice-call
openclaw --profile <same> gateway restart

(Or set OPENCLAW_STATE_DIR=... consistently.)

C) If you really want to force precedence: pin it via plugins.load.paths

Config load paths are highest precedence. You can force the exact directory to win:

{
  plugins: {
    load: {
      paths: ["~/.openclaw/extensions/voice-call"]
    }