@teamlearners/clawops 0.16.3 → 0.16.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -506,6 +506,14 @@ declare class ClawOpsAgent {
506
506
  private _handleRinging;
507
507
  private _handleFailed;
508
508
  private _onDtmfEvent;
509
+ /**
510
+ * _startCallSession 의 예외를 잡아 control WS 로 call.session_failed 전송한다.
511
+ *
512
+ * OpenAI/Gemini API 키 누락 등 session.start() 단계 실패는 media WS connect 에
513
+ * 도달하지 못해 call-engine 이 30 초간 무음 통화를 유지하게 만든다. 서버에 즉시
514
+ * 알려서 fail-fast 시키고 _activeSessions 에서 정리한다.
515
+ */
516
+ private _safeStartCallSession;
509
517
  private _startCallSession;
510
518
  }
511
519
 
@@ -506,6 +506,14 @@ declare class ClawOpsAgent {
506
506
  private _handleRinging;
507
507
  private _handleFailed;
508
508
  private _onDtmfEvent;
509
+ /**
510
+ * _startCallSession 의 예외를 잡아 control WS 로 call.session_failed 전송한다.
511
+ *
512
+ * OpenAI/Gemini API 키 누락 등 session.start() 단계 실패는 media WS connect 에
513
+ * 도달하지 못해 call-engine 이 30 초간 무음 통화를 유지하게 만든다. 서버에 즉시
514
+ * 알려서 fail-fast 시키고 _activeSessions 에서 정리한다.
515
+ */
516
+ private _safeStartCallSession;
509
517
  private _startCallSession;
510
518
  }
511
519
 
@@ -1,4 +1,4 @@
1
- import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-5IYJOP2D.js';
1
+ import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-BHGDB52N.js';
2
2
  import pino from 'pino';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
@@ -1928,9 +1928,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1928
1928
  this._controlWs.send({ event: "call.accept", callId });
1929
1929
  }
1930
1930
  if (mediaUrl) {
1931
- this._startCallSession(session, mediaUrl).catch((err) => {
1932
- this._log.error({ err }, "Call session error: %s", callId);
1933
- });
1931
+ this._safeStartCallSession(session, mediaUrl, callId);
1934
1932
  }
1935
1933
  }
1936
1934
  _handleEnded(event) {
@@ -1964,9 +1962,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1964
1962
  }
1965
1963
  if (mediaUrl) {
1966
1964
  this._log.info("Outbound call answered: %s -> %s (%s)", this._fromNumber, session.toNumber, callId);
1967
- this._startCallSession(session, mediaUrl).catch((err) => {
1968
- this._log.error({ err }, "Call session error: %s", callId);
1969
- });
1965
+ this._safeStartCallSession(session, mediaUrl, callId);
1970
1966
  }
1971
1967
  }
1972
1968
  _handleRinging(event) {
@@ -2010,6 +2006,32 @@ var ClawOpsAgent = class _ClawOpsAgent {
2010
2006
  }
2011
2007
  }, this._passiveDtmfDebounceMs);
2012
2008
  }
2009
+ /**
2010
+ * _startCallSession 의 예외를 잡아 control WS 로 call.session_failed 전송한다.
2011
+ *
2012
+ * OpenAI/Gemini API 키 누락 등 session.start() 단계 실패는 media WS connect 에
2013
+ * 도달하지 못해 call-engine 이 30 초간 무음 통화를 유지하게 만든다. 서버에 즉시
2014
+ * 알려서 fail-fast 시키고 _activeSessions 에서 정리한다.
2015
+ */
2016
+ _safeStartCallSession(session, mediaWsUrl, callId) {
2017
+ this._startCallSession(session, mediaWsUrl).catch((err) => {
2018
+ const error = err;
2019
+ this._log.error({ err }, "Session start failed for %s", callId);
2020
+ if (this._controlWs) {
2021
+ try {
2022
+ this._controlWs.send({
2023
+ event: "call.session_failed",
2024
+ callId,
2025
+ reason: error?.name ?? "Error",
2026
+ message: error?.message ?? String(err)
2027
+ });
2028
+ } catch {
2029
+ }
2030
+ }
2031
+ this._activeSessions.delete(callId);
2032
+ this._callSessions.delete(callId);
2033
+ });
2034
+ }
2013
2035
  async _startCallSession(session, mediaWsUrl) {
2014
2036
  await withSpan(
2015
2037
  "clawops.call_session",
@@ -2633,6 +2655,11 @@ var OpenAIRealtime = class {
2633
2655
  _holdAudioChunks = null;
2634
2656
  constructor(options = {}) {
2635
2657
  this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
2658
+ if (!this._apiKey) {
2659
+ throw new Error(
2660
+ "OpenAI API key is required. Set OPENAI_API_KEY env var or pass apiKey option."
2661
+ );
2662
+ }
2636
2663
  this._systemPrompt = options.systemPrompt ?? "";
2637
2664
  this._model = options.model ?? "gpt-realtime-2";
2638
2665
  this._voice = options.voice ?? "marin";
@@ -2658,15 +2685,11 @@ var OpenAIRealtime = class {
2658
2685
  this._closed = false;
2659
2686
  this._playback = null;
2660
2687
  this._latestMediaTs = 0;
2661
- if (!this._apiKey) {
2662
- throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey option.");
2663
- }
2664
2688
  const { WebSocket } = await import('ws');
2665
2689
  const url = `${OPENAI_REALTIME_URL}${this._model}`;
2666
2690
  this._ws = new WebSocket(url, {
2667
2691
  headers: {
2668
- Authorization: `Bearer ${this._apiKey}`,
2669
- "OpenAI-Beta": "realtime=v1"
2692
+ Authorization: `Bearer ${this._apiKey}`
2670
2693
  }
2671
2694
  });
2672
2695
  return new Promise((resolve, reject) => {
@@ -2736,17 +2759,24 @@ var OpenAIRealtime = class {
2736
2759
  this._send({
2737
2760
  type: "session.update",
2738
2761
  session: {
2739
- modalities: ["text", "audio"],
2740
- voice: this._voice,
2762
+ type: "realtime",
2763
+ output_modalities: ["audio"],
2741
2764
  instructions: this._systemPrompt,
2742
- input_audio_format: "g711_ulaw",
2743
- output_audio_format: "g711_ulaw",
2744
- input_audio_transcription: {
2745
- model: "whisper-1",
2746
- language: this._language
2765
+ audio: {
2766
+ input: {
2767
+ format: { type: "audio/pcmu" },
2768
+ noise_reduction: { type: "far_field" },
2769
+ transcription: {
2770
+ model: "gpt-realtime-whisper",
2771
+ language: this._language
2772
+ },
2773
+ turn_detection: this._turnDetection
2774
+ },
2775
+ output: {
2776
+ format: { type: "audio/pcmu" },
2777
+ voice: this._voice
2778
+ }
2747
2779
  },
2748
- input_audio_noise_reduction: { type: "far_field" },
2749
- turn_detection: this._turnDetection,
2750
2780
  tools: toolSchemas
2751
2781
  }
2752
2782
  });
@@ -2754,11 +2784,11 @@ var OpenAIRealtime = class {
2754
2784
  _handleMessage(msg) {
2755
2785
  const type = msg["type"];
2756
2786
  switch (type) {
2757
- case "response.audio.delta": {
2787
+ case "response.output_audio.delta": {
2758
2788
  this._handleAudioDelta(msg);
2759
2789
  break;
2760
2790
  }
2761
- case "response.audio.done": {
2791
+ case "response.output_audio.done": {
2762
2792
  if (this._playback) {
2763
2793
  this._playback.generating = false;
2764
2794
  if (this._playback.audioRemainder.length > 0) {
@@ -2792,7 +2822,7 @@ var OpenAIRealtime = class {
2792
2822
  }
2793
2823
  break;
2794
2824
  }
2795
- case "response.audio_transcript.done": {
2825
+ case "response.output_audio_transcript.done": {
2796
2826
  if (this._call) {
2797
2827
  this._call._emit("transcript", "assistant", msg["transcript"] ?? "");
2798
2828
  }