@coclaw/openclaw-coclaw 0.14.0 → 0.14.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/index.js CHANGED
@@ -14,10 +14,77 @@ import { AutoUpgradeScheduler } from './src/auto-upgrade/updater.js';
14
14
  import { getPackageInfo } from './src/auto-upgrade/updater-check.js';
15
15
  import { createFileHandler } from './src/file-manager/handler.js';
16
16
  import { abortAgentRun } from './src/agent-abort.js';
17
+ import { remoteLog } from './src/remote-log.js';
17
18
 
18
19
  import { getPluginVersion, __resetPluginVersion } from './src/plugin-version.js';
19
20
  export { getPluginVersion, __resetPluginVersion };
20
21
 
22
+ // 侧门注册表观测:patch OpenClaw embeddedRunState.activeRuns 的 set/delete,
23
+ // 用于跟踪 sessionId 何时注册/注销(agent 取消流程实际读取的就是这张表)。
24
+ // OpenClaw 侧门形状变化时(缺失 / 抛异常),通过 remoteLog 上报为升级契约变更的早期信号。
25
+ const PATCH_LABELS = [
26
+ ['embedded.activeRuns', () => globalThis[Symbol.for('openclaw.embeddedRunState')]?.activeRuns],
27
+ ];
28
+
29
+ function installAbortRegistryDiag(logger) {
30
+ const installed = [];
31
+ const missing = [];
32
+ try {
33
+ for (const [label, resolve] of PATCH_LABELS) {
34
+ if (patchMapLogging(resolve(), label, logger)) installed.push(label);
35
+ else missing.push(label);
36
+ }
37
+ }
38
+ catch (err) {
39
+ logger?.warn?.(`[coclaw.diag] installAbortRegistryDiag failed: ${String(err?.message ?? err)}`);
40
+ remoteLog(`abort.patch-failed reason=${String(err?.message ?? err)}`);
41
+ return;
42
+ }
43
+ remoteLog(`abort.patch installed=${installed.join(',') || 'none'} missing=${missing.join(',') || 'none'}`);
44
+ }
45
+
46
+ function patchMapLogging(map, label, logger) {
47
+ if (!map || typeof map.set !== 'function' || typeof map.delete !== 'function') return false;
48
+ if (map.__coclawDiagPatched) return true;
49
+ // 先打 idempotent 标记:若 map 是 frozen/sealed/Proxy 致 defineProperty 抛,
50
+ // 立即返回 false 让上层归入 missing;不留下半装好的 wrapper 状态
51
+ try {
52
+ Object.defineProperty(map, '__coclawDiagPatched', { value: true, enumerable: false });
53
+ }
54
+ catch (err) {
55
+ logger?.warn?.(`[coclaw.diag] cannot mark ${label} patched: ${String(err?.message ?? err)}`);
56
+ return false;
57
+ }
58
+ const origSet = map.set.bind(map);
59
+ const origDel = map.delete.bind(map);
60
+ // log 行包 try/catch 兜底:上游若把 Map 换成有 throwing getter(如 Proxy)的对象,
61
+ // 不能让本插件的诊断 log 把 OpenClaw 内部 set/delete 流程带崩
62
+ const safeLog = (msg) => {
63
+ try { logger?.info?.(msg); } catch { /* swallow — diag log 不得影响主流程 */ }
64
+ };
65
+ const safeSize = () => {
66
+ try { return map.size; } catch { return '?'; }
67
+ };
68
+ map.set = (key, value) => {
69
+ const res = origSet(key, value);
70
+ safeLog(`[coclaw.diag] ${label}.set key=${stringifyKey(key)} size=${safeSize()}`);
71
+ return res;
72
+ };
73
+ map.delete = (key) => {
74
+ let had;
75
+ try { had = map.has(key); } catch { had = '?'; }
76
+ const res = origDel(key);
77
+ safeLog(`[coclaw.diag] ${label}.delete key=${stringifyKey(key)} had=${had} size=${safeSize()}`);
78
+ return res;
79
+ };
80
+ return true;
81
+ }
82
+
83
+ function stringifyKey(k) {
84
+ if (typeof k === 'string') return k;
85
+ try { return JSON.stringify(k); } catch { return String(k); }
86
+ }
87
+
21
88
  /* c8 ignore start */
22
89
  function parseCommandArgs(args) {
23
90
  const tokens = (args ?? '').split(/\s+/).filter(Boolean);
@@ -65,6 +132,7 @@ const plugin = {
65
132
  register(api) {
66
133
  setRuntime(api.runtime);
67
134
  const logger = api?.logger ?? console;
135
+ installAbortRegistryDiag(logger);
68
136
  const manager = createSessionManager({ logger });
69
137
  const topicManager = new TopicManager({ logger });
70
138
  const chatHistoryManager = new ChatHistoryManager({ logger });
@@ -460,6 +528,7 @@ const plugin = {
460
528
 
461
529
  // 取消正在执行的 embedded agent run(通过 OpenClaw 全局 symbol 侧门)
462
530
  // 侧门不存在 / sessionId 未注册 / handle.abort 抛异常时返回 { ok:false, reason } —— UI 静默降级
531
+ // UI 可能在 OpenClaw 注册 sessionId 前点 STOP(注册空窗期),此时返回 not-found;UI 会按 500ms 间隔重试。
463
532
  api.registerGatewayMethod('coclaw.agent.abort', ({ params, respond }) => {
464
533
  try {
465
534
  const sessionId = params?.sessionId;
@@ -468,9 +537,18 @@ const plugin = {
468
537
  respondInvalid(respond, 'sessionId is required');
469
538
  return;
470
539
  }
471
- logger.info?.(`[coclaw.agent.abort] request sessionId=${sessionId}`);
472
- const result = abortAgentRun(sessionId, logger);
473
- logger.info?.(`[coclaw.agent.abort] result sessionId=${sessionId} ok=${result.ok}${result.reason ? ` reason=${result.reason}` : ''}${result.error ? ` error=${result.error}` : ''}`);
540
+ const result = abortAgentRun(sessionId);
541
+ // not-found UI 重试期常态(注册空窗),不打日志避免噪音;其余分支保留 info
542
+ if (result.reason !== 'not-found') {
543
+ logger.info?.(`[coclaw.agent.abort] result sessionId=${sessionId} ok=${result.ok}${result.reason ? ` reason=${result.reason}` : ''}${result.error ? ` error=${result.error}` : ''}`);
544
+ }
545
+ if (result.ok) {
546
+ remoteLog(`abort.success sid=${sessionId}`);
547
+ }
548
+ else if (result.reason === 'not-supported') {
549
+ // 侧门缺失或 handle shape 变化:OpenClaw 升级契约变更的早期信号
550
+ remoteLog(`abort.not-supported sid=${sessionId}`);
551
+ }
474
552
  respond(true, result);
475
553
  }
476
554
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coclaw/openclaw-coclaw",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "OpenClaw CoClaw channel plugin for remote chat",
@@ -9,91 +9,20 @@
9
9
  */
10
10
 
11
11
  const EMBEDDED_RUN_STATE_KEY = Symbol.for('openclaw.embeddedRunState');
12
- const REPLY_RUN_STATE_KEY = Symbol.for('openclaw.replyRunRegistry');
13
-
14
- /* c8 ignore start */ // 临时诊断代码,定位完问题会删除
15
- /**
16
- * 诊断用:从 reply-run-registry 全局单例中解析 sessionId → sessionKey 映射及概览
17
- * @param {string} sessionId
18
- * @returns {string}
19
- */
20
- function describeReplyRunRegistry(sessionId) {
21
- const state = globalThis[REPLY_RUN_STATE_KEY];
22
- if (!state) return 'reply.state=absent';
23
- const parts = [];
24
- const runs = state.activeRunsByKey;
25
- if (runs && typeof runs.size === 'number') {
26
- parts.push(`reply.activeRunsByKey.size=${runs.size}`);
27
- try {
28
- const ks = [];
29
- if (typeof runs.keys === 'function') {
30
- for (const k of runs.keys()) {
31
- ks.push(k);
32
- if (ks.length >= 10) break;
33
- }
34
- }
35
- parts.push(`reply.keys=${JSON.stringify(ks)}`);
36
- }
37
- catch (e) {
38
- parts.push(`reply.keysErr=${String(e?.message ?? e)}`);
39
- }
40
- }
41
- else {
42
- parts.push('reply.activeRunsByKey=absent');
43
- }
44
- const byId = state.activeKeysBySessionId;
45
- if (byId && typeof byId.get === 'function') {
46
- try {
47
- const mapped = byId.get(sessionId);
48
- parts.push(`reply.keyForSid=${mapped === undefined ? 'null' : JSON.stringify(mapped)}`);
49
- }
50
- catch (e) {
51
- parts.push(`reply.keyForSidErr=${String(e?.message ?? e)}`);
52
- }
53
- }
54
- else {
55
- parts.push('reply.activeKeysBySessionId=absent');
56
- }
57
- return parts.join(' ');
58
- }
59
- /* c8 ignore stop */
60
12
 
61
13
  /**
62
14
  * 尝试取消 sessionId 对应的 embedded agent run
63
15
  * @param {string} sessionId
64
- * @param {{ info?: Function }} [logger] - 可选 logger;传入时在 not-found 分支 dump activeRuns 诊断信息
65
16
  * @returns {{ ok: true } | { ok: false, reason: 'not-supported' | 'not-found' | 'abort-threw', error?: string }}
66
17
  */
67
- export function abortAgentRun(sessionId, logger) {
18
+ export function abortAgentRun(sessionId) {
68
19
  const state = globalThis[EMBEDDED_RUN_STATE_KEY];
69
20
  if (!state || !state.activeRuns || typeof state.activeRuns.get !== 'function') {
70
21
  return { ok: false, reason: 'not-supported' };
71
22
  }
72
23
  try {
73
24
  const handle = state.activeRuns.get(sessionId);
74
- if (!handle) {
75
- /* c8 ignore start */ // 临时诊断代码,定位完问题会删除
76
- if (logger?.info) {
77
- let diag = `sessionId=${sessionId} embedded.size=${state.activeRuns.size ?? '?'}`;
78
- try {
79
- const ks = [];
80
- if (typeof state.activeRuns.keys === 'function') {
81
- for (const k of state.activeRuns.keys()) {
82
- ks.push(k);
83
- if (ks.length >= 10) break;
84
- }
85
- }
86
- diag += ` embedded.keys=${JSON.stringify(ks)}`;
87
- }
88
- catch (e) {
89
- diag += ` embedded.keysErr=${String(e?.message ?? e)}`;
90
- }
91
- diag += ` ${describeReplyRunRegistry(sessionId)}`;
92
- logger.info(`[coclaw.agent.abort] not-found diag ${diag}`);
93
- }
94
- /* c8 ignore stop */
95
- return { ok: false, reason: 'not-found' };
96
- }
25
+ if (!handle) return { ok: false, reason: 'not-found' };
97
26
  // shape 守卫:abort 字段应为函数;若不是说明 OpenClaw handle 契约变化(归入 not-supported 让 UI 提示升级)
98
27
  if (typeof handle.abort !== 'function') return { ok: false, reason: 'not-supported' };
99
28
  handle.abort();
@@ -0,0 +1,60 @@
1
+ import os from 'node:os';
2
+ import process from 'node:process';
3
+
4
+ // 模块级缓存:所有字段在进程生命周期内不变,缓存后 ws 重连补发零开销。
5
+ let __cachedLine = null;
6
+
7
+ /**
8
+ * 以 "key=value" 形式收集运行环境信息,用于诊断平台相关二进制依赖(如 pion-ipc)问题。
9
+ *
10
+ * 尽力而为:每项独立 try/catch,单项失败不影响其它项;无法获取时该字段省略。
11
+ * 结果在进程生命周期内缓存,重复调用零额外开销。
12
+ *
13
+ * 字段:platform / arch / node / osrel / cpu / cores / mem
14
+ *
15
+ * @returns {string} - 形如 `platform=linux arch=x64 node=v20.11.0 osrel=6.6.87 cpu="Intel Xeon" cores=8 mem=16.0GB`
16
+ */
17
+ export function getPlatformInfoLine() {
18
+ if (__cachedLine !== null) return __cachedLine;
19
+ const parts = [];
20
+
21
+ tryPush(parts, 'platform', () => process.platform);
22
+ tryPush(parts, 'arch', () => process.arch);
23
+ tryPush(parts, 'node', () => process.version);
24
+ tryPush(parts, 'osrel', () => os.release());
25
+ tryPush(parts, 'cpu', () => {
26
+ const model = os.cpus()?.[0]?.model;
27
+ if (!model) return undefined;
28
+ // 外层包双引号以保留含空格的 model;内部双引号 / C0 控制字符 / DEL 替换为空格后折叠空白
29
+ const cleaned = String(model).replace(/["\x00-\x1F\x7F]/g, ' ').replace(/\s+/g, ' ').trim();
30
+ if (!cleaned) return undefined;
31
+ return `"${cleaned}"`;
32
+ });
33
+ tryPush(parts, 'cores', () => {
34
+ const n = os.cpus()?.length;
35
+ return n > 0 ? n : undefined;
36
+ });
37
+ tryPush(parts, 'mem', () => {
38
+ const bytes = os.totalmem();
39
+ if (!bytes || !Number.isFinite(bytes)) return undefined;
40
+ return `${(bytes / 1024 ** 3).toFixed(1)}GB`;
41
+ });
42
+
43
+ __cachedLine = parts.join(' ');
44
+ return __cachedLine;
45
+ }
46
+
47
+ /** 测试用:清缓存以便覆盖不同 monkey-patch 场景 */
48
+ export function __resetPlatformInfoCache() {
49
+ __cachedLine = null;
50
+ }
51
+
52
+ function tryPush(parts, key, resolver) {
53
+ try {
54
+ const value = resolver();
55
+ if (value === undefined || value === null || value === '') return;
56
+ parts.push(`${key}=${value}`);
57
+ } catch {
58
+ // 单项失败静默跳过,不影响其它字段
59
+ }
60
+ }
@@ -14,6 +14,7 @@ import {
14
14
  import { getRuntime } from './runtime.js';
15
15
  import { setSender as setRemoteLogSender, remoteLog } from './remote-log.js';
16
16
  import { getPluginVersion } from './plugin-version.js';
17
+ import { getPlatformInfoLine } from './platform-info.js';
17
18
 
18
19
  const DEFAULT_GATEWAY_WS_URL = `ws://127.0.0.1:${process.env.OPENCLAW_GATEWAY_PORT || '18789'}`;
19
20
  const RECONNECT_MS = 10_000;
@@ -811,9 +812,14 @@ export class RealtimeBridge {
811
812
  this.__clearConnectTimer();
812
813
  this.logger.info?.(`[coclaw] realtime bridge connected: ${maskedTarget}`);
813
814
  remoteLog('ws.connected peer=server');
815
+ // 顺序很重要:先注入 sender 再 remoteLog 环境信息——这样环境信息能随当前 sock
816
+ // 立即 flush;同时 sender 内部仅做 sock.send(不回调 remoteLog),无循环依赖。
814
817
  setRemoteLogSender((msg) => {
815
818
  if (sock.readyState === 1) sock.send(JSON.stringify(msg));
816
819
  });
820
+ // ws 重连后补发环境信息:server 重启重连后能立即看到当前 claw 的运行环境与 webrtc 选型。
821
+ // __buildEnvLine 内部所有读取均为缓存值,无 native syscall。
822
+ remoteLog(this.__buildEnvLine());
817
823
  this.__startServerHeartbeat(sock);
818
824
  this.__ensureGatewayConnection();
819
825
  });
@@ -967,12 +973,28 @@ export class RealtimeBridge {
967
973
  this.__ndcPreloadResult = preloadResult;
968
974
  this.__ndcCleanup = preloadResult.cleanup;
969
975
  const implLabel = preloadResult.impl === 'ndc' ? 'node-datachannel(ndc)' : preloadResult.impl;
976
+ this.__implLabel = implLabel; // 缓存供 ws.open 时发送
977
+ // 启动信息只本地 log;远程发送统一由 ws.open 触发,避免重复
970
978
  this.logger.info?.(`[coclaw] WebRTC impl: ${implLabel}`);
971
- remoteLog(`bridge.webrtc-impl impl=${implLabel}`);
972
- remoteLog(`bridge.started version=${this.__pluginVersion}`);
979
+ this.logger.info?.(`[coclaw] ${this.__buildEnvLine()}`);
980
+ remoteLog('bridge.started');
973
981
  await this.__connectIfNeeded();
974
982
  }
975
983
 
984
+ /**
985
+ * 组装一条覆盖最基础环境信息的 log 行:
986
+ * coclaw.env impl=<...> plugin=<ver> openclaw=<ver> platform=<...> ... mem=<...>
987
+ *
988
+ * 字段值均为已缓存的轻量同步读取,无 native syscall;不调用 remoteLog,无循环依赖。
989
+ */
990
+ __buildEnvLine() {
991
+ const rt = getRuntime();
992
+ const openclawVer = (rt?.version && rt.version !== 'unknown') ? rt.version : 'unknown';
993
+ const impl = this.__implLabel ?? 'pending';
994
+ const plugin = this.__pluginVersion ?? 'unknown';
995
+ return `coclaw.env impl=${impl} plugin=${plugin} openclaw=${openclawVer} ${getPlatformInfoLine()}`;
996
+ }
997
+
976
998
  async refresh() {
977
999
  await this.stop();
978
1000
  await this.start({