@coclaw/openclaw-coclaw 0.22.2 → 0.22.3
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 +15 -3
- package/package.json +1 -1
- package/src/chat-history-manager/manager.js +2 -1
- package/src/file-manager/handler.js +5 -2
- package/src/model-default/index.js +3 -0
- package/src/provider-auth/index.js +3 -0
- package/src/realtime-bridge.js +7 -0
- package/src/remote-log.js +5 -0
- package/src/runtime.js +5 -1
- package/src/session-manager/manager.js +3 -9
- package/src/webrtc/webrtc-peer.js +4 -3
package/index.js
CHANGED
|
@@ -23,6 +23,15 @@ import { registerModelDefaultHandlers } from './src/model-default/index.js';
|
|
|
23
23
|
import { getPluginVersion, __resetPluginVersion } from './src/plugin-version.js';
|
|
24
24
|
export { getPluginVersion, __resetPluginVersion };
|
|
25
25
|
|
|
26
|
+
// 收纳 register() 在 full 模式启动的 fire-and-forget 初始化任务(topic / chat-history
|
|
27
|
+
// load + reconcile)的完成信号。默认 Promise.resolve() 让 awaitPluginInit() 在 register
|
|
28
|
+
// 未跑或非 full 模式时立即返回。每次 full register 都重置——多次 register 互不串扰。
|
|
29
|
+
let __pluginInitDone = Promise.resolve();
|
|
30
|
+
|
|
31
|
+
export function awaitPluginInit() {
|
|
32
|
+
return __pluginInitDone;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
// 侧门注册表观测:patch OpenClaw embeddedRunState.activeRuns 的 set/delete,
|
|
27
36
|
// 用于跟踪 sessionId 何时注册/注销(agent 取消流程实际读取的就是这张表)。
|
|
28
37
|
// OpenClaw 侧门形状变化时(缺失 / 抛异常),通过 remoteLog 上报为升级契约变更的早期信号。
|
|
@@ -171,16 +180,19 @@ const plugin = {
|
|
|
171
180
|
const topicManager = new TopicManager({ logger });
|
|
172
181
|
const chatHistoryManager = new ChatHistoryManager({ logger });
|
|
173
182
|
|
|
174
|
-
// 懒加载 topic / chat history 数据(best-effort
|
|
175
|
-
|
|
183
|
+
// 懒加载 topic / chat history 数据(best-effort,不阻断注册)。
|
|
184
|
+
// 两条 promise 收成一个 bundle 挂到 __pluginInitDone,让测试 / 关心 done 时机的
|
|
185
|
+
// caller 通过 awaitPluginInit() 显式等待——生产 gateway 不调即原 fire-and-forget 语义
|
|
186
|
+
const topicLoadP = topicManager.load('main').catch((err) => {
|
|
176
187
|
logger.warn?.(`[coclaw] topic manager load failed: ${String(err?.message ?? err)}`);
|
|
177
188
|
});
|
|
178
|
-
chatHistoryManager.load('main')
|
|
189
|
+
const chatHistoryLoadP = chatHistoryManager.load('main')
|
|
179
190
|
.then(() => manager.listAllEntries('main'))
|
|
180
191
|
.then((entries) => chatHistoryManager.reconcileAll('main', entries))
|
|
181
192
|
.catch((err) => {
|
|
182
193
|
logger.warn?.(`[coclaw] chat history manager load/reconcile failed: ${String(err?.message ?? err)}`);
|
|
183
194
|
});
|
|
195
|
+
__pluginInitDone = Promise.all([topicLoadP, chatHistoryLoadP]);
|
|
184
196
|
|
|
185
197
|
// 追踪 chat 因 reset 产生的 session 流水。双源回调(hook + sessions.changed)共用 helper。
|
|
186
198
|
// recordSessionTransition 内部已 __reloadFromDisk + mutex,外层无需再 cache.has + load。
|
package/package.json
CHANGED
|
@@ -76,7 +76,8 @@ export class ChatHistoryManager {
|
|
|
76
76
|
constructor(opts = {}) {
|
|
77
77
|
this.__resolveSessionsDir = opts.resolveSessionsDir ?? agentSessionsDir;
|
|
78
78
|
this.__logger = opts.logger ?? console;
|
|
79
|
-
|
|
79
|
+
// readFile / writeJsonFile DI 注入点用于精细 mock;不注入时默认走 fs.readFile + atomicWriteJsonFile。
|
|
80
|
+
// 默认构造路径由"通过 setRuntime 端到端落盘"测试覆盖。
|
|
80
81
|
this.__readFile = opts.readFile ?? fs.readFile;
|
|
81
82
|
this.__writeJsonFile = opts.writeJsonFile ?? atomicWriteJsonFile;
|
|
82
83
|
// 内存缓存:agentId -> { version, [sessionKey]: [...] }
|
|
@@ -796,8 +796,10 @@ export function createFileHandler({ resolveWorkspace, logger, deps = {} }) {
|
|
|
796
796
|
// done 已收到但 drain 未完成 — finishUpload 中会检测 dcClosed 并清理 tmp
|
|
797
797
|
if (!finishing) finishUpload();
|
|
798
798
|
} else {
|
|
799
|
+
// 等 ws 关闭后再 unlink——fopen 未完成时直接 safeUnlink 会扑空被吞,
|
|
800
|
+
// 随后 fopen 完成创建文件却没人清,留下孤儿 tmp
|
|
801
|
+
ws.on('close', () => safeUnlink(tmpPath));
|
|
799
802
|
ws.destroy();
|
|
800
|
-
safeUnlink(tmpPath);
|
|
801
803
|
const elapsed = Date.now() - startTime;
|
|
802
804
|
remoteLog(`file.up.fail ${logTag}id=${transferId} reason=dc-closed received=${receivedBytes}/${declaredSize} elapsed=${elapsed}ms bp=${wsBackpressureCount}`);
|
|
803
805
|
log.warn?.(`[coclaw/file] [${connId ?? '?'}] up.fail id=${transferId} reason=dc-closed received=${receivedBytes}/${declaredSize} elapsed=${elapsed}ms bp=${wsBackpressureCount}`);
|
|
@@ -810,8 +812,9 @@ export function createFileHandler({ resolveWorkspace, logger, deps = {} }) {
|
|
|
810
812
|
wsError = true;
|
|
811
813
|
draining = false;
|
|
812
814
|
pendingQueue.length = 0;
|
|
815
|
+
// 同上:等 ws 关闭后再 unlink,避开 fopen-vs-unlink race
|
|
816
|
+
ws.on('close', () => safeUnlink(tmpPath));
|
|
813
817
|
ws.destroy();
|
|
814
|
-
safeUnlink(tmpPath);
|
|
815
818
|
const elapsed = Date.now() - startTime;
|
|
816
819
|
/* c8 ignore next -- ?? fallback for non-Error throw */
|
|
817
820
|
const errMsg = err?.message ?? String(err);
|
|
@@ -17,6 +17,9 @@ import { buildModelDefaultHandlers } from './handlers.js';
|
|
|
17
17
|
import { mainAgentDir } from '../claw-paths.js';
|
|
18
18
|
import { getClawConfig } from '../claw-config.js';
|
|
19
19
|
|
|
20
|
+
// link-UNSAFE:模块级 dedup 缓存。`--link` 模式下两实例各自 lazy-load 一次
|
|
21
|
+
// SDK(结果一致、运行无伤但去重失效)。当前仅 RPC handler 走该路径——
|
|
22
|
+
// 不要在 hook 回调里访问本模块的 export。详见 docs/module-boundaries.md。
|
|
20
23
|
let _configMutationP;
|
|
21
24
|
let _modelsP;
|
|
22
25
|
let _providerAuthP;
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
import { buildProviderAuthHandlers } from './handlers.js';
|
|
11
11
|
import { mainAgentDir } from '../claw-paths.js';
|
|
12
12
|
|
|
13
|
+
// link-UNSAFE:模块级 dedup 缓存。`--link` 模式下两实例各自 lazy-load 一次
|
|
14
|
+
// SDK(结果一致、运行无伤但去重失效)。当前仅 RPC handler 走该路径——
|
|
15
|
+
// 不要在 hook 回调里访问本模块的 export。详见 docs/module-boundaries.md。
|
|
13
16
|
let _sdkPromise;
|
|
14
17
|
|
|
15
18
|
// 默认 loader 仅作 fallback:生产路径必须由入口(plugins/openclaw/index.js)注入 loadSdk,
|
package/src/realtime-bridge.js
CHANGED
|
@@ -1717,6 +1717,13 @@ export class RealtimeBridge {
|
|
|
1717
1717
|
// restart(opts) — 无论当前状态,确保 bridge 以给定 opts 运行(幂等)
|
|
1718
1718
|
// stop() — 停止并销毁 singleton
|
|
1719
1719
|
// 调用方无需感知 singleton 是否为 null,选"要运行"或"要停止"即可。
|
|
1720
|
+
//
|
|
1721
|
+
// link-UNSAFE 警告:以下 singleton 状态与所有读 singleton 的 export(restart /
|
|
1722
|
+
// stop / waitForSessionsReady / ensureAgentSession / gatewayAgentRpc /
|
|
1723
|
+
// broadcastPluginEvent)在 `--link` 安装模式下,hook 路径与 RPC 路径可能拿
|
|
1724
|
+
// 到不同 ESM 模块实例 → 两份独立 singleton。**不要在 api.on(...) hook 回调内
|
|
1725
|
+
// 调用本文件的任何 export**。hook 内若需触发 bridge 副作用,请走 RPC
|
|
1726
|
+
// (api.callGatewayMethod('coclaw.xxx', ...))。详见 docs/module-boundaries.md。
|
|
1720
1727
|
|
|
1721
1728
|
let singleton = null;
|
|
1722
1729
|
|
package/src/remote-log.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 将诊断日志缓冲并通过 WS 通道推送到 CoClaw server。
|
|
5
5
|
* 单例模式——各模块直接 import { remoteLog } 使用。
|
|
6
|
+
*
|
|
7
|
+
* link-UNSAFE:buffer / sender / flushing 都是模块级状态。`--link` 模式下
|
|
8
|
+
* hook 实例可能从未被 setSender 注入 → hook 路径调 remoteLog 会落到没装
|
|
9
|
+
* sender 的实例 → 静默丢日志。**不要在 hook 回调内调用 remoteLog**;
|
|
10
|
+
* 要发就用 hook 入参里的 logger。详见 docs/module-boundaries.md。
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
const MAX_BUFFER = 1000;
|
package/src/runtime.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
// runtime 单例:在 plugin 模式下由 register()
|
|
1
|
+
// runtime 单例:在 plugin 模式下由 register() 注入。
|
|
2
|
+
// link-UNSAFE:`--link` 安装模式下 hook 与 RPC handler 可能跑在不同 ESM 实例 →
|
|
3
|
+
// 两份独立 runtime;hook 实例从未被 setRuntime → getRuntime() 返回 null。
|
|
4
|
+
// **不要在 hook 回调内调用 getRuntime()**——hook 入参 `api` 已带 runtime。
|
|
5
|
+
// 详见 docs/module-boundaries.md。
|
|
2
6
|
let runtime = null;
|
|
3
7
|
|
|
4
8
|
export function setRuntime(rt) {
|
|
@@ -83,24 +83,22 @@ function shouldReplaceByPriority(current, next) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export function createSessionManager(options = {}) {
|
|
86
|
-
/* c8 ignore next */
|
|
87
86
|
const logger = options.logger ?? console;
|
|
88
87
|
const resolveSessionsDir = options.resolveSessionsDir ?? agentSessionsDir;
|
|
89
88
|
const resolveStorePath = options.resolveStorePath ?? sessionStorePath;
|
|
90
89
|
const resolveTranscriptPath = options.resolveTranscriptPath ?? sessionTranscriptPath;
|
|
91
90
|
|
|
92
91
|
function sessionsDir(agentId = 'main') {
|
|
93
|
-
/* c8 ignore next */
|
|
94
92
|
const aid = typeof agentId === 'string' && agentId.trim() ? agentId.trim() : 'main';
|
|
95
93
|
return resolveSessionsDir(aid);
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
async function readIndex(agentId = 'main') {
|
|
99
|
-
/* c8 ignore next */
|
|
100
97
|
const aid = typeof agentId === 'string' && agentId.trim() ? agentId.trim() : 'main';
|
|
101
98
|
const file = resolveStorePath(aid);
|
|
102
99
|
const data = await readJsonSafe(file, {});
|
|
103
|
-
|
|
100
|
+
// readJsonSafe 抛错时返回 {}(已是 object),此处兜底 sessions.json 内容是合法 JSON
|
|
101
|
+
// 但非 object(number / string / boolean / null / array 由下游 listAllEntries 单独处理)
|
|
104
102
|
if (!data || typeof data !== 'object') return {};
|
|
105
103
|
return data;
|
|
106
104
|
}
|
|
@@ -176,7 +174,6 @@ export function createSessionManager(options = {}) {
|
|
|
176
174
|
// 补充 sessions.json 中有索引但无 transcript 文件的 session(如 reset 后未对话、新建 session)
|
|
177
175
|
for (const [sessionKey, entry] of Object.entries(index)) {
|
|
178
176
|
const sid = entry?.sessionId;
|
|
179
|
-
/* c8 ignore next -- !sid 防御性检查 */
|
|
180
177
|
if (!sid || grouped.has(sid)) continue;
|
|
181
178
|
grouped.set(sid, {
|
|
182
179
|
sessionId: sid,
|
|
@@ -184,7 +181,7 @@ export function createSessionManager(options = {}) {
|
|
|
184
181
|
indexed: true,
|
|
185
182
|
archiveType: 'live',
|
|
186
183
|
fileName: null,
|
|
187
|
-
|
|
184
|
+
// entry.updatedAt 缺失或非数字时回落 0;UI 端按 updatedAt 排序时无 transcript 项排到末位
|
|
188
185
|
updatedAt: entry.updatedAt ?? 0,
|
|
189
186
|
size: 0,
|
|
190
187
|
});
|
|
@@ -284,12 +281,10 @@ export function createSessionManager(options = {}) {
|
|
|
284
281
|
all.push(JSON.parse(line));
|
|
285
282
|
}
|
|
286
283
|
catch (err) {
|
|
287
|
-
/* c8 ignore next -- ?./?? fallback */
|
|
288
284
|
logger.warn?.(`[session-manager] bad json line skipped: ${String(err?.message ?? err)}`);
|
|
289
285
|
}
|
|
290
286
|
}
|
|
291
287
|
const messages = all.slice(cursor, cursor + limit);
|
|
292
|
-
/* c8 ignore next */
|
|
293
288
|
const nextCursor = cursor + limit < all.length ? String(cursor + limit) : null;
|
|
294
289
|
return {
|
|
295
290
|
agentId,
|
|
@@ -332,7 +327,6 @@ export function createSessionManager(options = {}) {
|
|
|
332
327
|
messages.push(row);
|
|
333
328
|
}
|
|
334
329
|
catch (err) {
|
|
335
|
-
/* c8 ignore next -- ?./?? fallback */
|
|
336
330
|
logger.warn?.(`[session-manager] bad json line skipped: ${String(err?.message ?? err)}`);
|
|
337
331
|
}
|
|
338
332
|
}
|
|
@@ -996,9 +996,10 @@ export class WebRtcPeer {
|
|
|
996
996
|
// 'fbq' 模式下若 queueDir 不可用则降级到 mem,避免阻塞装配。
|
|
997
997
|
// 同 connId race 隔离(决策 4):FBQ id 加唯一后缀 ${connId}-${ts}-${nonce},
|
|
998
998
|
// 让新旧实例文件名物理不同,destroy/init 期间互不踩踏。MemoryQueue 不碰 fs,无此需求。
|
|
999
|
-
// connId
|
|
1000
|
-
//
|
|
1001
|
-
// queue
|
|
999
|
+
// connId 字符集契约:FBQ / MemoryQueue 共用 `^[A-Za-z0-9._-]+$` 校验。
|
|
1000
|
+
// 上游 server 分配 connId 形如 `c_<digits>` 符合契约;若 server 引入特殊字符,
|
|
1001
|
+
// queue 构造抛 TypeError,由 __setupDataChannel 的 .catch 兜底 warn。
|
|
1002
|
+
// 完整契约 / 违反后果 / 修复方向见 docs/connid-contract.md
|
|
1002
1003
|
const useFbq = this.__rpcQueueImpl === 'fbq' && !!this.__queueDir;
|
|
1003
1004
|
const fbqFallback = !useFbq && this.__rpcQueueImpl === 'fbq';
|
|
1004
1005
|
const queue = useFbq
|