@teamlearners/clawops 0.31.0 → 0.32.1

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.
package/README.md CHANGED
@@ -480,7 +480,7 @@ const client = new ClawOps({
480
480
 
481
481
  | 언어 | 패키지 | 저장소 |
482
482
  |------|--------|--------|
483
- | Python | [`clawops`](https://pypi.org/project/clawops/) | [clawops-python](https://github.com/learners-superpumped/clawops-python) |
483
+ | Python | [`clawops`](https://pypi.org/project/clawops/) | [sdk-python](https://github.com/clawopshq/sdk-python) |
484
484
 
485
485
  ## 요구사항
486
486
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkKHEU4DKD_cjs = require('../chunk-KHEU4DKD.cjs');
3
+ var chunk6ABL54D2_cjs = require('../chunk-6ABL54D2.cjs');
4
4
  var chunkQD2ZY4UP_cjs = require('../chunk-QD2ZY4UP.cjs');
5
5
  var fs = require('fs');
6
6
  var path = require('path');
@@ -728,7 +728,7 @@ var MAX_ERROR_MESSAGE_LENGTH = 200;
728
728
  function getSdkInfo() {
729
729
  return {
730
730
  name: "clawops-node",
731
- version: chunkKHEU4DKD_cjs.VERSION,
731
+ version: chunk6ABL54D2_cjs.VERSION,
732
732
  runtime: `node/${process.versions.node}`,
733
733
  os: `${process.platform}/${os__default.default.arch()}`
734
734
  };
@@ -765,6 +765,7 @@ var CallSession = class {
765
765
  metadata;
766
766
  _status;
767
767
  _endedStatus = null;
768
+ _endedDuration = null;
768
769
  _sendAudioFn = null;
769
770
  _clearAudioFn = null;
770
771
  _hangupFn = null;
@@ -824,9 +825,22 @@ var CallSession = class {
824
825
  get endedStatus() {
825
826
  return this._endedStatus;
826
827
  }
828
+ /**
829
+ * 서버가 확정한 통화 시간(초). 통화가 끝나기 전에는 `null`.
830
+ *
831
+ * ⚠️ `duration` 과 다르다 — 그쪽은 SDK 가 로컬 시계로 재는 **경과 시간**이라 통화 중에도
832
+ * 읽히고, 세션이 붙기 전후의 오차를 포함한다. 기록·정산에 쓸 값은 이쪽이다.
833
+ */
834
+ get endedDuration() {
835
+ return this._endedDuration;
836
+ }
827
837
  get duration() {
828
838
  return (Date.now() - this.startTime.getTime()) / 1e3;
829
839
  }
840
+ /** @internal 서버가 통보한 통화 시간을 확정한다. */
841
+ _setEndedDuration(seconds) {
842
+ this._endedDuration = seconds;
843
+ }
830
844
  get metrics() {
831
845
  return this._metrics;
832
846
  }
@@ -1416,6 +1430,7 @@ var ATTR_CALL_DIRECTION = "clawops.call.direction";
1416
1430
  var ATTR_AGENT_ID = "clawops.agent.id";
1417
1431
 
1418
1432
  // src/agent/agent.ts
1433
+ var TERMINAL_FRAME_GRACE_MS = 2e3;
1419
1434
  var ClawOpsAgent = class _ClawOpsAgent {
1420
1435
  _apiKey;
1421
1436
  _accountId;
@@ -1429,6 +1444,8 @@ var ClawOpsAgent = class _ClawOpsAgent {
1429
1444
  _recording;
1430
1445
  _recordingPath;
1431
1446
  _activeSessions = /* @__PURE__ */ new Map();
1447
+ /** 미디어 정리를 마친 통화가 서버 종료 프레임을 기다리는 자리. callId → resolve. */
1448
+ _terminalWaiters = /* @__PURE__ */ new Map();
1432
1449
  _builtinTools;
1433
1450
  _passiveDtmfDebounceMs;
1434
1451
  _passiveDtmfBuffer = [];
@@ -1451,7 +1468,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1451
1468
  constructor(options) {
1452
1469
  this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
1453
1470
  this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
1454
- this._baseUrl = options.baseUrl ?? chunkKHEU4DKD_cjs.DEFAULT_BASE_URL;
1471
+ this._baseUrl = options.baseUrl ?? chunk6ABL54D2_cjs.DEFAULT_BASE_URL;
1455
1472
  this._fromNumber = options.from;
1456
1473
  this._session = options.session;
1457
1474
  this._recording = options.recording ?? false;
@@ -1475,7 +1492,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1475
1492
  }
1476
1493
  static _validateGain(name, gain) {
1477
1494
  if (typeof gain !== "number" || !Number.isFinite(gain) || gain < 0) {
1478
- throw new chunkKHEU4DKD_cjs.AgentError(`${name}=${gain} must be a finite number >= 0`);
1495
+ throw new chunk6ABL54D2_cjs.AgentError(`${name}=${gain} must be a finite number >= 0`);
1479
1496
  }
1480
1497
  return gain;
1481
1498
  }
@@ -1489,7 +1506,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1489
1506
  tool(nameOrTool, description, parameters, handler) {
1490
1507
  if (typeof nameOrTool === "string") {
1491
1508
  if (!description || !parameters || !handler) {
1492
- throw new chunkKHEU4DKD_cjs.AgentError(
1509
+ throw new chunk6ABL54D2_cjs.AgentError(
1493
1510
  "tool(name, description, parameters, handler) requires all arguments."
1494
1511
  );
1495
1512
  }
@@ -1525,10 +1542,10 @@ var ClawOpsAgent = class _ClawOpsAgent {
1525
1542
  async connect() {
1526
1543
  if (this._controlWs) return;
1527
1544
  if (!this._apiKey) {
1528
- throw new chunkKHEU4DKD_cjs.AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1545
+ throw new chunk6ABL54D2_cjs.AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1529
1546
  }
1530
1547
  if (!this._accountId) {
1531
- throw new chunkKHEU4DKD_cjs.AgentError(
1548
+ throw new chunk6ABL54D2_cjs.AgentError(
1532
1549
  "Account ID is required. Set CLAWOPS_ACCOUNT_ID or pass accountId option."
1533
1550
  );
1534
1551
  }
@@ -1552,7 +1569,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1552
1569
  } catch {
1553
1570
  }
1554
1571
  } catch (err) {
1555
- throw new chunkKHEU4DKD_cjs.AgentConnectionError(
1572
+ throw new chunk6ABL54D2_cjs.AgentConnectionError(
1556
1573
  `Failed to connect to ClawOps: ${err instanceof Error ? err.message : String(err)}`
1557
1574
  );
1558
1575
  }
@@ -1578,6 +1595,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1578
1595
  this._controlWs.close();
1579
1596
  this._controlWs = null;
1580
1597
  }
1598
+ for (const wake of [...this._terminalWaiters.values()]) wake();
1581
1599
  for (const session of this._activeSessions.values()) {
1582
1600
  session._markEnded();
1583
1601
  }
@@ -1616,7 +1634,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1616
1634
  });
1617
1635
  if (resp.status !== 201) {
1618
1636
  const error = await resp.json();
1619
- throw new chunkKHEU4DKD_cjs.AgentError(`\uBC1C\uC2E0 \uC2E4\uD328 (${resp.status}): ${error["error"] ?? ""}`);
1637
+ throw new chunk6ABL54D2_cjs.AgentError(`\uBC1C\uC2E0 \uC2E4\uD328 (${resp.status}): ${error["error"] ?? ""}`);
1620
1638
  }
1621
1639
  const data = await resp.json();
1622
1640
  const callSession = new CallSession({
@@ -1665,12 +1683,43 @@ var ClawOpsAgent = class _ClawOpsAgent {
1665
1683
  this._safeStartCallSession(session, mediaUrl, callId);
1666
1684
  }
1667
1685
  }
1686
+ /**
1687
+ * 서버 종료 프레임을 짧게 기다린다. 미디어 정리 직후에만 부른다.
1688
+ *
1689
+ * 왜 기다리나: `call_end` 는 인바운드 사용자가 통화 결과를 받는 **유일한 통로**인데, 서버는
1690
+ * 미디어 WS 를 먼저 닫고 자원 정리를 마친 뒤에야 control 종료 프레임을 보낸다. 안 기다리면
1691
+ * 그 핸들러 안에서 `endedDuration` 은 영영 null 이다.
1692
+ *
1693
+ * 상한을 다 쓰는 경우는 제어 연결이 죽어 프레임이 아예 안 오는 때뿐이다 — 그 프레임은
1694
+ * 예전부터 늘 오던 것이고(이번에 바뀐 건 내용뿐), 정상 경로에서는 밀리초 안에 풀린다.
1695
+ */
1696
+ async _awaitServerTerminal(session) {
1697
+ if (session.endedDuration !== null) return;
1698
+ await new Promise((resolve) => {
1699
+ const done = () => {
1700
+ clearTimeout(timer);
1701
+ this._terminalWaiters.delete(session.callId);
1702
+ resolve();
1703
+ };
1704
+ const timer = setTimeout(() => {
1705
+ this._log.debug(
1706
+ "\uC11C\uBC84 \uC885\uB8CC \uD504\uB808\uC784\uC774 %dms \uC548\uC5D0 \uC624\uC9C0 \uC54A\uC558\uB2E4 \u2014 endedDuration \uC5C6\uC774 \uC9C4\uD589: %s",
1707
+ TERMINAL_FRAME_GRACE_MS,
1708
+ session.callId
1709
+ );
1710
+ done();
1711
+ }, TERMINAL_FRAME_GRACE_MS);
1712
+ this._terminalWaiters.set(session.callId, done);
1713
+ });
1714
+ }
1668
1715
  _handleEnded(event) {
1669
1716
  const callId = event["callId"];
1670
1717
  const status = event["status"] || "completed";
1718
+ const endedDuration = event["duration"];
1671
1719
  const session = this._activeSessions.get(callId);
1672
1720
  if (session) {
1673
1721
  this._log.info("Call ended (server): %s (status=%s)", callId, status);
1722
+ if (typeof endedDuration === "number") session._setEndedDuration(endedDuration);
1674
1723
  if (status !== "completed") {
1675
1724
  this._log.info("Outbound call not connected: %s (%s)", callId, status);
1676
1725
  session._emit("call_failed", status);
@@ -1678,6 +1727,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1678
1727
  session._markEnded(status);
1679
1728
  this._activeSessions.delete(callId);
1680
1729
  }
1730
+ this._terminalWaiters.get(callId)?.();
1681
1731
  void this._cleanupPrewarm(callId);
1682
1732
  }
1683
1733
  /**
@@ -2032,6 +2082,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
2032
2082
  this._controlWs?.send({ event: "call.metrics", callId: session.callId, metrics: session.metrics });
2033
2083
  } catch {
2034
2084
  }
2085
+ await this._awaitServerTerminal(session);
2035
2086
  session._emit("call_end");
2036
2087
  session._markEnded();
2037
2088
  this._activeSessions.delete(session.callId);