@coclaw/openclaw-coclaw 0.13.0 → 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.0",
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;
@@ -96,6 +109,22 @@ export class WebRtcPeer {
96
109
  if (isIceRestart) {
97
110
  const existing = this.__sessions.get(connId);
98
111
  if (existing) {
112
+ // 仅已验证支持 ICE restart 的 impl 放行,其余立即 reject 让 UI 走 rebuild
113
+ if (this.__impl !== 'pion') {
114
+ this.__remoteLog(`rtc.ice-restart-unsupported conn=${connId} impl=${this.__impl}`);
115
+ this.logger.info?.(`${this.__rtcTag} ICE restart rejected: impl=${this.__impl} not verified`);
116
+ this.__onSend({
117
+ type: 'rtc:restart-rejected',
118
+ toConnId: connId,
119
+ payload: { reason: 'impl_unsupported' },
120
+ });
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;
127
+ }
99
128
  this.__remoteLog(`rtc.ice-restart conn=${connId}`);
100
129
  this.logger.info?.(`${this.__rtcTag} ICE restart offer from ${connId}, renegotiating`);
101
130
  try {
@@ -144,6 +173,11 @@ export class WebRtcPeer {
144
173
  await this.closeByConnId(connId);
145
174
  }
146
175
 
176
+ // session 总数限制:溢出时淘汰最旧的 failed session
177
+ if (this.__sessions.size >= MAX_SESSIONS) {
178
+ this.__evictOldestFailed();
179
+ }
180
+
147
181
  // 从 Server 注入的 turnCreds 构建 iceServers
148
182
  // werift 的 urls 必须是单个 string,每个 URL 独立一个对象
149
183
  const iceServers = [];
@@ -211,6 +245,12 @@ export class WebRtcPeer {
211
245
  const cur = this.__sessions.get(connId);
212
246
  if (!cur || cur.pc !== pc) return;
213
247
 
248
+ // 离开 failed 状态时清理 TTL timer(ICE restart 恢复、自然关闭等)
249
+ if (state !== 'failed' && cur.__failedTimer) {
250
+ clearTimeout(cur.__failedTimer);
251
+ cur.__failedTimer = null;
252
+ }
253
+
214
254
  if (state === 'connected') {
215
255
  // 重置 dump 去重水位(disconnected → connected → disconnected 仍能再 dump)
216
256
  cur.__lastDumpState = null;
@@ -227,16 +267,25 @@ export class WebRtcPeer {
227
267
  // pion: pair 通过独立的 selectedcandidatepairchange 事件上报
228
268
  } else if (state === 'disconnected' || state === 'failed' || state === 'closed') {
229
269
  // 诊断 dump:失败/断连/关闭时输出当前 PC 上 DC 状态,定位"PC 假活/DC 死"现象
230
- // - closed 多由本地主动关闭触发,dump 收敛诊断噪声但仍清理 session
270
+ // - closed 由 closeByConnId 接管清理,dump 收敛诊断噪声
231
271
  // - disconnected 可能反复触发,去重避免噪声
232
272
  if (state !== 'closed' && cur.__lastDumpState !== state) {
233
273
  cur.__lastDumpState = state;
234
274
  this.__dumpSessionState(connId, cur, state);
235
275
  }
236
- // closed 删除 session;failed 保留以支持 ICE restart 恢复
237
- // (如 app 后台冻结 pion ICE failed 前台恢复后 restart)
238
- if (state === 'closed') {
239
- 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(() => {});
240
289
  }
241
290
  }
242
291
  };
@@ -287,6 +336,10 @@ export class WebRtcPeer {
287
336
  // SDP 协商失败 → 清理已入 Map 的 session,避免泄漏
288
337
  const cur = this.__sessions.get(connId);
289
338
  if (cur && cur.pc === pc) {
339
+ if (cur.__failedTimer) {
340
+ clearTimeout(cur.__failedTimer);
341
+ cur.__failedTimer = null;
342
+ }
290
343
  this.__sessions.delete(connId);
291
344
  }
292
345
  await pc.close().catch(() => {});
@@ -393,9 +446,25 @@ export class WebRtcPeer {
393
446
  remoteLog(this.__impl ? `${msg} rtc=${this.__impl}` : msg);
394
447
  }
395
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
+
396
463
  __logDebug(message) {
397
464
  if (typeof this.logger?.debug === 'function') {
398
465
  this.logger.debug(`${this.__rtcTag} ${message}`);
399
466
  }
400
467
  }
401
468
  }
469
+
470
+ export { FAILED_SESSION_TTL_MS, MAX_SESSIONS };