mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(voice-call): coalesce webhook server starts
This commit is contained in:
@@ -75,6 +75,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Browser/sandbox: clean up idle tracked tabs opened by primary-agent browser sessions, while preserving active tab reuse and lifecycle cleanup for subagents, cron, and ACP sessions. Fixes #71165. Thanks @dwbutler.
|
||||
- Plugins/Voice Call: reuse the webhook runtime across in-process plugin contexts, avoiding `EADDRINUSE` when agent tools or CLI commands run while the Gateway already owns the voice webhook port. Fixes #58115. Thanks @sfbrian.
|
||||
- Plugins/Voice Call: answer accepted Telnyx inbound Call Control legs on `call.initiated`, so webhooks that reach OpenClaw no longer leave the caller ringing until hangup. Fixes #58231 and #40131. Thanks @KonsultDigital.
|
||||
- Plugins/Voice Call: coalesce concurrent webhook server starts on the same runtime instance, avoiding a second `listen()` bind when overlapping startup paths race. Thanks @education-01.
|
||||
- Plugins/Voice Call: pin voice response sessions to `responseModel` before embedded agent runs, avoiding live-session model switch failures when the global default model differs. Fixes #60118. Thanks @xinbenlv.
|
||||
- Media tools: honor the configured web-fetch SSRF policy for media understanding, image/music/video generation references, and PDF inputs, so explicit RFC2544 opt-ins cover WebChat OSS uploads without weakening defaults. Fixes #71300. (#71321) Thanks @neeravmakwana.
|
||||
- Agents/TTS: suppress successful spoken transcripts from verbose chat tool output when structured voice media is already queued, while preserving text output for non-builtin tool-name collisions. Fixes #71282. Thanks @neeravmakwana.
|
||||
|
||||
@@ -1002,6 +1002,21 @@ describe("VoiceCallWebhookServer start idempotency", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("supports concurrent start() calls without double-binding the port", async () => {
|
||||
const { manager } = createManager([]);
|
||||
const config = createConfig({ serve: { port: 0, bind: "127.0.0.1", path: "/voice/webhook" } });
|
||||
const server = new VoiceCallWebhookServer(config, manager, provider);
|
||||
|
||||
try {
|
||||
const [firstUrl, secondUrl] = await Promise.all([server.start(), server.start()]);
|
||||
|
||||
expectWebhookUrl(firstUrl, "/voice/webhook");
|
||||
expect(secondUrl).toBe(firstUrl);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("can start again after stop()", async () => {
|
||||
const { manager } = createManager([]);
|
||||
const config = createConfig({ serve: { port: 0, bind: "127.0.0.1", path: "/voice/webhook" } });
|
||||
|
||||
@@ -150,6 +150,7 @@ function buildRealtimeRejectedTwiML(): WebhookResponsePayload {
|
||||
export class VoiceCallWebhookServer {
|
||||
private server: http.Server | null = null;
|
||||
private listeningUrl: string | null = null;
|
||||
private startPromise: Promise<string> | null = null;
|
||||
private config: VoiceCallConfig;
|
||||
private manager: CallManager;
|
||||
private provider: VoiceCallProvider;
|
||||
@@ -443,7 +444,11 @@ export class VoiceCallWebhookServer {
|
||||
await this.initializeMediaStreaming();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.startPromise) {
|
||||
return this.startPromise;
|
||||
}
|
||||
|
||||
this.startPromise = new Promise((resolve, reject) => {
|
||||
this.server = http.createServer((req, res) => {
|
||||
this.handleRequest(req, res, webhookPath).catch((err) => {
|
||||
console.error("[voice-call] Webhook error:", err);
|
||||
@@ -468,11 +473,17 @@ export class VoiceCallWebhookServer {
|
||||
});
|
||||
}
|
||||
|
||||
this.server.on("error", reject);
|
||||
this.server.on("error", (err) => {
|
||||
this.server = null;
|
||||
this.listeningUrl = null;
|
||||
this.startPromise = null;
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.server.listen(port, bind, () => {
|
||||
const url = this.resolveListeningUrl(bind, webhookPath);
|
||||
this.listeningUrl = url;
|
||||
this.startPromise = null;
|
||||
console.log(`[voice-call] Webhook server listening on ${url}`);
|
||||
if (this.mediaStreamHandler) {
|
||||
const address = this.server?.address();
|
||||
@@ -491,6 +502,8 @@ export class VoiceCallWebhookServer {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return this.startPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -502,6 +515,7 @@ export class VoiceCallWebhookServer {
|
||||
}
|
||||
this.pendingDisconnectHangups.clear();
|
||||
this.webhookInFlightLimiter.clear();
|
||||
this.startPromise = null;
|
||||
|
||||
if (this.stopStaleCallReaper) {
|
||||
this.stopStaleCallReaper();
|
||||
|
||||
Reference in New Issue
Block a user