@teamlearners/clawops 0.30.0 → 0.32.0

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.
@@ -1,5 +1,5 @@
1
- import { F as FunctionTool, S as Session, B as BuiltinTool, C as CallSession, a as STT, L as LLM, T as TTS, b as ToolRegistry, c as SessionTelemetry, d as SpeechEvent, e as ConversationMessage, f as LLMChunk } from '../base-Cje1TDJi.cjs';
2
- export { g as CallDirection, h as CallStatus, O as OpenAIToolDefinition, i as ToolCallRequest, j as functionTool, z as zodToToolParams } from '../base-Cje1TDJi.cjs';
1
+ import { F as FunctionTool, S as Session, B as BuiltinTool, C as CallSession, a as STT, L as LLM, T as TTS, b as ToolRegistry, c as SessionTelemetry, d as SpeechEvent, e as ConversationMessage, f as LLMChunk } from '../base-B6S9WQRN.cjs';
2
+ export { g as CallDirection, h as CallStatus, O as OpenAIToolDefinition, i as ToolCallRequest, j as functionTool, z as zodToToolParams } from '../base-B6S9WQRN.cjs';
3
3
  import { Logger } from 'pino';
4
4
  export { Logger } from 'pino';
5
5
  import 'zod';
@@ -164,6 +164,8 @@ declare class ClawOpsAgent {
164
164
  private _recording;
165
165
  private _recordingPath;
166
166
  private _activeSessions;
167
+ /** 미디어 정리를 마친 통화가 서버 종료 프레임을 기다리는 자리. callId → resolve. */
168
+ private _terminalWaiters;
167
169
  private _builtinTools;
168
170
  private _passiveDtmfDebounceMs;
169
171
  private _passiveDtmfBuffer;
@@ -224,6 +226,17 @@ declare class ClawOpsAgent {
224
226
  machineDetection?: 'Enable' | 'Hangup';
225
227
  }): Promise<CallSession>;
226
228
  private _handleIncoming;
229
+ /**
230
+ * 서버 종료 프레임을 짧게 기다린다. 미디어 정리 직후에만 부른다.
231
+ *
232
+ * 왜 기다리나: `call_end` 는 인바운드 사용자가 통화 결과를 받는 **유일한 통로**인데, 서버는
233
+ * 미디어 WS 를 먼저 닫고 자원 정리를 마친 뒤에야 control 종료 프레임을 보낸다. 안 기다리면
234
+ * 그 핸들러 안에서 `endedDuration` 은 영영 null 이다.
235
+ *
236
+ * 상한을 다 쓰는 경우는 제어 연결이 죽어 프레임이 아예 안 오는 때뿐이다 — 그 프레임은
237
+ * 예전부터 늘 오던 것이고(이번에 바뀐 건 내용뿐), 정상 경로에서는 밀리초 안에 풀린다.
238
+ */
239
+ private _awaitServerTerminal;
227
240
  private _handleEnded;
228
241
  /**
229
242
  * Drop prewarm bookkeeping for a callId. Used on hangup/failure paths.
@@ -1,5 +1,5 @@
1
- import { F as FunctionTool, S as Session, B as BuiltinTool, C as CallSession, a as STT, L as LLM, T as TTS, b as ToolRegistry, c as SessionTelemetry, d as SpeechEvent, e as ConversationMessage, f as LLMChunk } from '../base-Cje1TDJi.js';
2
- export { g as CallDirection, h as CallStatus, O as OpenAIToolDefinition, i as ToolCallRequest, j as functionTool, z as zodToToolParams } from '../base-Cje1TDJi.js';
1
+ import { F as FunctionTool, S as Session, B as BuiltinTool, C as CallSession, a as STT, L as LLM, T as TTS, b as ToolRegistry, c as SessionTelemetry, d as SpeechEvent, e as ConversationMessage, f as LLMChunk } from '../base-B6S9WQRN.js';
2
+ export { g as CallDirection, h as CallStatus, O as OpenAIToolDefinition, i as ToolCallRequest, j as functionTool, z as zodToToolParams } from '../base-B6S9WQRN.js';
3
3
  import { Logger } from 'pino';
4
4
  export { Logger } from 'pino';
5
5
  import 'zod';
@@ -164,6 +164,8 @@ declare class ClawOpsAgent {
164
164
  private _recording;
165
165
  private _recordingPath;
166
166
  private _activeSessions;
167
+ /** 미디어 정리를 마친 통화가 서버 종료 프레임을 기다리는 자리. callId → resolve. */
168
+ private _terminalWaiters;
167
169
  private _builtinTools;
168
170
  private _passiveDtmfDebounceMs;
169
171
  private _passiveDtmfBuffer;
@@ -224,6 +226,17 @@ declare class ClawOpsAgent {
224
226
  machineDetection?: 'Enable' | 'Hangup';
225
227
  }): Promise<CallSession>;
226
228
  private _handleIncoming;
229
+ /**
230
+ * 서버 종료 프레임을 짧게 기다린다. 미디어 정리 직후에만 부른다.
231
+ *
232
+ * 왜 기다리나: `call_end` 는 인바운드 사용자가 통화 결과를 받는 **유일한 통로**인데, 서버는
233
+ * 미디어 WS 를 먼저 닫고 자원 정리를 마친 뒤에야 control 종료 프레임을 보낸다. 안 기다리면
234
+ * 그 핸들러 안에서 `endedDuration` 은 영영 null 이다.
235
+ *
236
+ * 상한을 다 쓰는 경우는 제어 연결이 죽어 프레임이 아예 안 오는 때뿐이다 — 그 프레임은
237
+ * 예전부터 늘 오던 것이고(이번에 바뀐 건 내용뿐), 정상 경로에서는 밀리초 안에 풀린다.
238
+ */
239
+ private _awaitServerTerminal;
227
240
  private _handleEnded;
228
241
  /**
229
242
  * Drop prewarm bookkeeping for a callId. Used on hangup/failure paths.
@@ -1,6 +1,6 @@
1
- import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-622J6BJ5.js';
2
- import { NOOP_LOGGER, resolveBuiltinTools, createAgentLogger, createPipelineLogger, BufferingCall, attachBuffered, ulawToPcm16, resamplePcm16, getBuiltinToolSchemas, BUILTIN_TOOL_NAMES, executeBuiltinTool, CALL_NOT_READY_RESULT, pcm16ToUlaw, applyUlawGain } from '../chunk-WKGTY7QT.js';
3
- export { BUILTIN_TOOL_NAMES, BuiltinTool, DECODE_TABLE, createAgentLogger, createPipelineLogger, executeBuiltinTool, getBuiltinToolSchemas, isBuiltinTool, pcm16ToUlaw, resamplePcm16, ulawToPcm16 } from '../chunk-WKGTY7QT.js';
1
+ import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-MXJ2LM6R.js';
2
+ import { NOOP_LOGGER, resolveBuiltinTools, createAgentLogger, createPipelineLogger, BufferingCall, attachBuffered, ulawToPcm16, resamplePcm16, getBuiltinToolSchemas, BUILTIN_TOOL_NAMES, executeBuiltinTool, CALL_NOT_READY_RESULT, pcm16ToUlaw, applyUlawGain } from '../chunk-4YLSDMLL.js';
3
+ export { BUILTIN_TOOL_NAMES, BuiltinTool, DECODE_TABLE, createAgentLogger, createPipelineLogger, executeBuiltinTool, getBuiltinToolSchemas, isBuiltinTool, pcm16ToUlaw, resamplePcm16, ulawToPcm16 } from '../chunk-4YLSDMLL.js';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import os from 'os';
@@ -740,6 +740,7 @@ var CallSession = class {
740
740
  metadata;
741
741
  _status;
742
742
  _endedStatus = null;
743
+ _endedDuration = null;
743
744
  _sendAudioFn = null;
744
745
  _clearAudioFn = null;
745
746
  _hangupFn = null;
@@ -799,9 +800,22 @@ var CallSession = class {
799
800
  get endedStatus() {
800
801
  return this._endedStatus;
801
802
  }
803
+ /**
804
+ * 서버가 확정한 통화 시간(초). 통화가 끝나기 전에는 `null`.
805
+ *
806
+ * ⚠️ `duration` 과 다르다 — 그쪽은 SDK 가 로컬 시계로 재는 **경과 시간**이라 통화 중에도
807
+ * 읽히고, 세션이 붙기 전후의 오차를 포함한다. 기록·정산에 쓸 값은 이쪽이다.
808
+ */
809
+ get endedDuration() {
810
+ return this._endedDuration;
811
+ }
802
812
  get duration() {
803
813
  return (Date.now() - this.startTime.getTime()) / 1e3;
804
814
  }
815
+ /** @internal 서버가 통보한 통화 시간을 확정한다. */
816
+ _setEndedDuration(seconds) {
817
+ this._endedDuration = seconds;
818
+ }
805
819
  get metrics() {
806
820
  return this._metrics;
807
821
  }
@@ -929,11 +943,27 @@ var CallSession = class {
929
943
  * directly to a SIP endpoint without going through the PSTN carrier. Requires the
930
944
  * account to have an active `sip_trunk` add-on; otherwise the transfer fails and
931
945
  * the call continues with the AI (result `{ status: 'failed', ... }`).
946
+ *
947
+ * 전환받는 쪽에 표시되는 발신번호는 기본이 **계정 보유번호**(인바운드면 착신 070)다.
948
+ *
949
+ * `callerIdMode: 'original'` 은 인바운드 통화의 **원 발신자 번호를 승계하려는 선호**다.
950
+ * 승계할 수 없는 통화(KCT 직결 인입이 아니거나 국내 번호로 정규화되지 않는 발신번호)면
951
+ * 조용히 계정 보유번호로 내려앉고 **전환은 그대로 성사된다**.
952
+ *
953
+ * `callerId` 는 번호를 직접 주는 **지시**라 성격이 다르다. 허용 범위(계정 보유번호 또는
954
+ * KCT 직결 인입의 원 발신자)를 벗어나면 전환 자체가 실패한다. 둘 다 주면 `callerId` 가
955
+ * 이기고 `callerIdMode` 는 무시된다 — 우선순위 판단은 서버가 한다.
932
956
  */
933
957
  async transfer(to, options) {
934
958
  if (!this._transferFn) {
935
959
  throw new Error("transfer not available");
936
960
  }
961
+ const callerIdMode = options?.callerIdMode;
962
+ if (callerIdMode !== void 0 && callerIdMode !== "account" && callerIdMode !== "original") {
963
+ throw new Error(
964
+ `callerIdMode must be 'account' or 'original', got ${JSON.stringify(callerIdMode)}`
965
+ );
966
+ }
937
967
  return this._transferFn({
938
968
  to,
939
969
  destinationType: options?.destinationType ?? "pstn",
@@ -943,7 +973,9 @@ var CallSession = class {
943
973
  whisper: options?.whisper ?? null,
944
974
  context: options?.context ?? null,
945
975
  callerId: options?.callerId ?? null,
946
- timeout: options?.timeout ?? 30
976
+ timeout: options?.timeout ?? 30,
977
+ // 안 주면 키를 붙이지 않는다 — 구 서버와 기존 동작을 그대로 둔다(additive).
978
+ ...callerIdMode !== void 0 ? { callerIdMode } : {}
947
979
  });
948
980
  }
949
981
  /** Register an event handler. */
@@ -1373,6 +1405,7 @@ var ATTR_CALL_DIRECTION = "clawops.call.direction";
1373
1405
  var ATTR_AGENT_ID = "clawops.agent.id";
1374
1406
 
1375
1407
  // src/agent/agent.ts
1408
+ var TERMINAL_FRAME_GRACE_MS = 2e3;
1376
1409
  var ClawOpsAgent = class _ClawOpsAgent {
1377
1410
  _apiKey;
1378
1411
  _accountId;
@@ -1386,6 +1419,8 @@ var ClawOpsAgent = class _ClawOpsAgent {
1386
1419
  _recording;
1387
1420
  _recordingPath;
1388
1421
  _activeSessions = /* @__PURE__ */ new Map();
1422
+ /** 미디어 정리를 마친 통화가 서버 종료 프레임을 기다리는 자리. callId → resolve. */
1423
+ _terminalWaiters = /* @__PURE__ */ new Map();
1389
1424
  _builtinTools;
1390
1425
  _passiveDtmfDebounceMs;
1391
1426
  _passiveDtmfBuffer = [];
@@ -1535,6 +1570,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1535
1570
  this._controlWs.close();
1536
1571
  this._controlWs = null;
1537
1572
  }
1573
+ for (const wake of [...this._terminalWaiters.values()]) wake();
1538
1574
  for (const session of this._activeSessions.values()) {
1539
1575
  session._markEnded();
1540
1576
  }
@@ -1622,12 +1658,43 @@ var ClawOpsAgent = class _ClawOpsAgent {
1622
1658
  this._safeStartCallSession(session, mediaUrl, callId);
1623
1659
  }
1624
1660
  }
1661
+ /**
1662
+ * 서버 종료 프레임을 짧게 기다린다. 미디어 정리 직후에만 부른다.
1663
+ *
1664
+ * 왜 기다리나: `call_end` 는 인바운드 사용자가 통화 결과를 받는 **유일한 통로**인데, 서버는
1665
+ * 미디어 WS 를 먼저 닫고 자원 정리를 마친 뒤에야 control 종료 프레임을 보낸다. 안 기다리면
1666
+ * 그 핸들러 안에서 `endedDuration` 은 영영 null 이다.
1667
+ *
1668
+ * 상한을 다 쓰는 경우는 제어 연결이 죽어 프레임이 아예 안 오는 때뿐이다 — 그 프레임은
1669
+ * 예전부터 늘 오던 것이고(이번에 바뀐 건 내용뿐), 정상 경로에서는 밀리초 안에 풀린다.
1670
+ */
1671
+ async _awaitServerTerminal(session) {
1672
+ if (session.endedDuration !== null) return;
1673
+ await new Promise((resolve) => {
1674
+ const done = () => {
1675
+ clearTimeout(timer);
1676
+ this._terminalWaiters.delete(session.callId);
1677
+ resolve();
1678
+ };
1679
+ const timer = setTimeout(() => {
1680
+ this._log.debug(
1681
+ "\uC11C\uBC84 \uC885\uB8CC \uD504\uB808\uC784\uC774 %dms \uC548\uC5D0 \uC624\uC9C0 \uC54A\uC558\uB2E4 \u2014 endedDuration \uC5C6\uC774 \uC9C4\uD589: %s",
1682
+ TERMINAL_FRAME_GRACE_MS,
1683
+ session.callId
1684
+ );
1685
+ done();
1686
+ }, TERMINAL_FRAME_GRACE_MS);
1687
+ this._terminalWaiters.set(session.callId, done);
1688
+ });
1689
+ }
1625
1690
  _handleEnded(event) {
1626
1691
  const callId = event["callId"];
1627
1692
  const status = event["status"] || "completed";
1693
+ const endedDuration = event["duration"];
1628
1694
  const session = this._activeSessions.get(callId);
1629
1695
  if (session) {
1630
1696
  this._log.info("Call ended (server): %s (status=%s)", callId, status);
1697
+ if (typeof endedDuration === "number") session._setEndedDuration(endedDuration);
1631
1698
  if (status !== "completed") {
1632
1699
  this._log.info("Outbound call not connected: %s (%s)", callId, status);
1633
1700
  session._emit("call_failed", status);
@@ -1635,6 +1702,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1635
1702
  session._markEnded(status);
1636
1703
  this._activeSessions.delete(callId);
1637
1704
  }
1705
+ this._terminalWaiters.get(callId)?.();
1638
1706
  void this._cleanupPrewarm(callId);
1639
1707
  }
1640
1708
  /**
@@ -1989,6 +2057,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1989
2057
  this._controlWs?.send({ event: "call.metrics", callId: session.callId, metrics: session.metrics });
1990
2058
  } catch {
1991
2059
  }
2060
+ await this._awaitServerTerminal(session);
1992
2061
  session._emit("call_end");
1993
2062
  session._markEnded();
1994
2063
  this._activeSessions.delete(session.callId);