@coclaw/openclaw-coclaw 0.1.0

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.
@@ -0,0 +1,274 @@
1
+ /* c8 ignore start */
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import nodePath from 'node:path';
5
+
6
+ const DERIVED_TITLE_MAX_LEN = 60;
7
+
8
+ // OC 注入的 untrusted metadata 头部
9
+ const CONV_INFO_RE = /^\w[\w ]* \(untrusted metadata\):\n```json\n[\s\S]*?\n```\n\n/;
10
+ // OC 注入的用户消息时间戳前缀
11
+ const USER_TS_RE = /^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+[^\]]+\]\s*/;
12
+ // 尾部 [message_id: xxx]
13
+ const MSG_ID_SUFFIX_RE = /\n\[message_id:\s*[^\]]+\]\s*$/;
14
+ // 定时任务前缀
15
+ const CRON_UUID_RE = /\[cron:[0-9a-f-]+(?:\s+([^\]]*))?\]\s*/;
16
+
17
+ function cleanTitleText(text) {
18
+ if (!text) return '';
19
+ return text
20
+ .replace(CONV_INFO_RE, '')
21
+ .replace(USER_TS_RE, '')
22
+ .replace(CRON_UUID_RE, (_, taskName) => taskName ? `${taskName} ` : '')
23
+ .replace(MSG_ID_SUFFIX_RE, '')
24
+ .trim();
25
+ }
26
+
27
+ function toNum(value, fallback) {
28
+ const n = Number(value);
29
+ return Number.isFinite(n) ? Math.trunc(n) : fallback;
30
+ }
31
+
32
+ function clamp(value, min, max, fallback) {
33
+ const n = toNum(value, fallback);
34
+ if (n < min) return min;
35
+ if (n > max) return max;
36
+ return n;
37
+ }
38
+
39
+ function readJsonSafe(filePath, fallback) {
40
+ try {
41
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
42
+ }
43
+ catch {
44
+ return fallback;
45
+ }
46
+ }
47
+
48
+ function parseSessionFileName(fileName) {
49
+ if (typeof fileName !== 'string' || !fileName.includes('.jsonl')) return null;
50
+ if (fileName.includes('.jsonl.delete.') || fileName.includes('.jsonl.deleted.')) return null;
51
+
52
+ if (fileName.endsWith('.jsonl')) {
53
+ return {
54
+ sessionId: fileName.slice(0, -6),
55
+ archiveType: 'live',
56
+ };
57
+ }
58
+ if (fileName.includes('.jsonl.reset.')) {
59
+ return {
60
+ sessionId: fileName.split('.jsonl.reset.')[0],
61
+ archiveType: 'reset',
62
+ };
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function archiveTypePriority(archiveType) {
68
+ return archiveType === 'reset' ? 2 : 1;
69
+ }
70
+
71
+ function shouldReplaceByPriority(current, next) {
72
+ const currentPriority = archiveTypePriority(current.archiveType);
73
+ const nextPriority = archiveTypePriority(next.archiveType);
74
+ if (nextPriority !== currentPriority) {
75
+ return nextPriority > currentPriority;
76
+ }
77
+ return next.updatedAt > current.updatedAt;
78
+ }
79
+
80
+ function truncateTitle(text, maxLen = DERIVED_TITLE_MAX_LEN) {
81
+ if (text.length <= maxLen) return text;
82
+ const cut = text.slice(0, maxLen - 1);
83
+ const lastSpace = cut.lastIndexOf(' ');
84
+ if (lastSpace > maxLen * 0.6) {
85
+ return `${cut.slice(0, lastSpace)}…`;
86
+ }
87
+ return `${cut}…`;
88
+ }
89
+
90
+ function extractRawTextFromContent(content) {
91
+ if (typeof content === 'string') return content;
92
+ if (!Array.isArray(content)) return undefined;
93
+ for (const part of content) {
94
+ if (!part || typeof part !== 'object') continue;
95
+ if (part.type !== 'text') continue;
96
+ if (typeof part.text !== 'string') continue;
97
+ if (part.text.trim()) return part.text;
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ function findFirstUserRawText(filePath, logger) {
103
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n');
104
+ for (const line of lines) {
105
+ if (!line) continue;
106
+ try {
107
+ const row = JSON.parse(line);
108
+ if (row?.type !== 'message') continue;
109
+ if (row?.message?.role !== 'user') continue;
110
+ const raw = extractRawTextFromContent(row?.message?.content);
111
+ if (raw && raw.trim()) return raw;
112
+ }
113
+ catch (err) {
114
+ logger.warn?.(`[session-manager] bad json line skipped when deriving title: ${String(err?.message ?? err)}`);
115
+ }
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ function deriveTitle(filePath, logger) {
121
+ const rawText = findFirstUserRawText(filePath, logger);
122
+ if (!rawText) return undefined;
123
+ const cleaned = cleanTitleText(rawText);
124
+ if (!cleaned) return undefined;
125
+ const normalized = cleaned.replace(/\s+/g, ' ').trim();
126
+ if (!normalized) return undefined;
127
+ return truncateTitle(normalized, DERIVED_TITLE_MAX_LEN);
128
+ }
129
+
130
+ export function createSessionManager(options = {}) {
131
+ const rootDir = options.rootDir ?? nodePath.join(os.homedir(), '.openclaw', 'agents');
132
+ const logger = options.logger ?? console;
133
+
134
+ function sessionsDir(agentId = 'main') {
135
+ const aid = typeof agentId === 'string' && agentId.trim() ? agentId.trim() : 'main';
136
+ return nodePath.join(rootDir, aid, 'sessions');
137
+ }
138
+
139
+ function readIndex(agentId = 'main') {
140
+ const file = nodePath.join(sessionsDir(agentId), 'sessions.json');
141
+ const data = readJsonSafe(file, {});
142
+ if (!data || typeof data !== 'object') return {};
143
+ return data;
144
+ }
145
+
146
+ function listAll(params = {}) {
147
+ const agentId = typeof params.agentId === 'string' && params.agentId.trim() ? params.agentId.trim() : 'main';
148
+ const limit = clamp(params.limit, 1, 200, 50);
149
+ const cursor = clamp(params.cursor, 0, Number.MAX_SAFE_INTEGER, 0);
150
+ const dir = sessionsDir(agentId);
151
+ const index = readIndex(agentId);
152
+ const indexed = new Set(
153
+ Object.values(index)
154
+ .map((item) => item?.sessionId)
155
+ .filter(Boolean),
156
+ );
157
+ const sessionKeyById = new Map();
158
+ for (const [sessionKey, item] of Object.entries(index)) {
159
+ const sid = item?.sessionId;
160
+ if (sid) {
161
+ sessionKeyById.set(sid, sessionKey);
162
+ }
163
+ }
164
+
165
+ const files = fs.existsSync(dir) ? fs.readdirSync(dir) : [];
166
+ const grouped = new Map();
167
+ for (const file of files) {
168
+ const parsed = parseSessionFileName(file);
169
+ if (!parsed?.sessionId) continue;
170
+ const full = nodePath.join(dir, file);
171
+ const stat = fs.statSync(full);
172
+ const row = {
173
+ sessionId: parsed.sessionId,
174
+ sessionKey: sessionKeyById.get(parsed.sessionId) ?? null,
175
+ indexed: indexed.has(parsed.sessionId),
176
+ archiveType: parsed.archiveType,
177
+ fileName: file,
178
+ updatedAt: stat.mtimeMs,
179
+ size: stat.size,
180
+ };
181
+ const previous = grouped.get(parsed.sessionId);
182
+ if (!previous || shouldReplaceByPriority(previous, row)) {
183
+ grouped.set(parsed.sessionId, row);
184
+ }
185
+ }
186
+
187
+ const rows = Array.from(grouped.values());
188
+ rows.sort((a, b) => b.updatedAt - a.updatedAt);
189
+
190
+ const items = rows.slice(cursor, cursor + limit).map((row) => {
191
+ const transcriptPath = nodePath.join(dir, row.fileName);
192
+ const derivedTitle = deriveTitle(transcriptPath, logger);
193
+ if (!derivedTitle) {
194
+ return { ...row };
195
+ }
196
+ return {
197
+ ...row,
198
+ derivedTitle,
199
+ };
200
+ });
201
+ const nextCursor = cursor + limit < rows.length ? String(cursor + limit) : null;
202
+ return {
203
+ agentId,
204
+ total: rows.length,
205
+ cursor: String(cursor),
206
+ nextCursor,
207
+ items,
208
+ };
209
+ }
210
+
211
+ function resolveTranscriptFile(agentId, sessionId) {
212
+ const dir = sessionsDir(agentId);
213
+ const files = fs.existsSync(dir) ? fs.readdirSync(dir) : [];
214
+ const resetPrefix = `${sessionId}.jsonl.reset.`;
215
+ const resetCandidates = files
216
+ .filter((name) => name.startsWith(resetPrefix))
217
+ .map((name) => {
218
+ const full = nodePath.join(dir, name);
219
+ const stat = fs.statSync(full);
220
+ return {
221
+ path: full,
222
+ archiveStamp: name.slice(resetPrefix.length),
223
+ updatedAt: stat.mtimeMs,
224
+ };
225
+ })
226
+ .sort((a, b) => {
227
+ if (a.archiveStamp !== b.archiveStamp) {
228
+ return b.archiveStamp.localeCompare(a.archiveStamp);
229
+ }
230
+ return b.updatedAt - a.updatedAt;
231
+ });
232
+ if (resetCandidates.length > 0) {
233
+ return resetCandidates[0].path;
234
+ }
235
+ const livePath = nodePath.join(dir, `${sessionId}.jsonl`);
236
+ if (fs.existsSync(livePath)) {
237
+ return livePath;
238
+ }
239
+ return null;
240
+ }
241
+
242
+ function get(params = {}) {
243
+ const agentId = typeof params.agentId === 'string' && params.agentId.trim() ? params.agentId.trim() : 'main';
244
+ const sessionId = typeof params.sessionId === 'string' ? params.sessionId.trim() : '';
245
+ if (!sessionId) throw new Error('sessionId required');
246
+ const limit = clamp(params.limit, 1, 500, 100);
247
+ const cursor = clamp(params.cursor, 0, Number.MAX_SAFE_INTEGER, 0);
248
+ const file = resolveTranscriptFile(agentId, sessionId);
249
+ if (!file) throw new Error(`session transcript not found: ${sessionId}`);
250
+
251
+ const all = [];
252
+ for (const line of fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
253
+ try {
254
+ all.push(JSON.parse(line));
255
+ }
256
+ catch (err) {
257
+ logger.warn?.(`[session-manager] bad json line skipped: ${String(err?.message ?? err)}`);
258
+ }
259
+ }
260
+ const messages = all.slice(cursor, cursor + limit);
261
+ const nextCursor = cursor + limit < all.length ? String(cursor + limit) : null;
262
+ return {
263
+ agentId,
264
+ sessionId,
265
+ total: all.length,
266
+ cursor: String(cursor),
267
+ nextCursor,
268
+ messages,
269
+ };
270
+ }
271
+
272
+ return { listAll, get };
273
+ }
274
+ /* c8 ignore stop */
@@ -0,0 +1,50 @@
1
+ import { buildOutboundEnvelope, normalizeInboundEnvelope } from './message-model.js';
2
+
3
+ export function createTransportAdapter(deps = {}) {
4
+ const {
5
+ sendOutbound = async () => ({ accepted: true }),
6
+ onInbound = async () => undefined,
7
+ logger = console,
8
+ } = deps;
9
+
10
+ async function dispatchInbound(rawEnvelope) {
11
+ const inbound = normalizeInboundEnvelope(rawEnvelope);
12
+ await onInbound(inbound);
13
+ return inbound;
14
+ }
15
+
16
+ async function dispatchOutbound(rawEnvelope) {
17
+ const outbound = buildOutboundEnvelope(rawEnvelope);
18
+ const result = await sendOutbound(outbound);
19
+ return {
20
+ accepted: Boolean(result?.accepted ?? true),
21
+ messageId: outbound.messageId,
22
+ };
23
+ }
24
+
25
+ function safeDispatchInbound(rawEnvelope) {
26
+ return dispatchInbound(rawEnvelope).catch((err) => {
27
+ logger.warn?.(`[coclaw-transport] inbound failed: ${String(err?.message ?? err)}`);
28
+ return null;
29
+ });
30
+ }
31
+
32
+ function safeDispatchOutbound(rawEnvelope) {
33
+ /* c8 ignore start */
34
+ return dispatchOutbound(rawEnvelope).catch((err) => {
35
+ logger.warn?.(`[coclaw-transport] outbound failed: ${String(err?.message ?? err)}`);
36
+ return {
37
+ accepted: false,
38
+ error: String(err?.message ?? err),
39
+ };
40
+ });
41
+ /* c8 ignore stop */
42
+ }
43
+
44
+ return {
45
+ dispatchInbound,
46
+ dispatchOutbound,
47
+ safeDispatchInbound,
48
+ safeDispatchOutbound,
49
+ };
50
+ }