@coclaw/openclaw-coclaw 0.13.1 → 0.13.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coclaw/openclaw-coclaw",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "OpenClaw CoClaw channel plugin for remote chat",
@@ -5,6 +5,14 @@ import { remoteLog } from '../remote-log.js';
5
5
  // 用于诊断 dump:过大会撑爆 remoteLog 单帧,20 足以覆盖典型多文件传输会话。
6
6
  const FILE_CHANNEL_HISTORY_LIMIT = 20;
7
7
 
8
+ // Failed session 保留 24 小时,支持 Capacitor 长时间后台恢复后 ICE restart。
9
+ // 超时后 session 被回收释放 IPC listeners 和 Go 侧资源。
10
+ const FAILED_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
11
+
12
+ // Session 总数上限(活跃 + failed)。溢出时淘汰最旧的 failed session。
13
+ // 20 足以覆盖多 UI 实例(浏览器多标签 + 移动端)的典型场景。
14
+ const MAX_SESSIONS = 20;
15
+
8
16
  /**
9
17
  * 管理多个 WebRTC PeerConnection(以 connId 为粒度)。
10
18
  * Plugin 作为被叫方:收到 UI 的 offer → 回复 answer。
@@ -55,6 +63,11 @@ export class WebRtcPeer {
55
63
  async closeByConnId(connId) {
56
64
  const session = this.__sessions.get(connId);
57
65
  if (!session) return;
66
+ // 清理 failed TTL 定时器
67
+ if (session.__failedTimer) {
68
+ clearTimeout(session.__failedTimer);
69
+ session.__failedTimer = null;
70
+ }
58
71
  this.__sessions.delete(connId);
59
72
  // 先 detach 事件,防止 pc.close() 异步触发 onconnectionstatechange 删除新 session
60
73
  session.pc.onconnectionstatechange = null;
@@ -105,7 +118,12 @@ export class WebRtcPeer {
105
118
  toConnId: connId,
106
119
  payload: { reason: 'impl_unsupported' },
107
120
  });
108
- return;
121
+ return; // TTL timer 保持不变(reject 是同步的,不影响 timer 正常工作)
122
+ }
123
+ // 暂停 failed TTL timer:pion restart 涉及异步协商,期间不应被回收
124
+ if (existing.__failedTimer) {
125
+ clearTimeout(existing.__failedTimer);
126
+ existing.__failedTimer = null;
109
127
  }
110
128
  this.__remoteLog(`rtc.ice-restart conn=${connId}`);
111
129
  this.logger.info?.(`${this.__rtcTag} ICE restart offer from ${connId}, renegotiating`);
@@ -155,6 +173,11 @@ export class WebRtcPeer {
155
173
  await this.closeByConnId(connId);
156
174
  }
157
175
 
176
+ // session 总数限制:溢出时淘汰最旧的 failed session
177
+ if (this.__sessions.size >= MAX_SESSIONS) {
178
+ this.__evictOldestFailed();
179
+ }
180
+
158
181
  // 从 Server 注入的 turnCreds 构建 iceServers
159
182
  // werift 的 urls 必须是单个 string,每个 URL 独立一个对象
160
183
  const iceServers = [];
@@ -222,6 +245,12 @@ export class WebRtcPeer {
222
245
  const cur = this.__sessions.get(connId);
223
246
  if (!cur || cur.pc !== pc) return;
224
247
 
248
+ // 离开 failed 状态时清理 TTL timer(ICE restart 恢复、自然关闭等)
249
+ if (state !== 'failed' && cur.__failedTimer) {
250
+ clearTimeout(cur.__failedTimer);
251
+ cur.__failedTimer = null;
252
+ }
253
+
225
254
  if (state === 'connected') {
226
255
  // 重置 dump 去重水位(disconnected → connected → disconnected 仍能再 dump)
227
256
  cur.__lastDumpState = null;
@@ -238,16 +267,25 @@ export class WebRtcPeer {
238
267
  // pion: pair 通过独立的 selectedcandidatepairchange 事件上报
239
268
  } else if (state === 'disconnected' || state === 'failed' || state === 'closed') {
240
269
  // 诊断 dump:失败/断连/关闭时输出当前 PC 上 DC 状态,定位"PC 假活/DC 死"现象
241
- // - closed 多由本地主动关闭触发,dump 收敛诊断噪声但仍清理 session
270
+ // - closed 由 closeByConnId 接管清理,dump 收敛诊断噪声
242
271
  // - disconnected 可能反复触发,去重避免噪声
243
272
  if (state !== 'closed' && cur.__lastDumpState !== state) {
244
273
  cur.__lastDumpState = state;
245
274
  this.__dumpSessionState(connId, cur, state);
246
275
  }
247
- // closed 删除 session;failed 保留以支持 ICE restart 恢复
248
- // (如 app 后台冻结 pion ICE failed 前台恢复后 restart)
249
- if (state === 'closed') {
250
- this.__sessions.delete(connId);
276
+ if (state === 'failed') {
277
+ // 启动 TTL 定时器:超时后回收 session 释放 IPC listeners Go 侧资源。
278
+ // unref() 确保定时器不阻止进程退出(gateway 由其他连接保活)。
279
+ if (cur.__failedTimer) clearTimeout(cur.__failedTimer);
280
+ cur.__failedTimer = setTimeout(() => {
281
+ this.__remoteLog(`rtc.session-expired conn=${connId} ttl=${FAILED_SESSION_TTL_MS / 1000}s`);
282
+ this.logger.info?.(`${this.__rtcTag} [${connId}] session TTL expired, closing`);
283
+ this.closeByConnId(connId).catch(() => {});
284
+ }, FAILED_SESSION_TTL_MS);
285
+ cur.__failedTimer.unref?.();
286
+ } else if (state === 'closed') {
287
+ // 自然进入 closed 时也需通过 closeByConnId 释放 IPC listeners 和 Go 资源
288
+ this.closeByConnId(connId).catch(() => {});
251
289
  }
252
290
  }
253
291
  };
@@ -298,6 +336,10 @@ export class WebRtcPeer {
298
336
  // SDP 协商失败 → 清理已入 Map 的 session,避免泄漏
299
337
  const cur = this.__sessions.get(connId);
300
338
  if (cur && cur.pc === pc) {
339
+ if (cur.__failedTimer) {
340
+ clearTimeout(cur.__failedTimer);
341
+ cur.__failedTimer = null;
342
+ }
301
343
  this.__sessions.delete(connId);
302
344
  }
303
345
  await pc.close().catch(() => {});
@@ -404,9 +446,25 @@ export class WebRtcPeer {
404
446
  remoteLog(this.__impl ? `${msg} rtc=${this.__impl}` : msg);
405
447
  }
406
448
 
449
+ /** 淘汰最旧的 failed session(Map 迭代序 ≈ 创建时间序),用于 queue length 限制 */
450
+ __evictOldestFailed() {
451
+ for (const [connId, session] of this.__sessions) {
452
+ if (session.pc.connectionState === 'failed') {
453
+ this.__remoteLog(`rtc.session-evicted conn=${connId} sessions=${this.__sessions.size}`);
454
+ this.logger.info?.(`${this.__rtcTag} [${connId}] session evicted (limit ${MAX_SESSIONS}), closing`);
455
+ this.closeByConnId(connId).catch(() => {});
456
+ return true;
457
+ }
458
+ }
459
+ this.logger.warn?.(`${this.__rtcTag} session limit (${MAX_SESSIONS}) reached, no failed sessions to evict`);
460
+ return false;
461
+ }
462
+
407
463
  __logDebug(message) {
408
464
  if (typeof this.logger?.debug === 'function') {
409
465
  this.logger.debug(`${this.__rtcTag} ${message}`);
410
466
  }
411
467
  }
412
468
  }
469
+
470
+ export { FAILED_SESSION_TTL_MS, MAX_SESSIONS };