clawvault 2.0.2 → 2.1.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/README.md +5 -0
- package/bin/register-query-commands.js +16 -0
- package/dist/chunk-UPHUI5PD.js +723 -0
- package/dist/{chunk-NU7VKTWG.js → chunk-USAY3OIO.js} +16 -6
- package/dist/commands/observe.d.ts +5 -0
- package/dist/commands/observe.js +4 -2
- package/dist/commands/sleep.js +1 -1
- package/dist/index.js +2 -2
- package/hooks/clawvault/HOOK.md +4 -2
- package/hooks/clawvault/handler.js +238 -2
- package/hooks/clawvault/handler.test.js +82 -0
- package/package.json +1 -1
- package/dist/chunk-BAS32WLF.js +0 -319
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSessionsDir
|
|
3
|
+
} from "./chunk-HRLWZGMA.js";
|
|
4
|
+
import {
|
|
5
|
+
Observer,
|
|
6
|
+
parseSessionFile
|
|
7
|
+
} from "./chunk-USAY3OIO.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveVaultPath
|
|
10
|
+
} from "./chunk-MXSSG3QU.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/observe.ts
|
|
13
|
+
import * as fs3 from "fs";
|
|
14
|
+
import * as path3 from "path";
|
|
15
|
+
import { spawn } from "child_process";
|
|
16
|
+
|
|
17
|
+
// src/observer/watcher.ts
|
|
18
|
+
import * as fs from "fs";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
import chokidar from "chokidar";
|
|
21
|
+
var DEFAULT_FLUSH_THRESHOLD_CHARS = 500;
|
|
22
|
+
var SessionWatcher = class {
|
|
23
|
+
watchPath;
|
|
24
|
+
observer;
|
|
25
|
+
ignoreInitial;
|
|
26
|
+
debounceMs;
|
|
27
|
+
flushThresholdChars;
|
|
28
|
+
watcher = null;
|
|
29
|
+
fileOffsets = /* @__PURE__ */ new Map();
|
|
30
|
+
pendingPaths = /* @__PURE__ */ new Set();
|
|
31
|
+
debounceTimer = null;
|
|
32
|
+
processingQueue = Promise.resolve();
|
|
33
|
+
bufferedChars = 0;
|
|
34
|
+
constructor(watchPath, observer, options = {}) {
|
|
35
|
+
this.watchPath = path.resolve(watchPath);
|
|
36
|
+
this.observer = observer;
|
|
37
|
+
this.ignoreInitial = options.ignoreInitial ?? false;
|
|
38
|
+
this.debounceMs = options.debounceMs ?? 500;
|
|
39
|
+
this.flushThresholdChars = Math.max(1, options.flushThresholdChars ?? DEFAULT_FLUSH_THRESHOLD_CHARS);
|
|
40
|
+
}
|
|
41
|
+
async start() {
|
|
42
|
+
if (!fs.existsSync(this.watchPath)) {
|
|
43
|
+
throw new Error(`Watch path does not exist: ${this.watchPath}`);
|
|
44
|
+
}
|
|
45
|
+
this.watcher = chokidar.watch(this.watchPath, {
|
|
46
|
+
persistent: true,
|
|
47
|
+
ignoreInitial: this.ignoreInitial,
|
|
48
|
+
awaitWriteFinish: {
|
|
49
|
+
stabilityThreshold: 120,
|
|
50
|
+
pollInterval: 30
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const enqueue = (changedPath) => {
|
|
54
|
+
this.pendingPaths.add(path.resolve(changedPath));
|
|
55
|
+
this.scheduleDrain();
|
|
56
|
+
};
|
|
57
|
+
this.watcher.on("add", enqueue);
|
|
58
|
+
this.watcher.on("change", enqueue);
|
|
59
|
+
this.watcher.on("unlink", (deletedPath) => {
|
|
60
|
+
const resolved = path.resolve(deletedPath);
|
|
61
|
+
this.fileOffsets.delete(resolved);
|
|
62
|
+
this.pendingPaths.delete(resolved);
|
|
63
|
+
});
|
|
64
|
+
await new Promise((resolve4, reject) => {
|
|
65
|
+
this.watcher?.once("ready", () => resolve4());
|
|
66
|
+
this.watcher?.once("error", (error) => reject(error));
|
|
67
|
+
});
|
|
68
|
+
if (this.ignoreInitial) {
|
|
69
|
+
this.primeInitialOffsets();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async stop() {
|
|
73
|
+
if (this.debounceTimer) {
|
|
74
|
+
clearTimeout(this.debounceTimer);
|
|
75
|
+
this.debounceTimer = null;
|
|
76
|
+
this.drainPendingPaths();
|
|
77
|
+
}
|
|
78
|
+
await this.processingQueue.catch(() => void 0);
|
|
79
|
+
if (this.bufferedChars > 0) {
|
|
80
|
+
await this.observer.flush();
|
|
81
|
+
this.bufferedChars = 0;
|
|
82
|
+
}
|
|
83
|
+
this.pendingPaths.clear();
|
|
84
|
+
await this.watcher?.close();
|
|
85
|
+
this.watcher = null;
|
|
86
|
+
}
|
|
87
|
+
scheduleDrain() {
|
|
88
|
+
if (this.debounceTimer) {
|
|
89
|
+
clearTimeout(this.debounceTimer);
|
|
90
|
+
}
|
|
91
|
+
this.debounceTimer = setTimeout(() => {
|
|
92
|
+
this.debounceTimer = null;
|
|
93
|
+
this.drainPendingPaths();
|
|
94
|
+
}, this.debounceMs);
|
|
95
|
+
}
|
|
96
|
+
drainPendingPaths() {
|
|
97
|
+
const nextPaths = [...this.pendingPaths];
|
|
98
|
+
this.pendingPaths.clear();
|
|
99
|
+
for (const changedPath of nextPaths) {
|
|
100
|
+
this.processingQueue = this.processingQueue.then(() => this.consumeFile(changedPath)).catch(() => void 0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async consumeFile(filePath) {
|
|
104
|
+
const resolved = path.resolve(filePath);
|
|
105
|
+
if (!fs.existsSync(resolved)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const stats = fs.statSync(resolved);
|
|
109
|
+
if (!stats.isFile()) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const previousOffset = this.fileOffsets.get(resolved) ?? 0;
|
|
113
|
+
const startOffset = stats.size < previousOffset ? 0 : previousOffset;
|
|
114
|
+
if (stats.size <= startOffset) {
|
|
115
|
+
this.fileOffsets.set(resolved, stats.size);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const bytesToRead = stats.size - startOffset;
|
|
119
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
120
|
+
const fd = fs.openSync(resolved, "r");
|
|
121
|
+
try {
|
|
122
|
+
fs.readSync(fd, buffer, 0, bytesToRead, startOffset);
|
|
123
|
+
} finally {
|
|
124
|
+
fs.closeSync(fd);
|
|
125
|
+
}
|
|
126
|
+
this.fileOffsets.set(resolved, stats.size);
|
|
127
|
+
const chunk = buffer.toString("utf-8");
|
|
128
|
+
const messages = chunk.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
129
|
+
if (messages.length === 0) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await this.observer.processMessages(messages);
|
|
133
|
+
this.bufferedChars += chunk.length;
|
|
134
|
+
if (this.bufferedChars >= this.flushThresholdChars) {
|
|
135
|
+
await this.observer.flush();
|
|
136
|
+
this.bufferedChars = 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
primeInitialOffsets() {
|
|
140
|
+
for (const filePath of this.collectFiles(this.watchPath)) {
|
|
141
|
+
try {
|
|
142
|
+
const stats = fs.statSync(filePath);
|
|
143
|
+
if (stats.isFile()) {
|
|
144
|
+
this.fileOffsets.set(filePath, stats.size);
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
collectFiles(targetPath) {
|
|
151
|
+
if (!fs.existsSync(targetPath)) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const resolved = path.resolve(targetPath);
|
|
155
|
+
const stats = fs.statSync(resolved);
|
|
156
|
+
if (stats.isFile()) {
|
|
157
|
+
return [resolved];
|
|
158
|
+
}
|
|
159
|
+
if (!stats.isDirectory()) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const collected = [];
|
|
163
|
+
for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
|
|
164
|
+
const childPath = path.join(resolved, entry.name);
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
collected.push(...this.collectFiles(childPath));
|
|
167
|
+
} else if (entry.isFile()) {
|
|
168
|
+
collected.push(path.resolve(childPath));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return collected;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// src/observer/active-session-observer.ts
|
|
176
|
+
import * as fs2 from "fs";
|
|
177
|
+
import * as path2 from "path";
|
|
178
|
+
var ONE_KIB = 1024;
|
|
179
|
+
var ONE_MIB = ONE_KIB * ONE_KIB;
|
|
180
|
+
var SMALL_SESSION_THRESHOLD_BYTES = 50 * ONE_KIB;
|
|
181
|
+
var MEDIUM_SESSION_THRESHOLD_BYTES = 150 * ONE_KIB;
|
|
182
|
+
var LARGE_SESSION_THRESHOLD_BYTES = 300 * ONE_KIB;
|
|
183
|
+
var DEFAULT_AGENT_ID = "clawdious";
|
|
184
|
+
var AGENT_ID_RE = /^[a-zA-Z0-9_-]{1,100}$/;
|
|
185
|
+
var SESSION_ID_RE = /^[a-zA-Z0-9._-]{1,200}$/;
|
|
186
|
+
var CURSOR_FILE_NAME = "observe-cursors.json";
|
|
187
|
+
function isFiniteNonNegative(value) {
|
|
188
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
189
|
+
}
|
|
190
|
+
function normalizeAgentId(input) {
|
|
191
|
+
const raw = (input ?? process.env.OPENCLAW_AGENT_ID ?? DEFAULT_AGENT_ID).trim();
|
|
192
|
+
if (!AGENT_ID_RE.test(raw)) {
|
|
193
|
+
return DEFAULT_AGENT_ID;
|
|
194
|
+
}
|
|
195
|
+
return raw;
|
|
196
|
+
}
|
|
197
|
+
function resolveSessionsDirectory(agentId, override) {
|
|
198
|
+
if (override?.trim()) {
|
|
199
|
+
return path2.resolve(override.trim());
|
|
200
|
+
}
|
|
201
|
+
return getSessionsDir(agentId);
|
|
202
|
+
}
|
|
203
|
+
function getCursorPath(vaultPath) {
|
|
204
|
+
return path2.join(vaultPath, ".clawvault", CURSOR_FILE_NAME);
|
|
205
|
+
}
|
|
206
|
+
function getScaledObservationThresholdBytes(fileSizeBytes) {
|
|
207
|
+
if (fileSizeBytes < ONE_MIB) {
|
|
208
|
+
return SMALL_SESSION_THRESHOLD_BYTES;
|
|
209
|
+
}
|
|
210
|
+
if (fileSizeBytes <= 5 * ONE_MIB) {
|
|
211
|
+
return MEDIUM_SESSION_THRESHOLD_BYTES;
|
|
212
|
+
}
|
|
213
|
+
return LARGE_SESSION_THRESHOLD_BYTES;
|
|
214
|
+
}
|
|
215
|
+
function parseCursorStore(raw) {
|
|
216
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
const input = raw;
|
|
220
|
+
const store = {};
|
|
221
|
+
for (const [sessionId, value] of Object.entries(input)) {
|
|
222
|
+
if (!SESSION_ID_RE.test(sessionId)) continue;
|
|
223
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
224
|
+
const entry = value;
|
|
225
|
+
if (!isFiniteNonNegative(entry.lastObservedOffset)) continue;
|
|
226
|
+
if (!isFiniteNonNegative(entry.lastFileSize)) continue;
|
|
227
|
+
if (typeof entry.lastObservedAt !== "string" || !entry.lastObservedAt.trim()) continue;
|
|
228
|
+
if (typeof entry.sessionKey !== "string" || !entry.sessionKey.trim()) continue;
|
|
229
|
+
store[sessionId] = {
|
|
230
|
+
lastObservedOffset: entry.lastObservedOffset,
|
|
231
|
+
lastObservedAt: entry.lastObservedAt,
|
|
232
|
+
sessionKey: entry.sessionKey,
|
|
233
|
+
lastFileSize: entry.lastFileSize
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return store;
|
|
237
|
+
}
|
|
238
|
+
function loadObserveCursorStore(vaultPath) {
|
|
239
|
+
const cursorPath = getCursorPath(vaultPath);
|
|
240
|
+
if (!fs2.existsSync(cursorPath)) {
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const raw = JSON.parse(fs2.readFileSync(cursorPath, "utf-8"));
|
|
245
|
+
return parseCursorStore(raw);
|
|
246
|
+
} catch {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function saveObserveCursorStore(vaultPath, store) {
|
|
251
|
+
const cursorPath = getCursorPath(vaultPath);
|
|
252
|
+
fs2.mkdirSync(path2.dirname(cursorPath), { recursive: true });
|
|
253
|
+
fs2.writeFileSync(cursorPath, `${JSON.stringify(store, null, 2)}
|
|
254
|
+
`, "utf-8");
|
|
255
|
+
}
|
|
256
|
+
function loadSessionIndex(sessionsDir) {
|
|
257
|
+
const indexPath = path2.join(sessionsDir, "sessions.json");
|
|
258
|
+
if (!fs2.existsSync(indexPath)) {
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const parsed = JSON.parse(fs2.readFileSync(indexPath, "utf-8"));
|
|
263
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
264
|
+
return {};
|
|
265
|
+
}
|
|
266
|
+
return parsed;
|
|
267
|
+
} catch {
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function resolveTranscriptPath(sessionsDir, sessionId) {
|
|
272
|
+
return path2.join(sessionsDir, `${sessionId}.jsonl`);
|
|
273
|
+
}
|
|
274
|
+
function discoverSessionDescriptors(sessionsDir, fallbackAgentId) {
|
|
275
|
+
const descriptors = [];
|
|
276
|
+
const seen = /* @__PURE__ */ new Set();
|
|
277
|
+
const index = loadSessionIndex(sessionsDir);
|
|
278
|
+
const indexedEntries = Object.entries(index).sort((left, right) => {
|
|
279
|
+
const leftUpdated = Number(left[1]?.updatedAt ?? 0);
|
|
280
|
+
const rightUpdated = Number(right[1]?.updatedAt ?? 0);
|
|
281
|
+
return rightUpdated - leftUpdated;
|
|
282
|
+
});
|
|
283
|
+
for (const [sessionKey, entry] of indexedEntries) {
|
|
284
|
+
if (!entry || typeof entry !== "object") continue;
|
|
285
|
+
const sessionId = typeof entry.sessionId === "string" ? entry.sessionId.trim() : "";
|
|
286
|
+
if (!SESSION_ID_RE.test(sessionId) || seen.has(sessionId)) continue;
|
|
287
|
+
const filePath = resolveTranscriptPath(sessionsDir, sessionId);
|
|
288
|
+
try {
|
|
289
|
+
const stat = fs2.statSync(filePath);
|
|
290
|
+
if (!stat.isFile()) continue;
|
|
291
|
+
seen.add(sessionId);
|
|
292
|
+
descriptors.push({ sessionId, sessionKey, filePath });
|
|
293
|
+
} catch {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const fallbackPrefix = `agent:${fallbackAgentId}:`;
|
|
298
|
+
for (const fileName of fs2.readdirSync(sessionsDir)) {
|
|
299
|
+
if (!fileName.endsWith(".jsonl") || fileName.includes(".backup") || fileName.includes(".deleted")) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const sessionId = fileName.slice(0, -".jsonl".length);
|
|
303
|
+
if (!SESSION_ID_RE.test(sessionId) || seen.has(sessionId)) continue;
|
|
304
|
+
const filePath = path2.join(sessionsDir, fileName);
|
|
305
|
+
try {
|
|
306
|
+
const stat = fs2.statSync(filePath);
|
|
307
|
+
if (!stat.isFile()) continue;
|
|
308
|
+
seen.add(sessionId);
|
|
309
|
+
descriptors.push({
|
|
310
|
+
sessionId,
|
|
311
|
+
sessionKey: `${fallbackPrefix}unknown:${sessionId}`,
|
|
312
|
+
filePath
|
|
313
|
+
});
|
|
314
|
+
} catch {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return descriptors;
|
|
319
|
+
}
|
|
320
|
+
function normalizeWhitespace(value) {
|
|
321
|
+
return value.replace(/\s+/g, " ").trim();
|
|
322
|
+
}
|
|
323
|
+
function extractContentText(value) {
|
|
324
|
+
if (typeof value === "string") {
|
|
325
|
+
return normalizeWhitespace(value);
|
|
326
|
+
}
|
|
327
|
+
if (Array.isArray(value)) {
|
|
328
|
+
const parts = value.map((item) => extractContentText(item)).filter(Boolean);
|
|
329
|
+
return normalizeWhitespace(parts.join(" "));
|
|
330
|
+
}
|
|
331
|
+
if (!value || typeof value !== "object") {
|
|
332
|
+
return "";
|
|
333
|
+
}
|
|
334
|
+
const input = value;
|
|
335
|
+
if (typeof input.text === "string") {
|
|
336
|
+
return normalizeWhitespace(input.text);
|
|
337
|
+
}
|
|
338
|
+
if (typeof input.content === "string") {
|
|
339
|
+
return normalizeWhitespace(input.content);
|
|
340
|
+
}
|
|
341
|
+
return "";
|
|
342
|
+
}
|
|
343
|
+
function normalizeRole(role) {
|
|
344
|
+
if (typeof role !== "string") {
|
|
345
|
+
return "";
|
|
346
|
+
}
|
|
347
|
+
return role.trim().toLowerCase();
|
|
348
|
+
}
|
|
349
|
+
function parseOpenClawJsonLine(line) {
|
|
350
|
+
if (!line.trim()) {
|
|
351
|
+
return "";
|
|
352
|
+
}
|
|
353
|
+
let parsed;
|
|
354
|
+
try {
|
|
355
|
+
parsed = JSON.parse(line);
|
|
356
|
+
} catch {
|
|
357
|
+
return "";
|
|
358
|
+
}
|
|
359
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
360
|
+
return "";
|
|
361
|
+
}
|
|
362
|
+
const entry = parsed;
|
|
363
|
+
if ("role" in entry && "content" in entry) {
|
|
364
|
+
const role = normalizeRole(entry.role);
|
|
365
|
+
const content = extractContentText(entry.content);
|
|
366
|
+
if (!content) return "";
|
|
367
|
+
return role ? `${role}: ${content}` : content;
|
|
368
|
+
}
|
|
369
|
+
if (entry.type === "message" && entry.message && typeof entry.message === "object") {
|
|
370
|
+
const message = entry.message;
|
|
371
|
+
const role = normalizeRole(message.role);
|
|
372
|
+
const content = extractContentText(message.content);
|
|
373
|
+
if (!content) return "";
|
|
374
|
+
return role ? `${role}: ${content}` : content;
|
|
375
|
+
}
|
|
376
|
+
return "";
|
|
377
|
+
}
|
|
378
|
+
function decodeLineBuffer(lineBuffer) {
|
|
379
|
+
if (lineBuffer.length === 0) {
|
|
380
|
+
return "";
|
|
381
|
+
}
|
|
382
|
+
const normalized = lineBuffer[lineBuffer.length - 1] === 13 ? lineBuffer.subarray(0, lineBuffer.length - 1) : lineBuffer;
|
|
383
|
+
return normalized.toString("utf-8").trim();
|
|
384
|
+
}
|
|
385
|
+
async function readIncrementalMessages(filePath, startOffset) {
|
|
386
|
+
const messages = [];
|
|
387
|
+
let nextOffset = startOffset;
|
|
388
|
+
let remainder = Buffer.alloc(0);
|
|
389
|
+
const stream = fs2.createReadStream(filePath, {
|
|
390
|
+
start: startOffset
|
|
391
|
+
});
|
|
392
|
+
for await (const chunk of stream) {
|
|
393
|
+
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
394
|
+
const combined = remainder.length > 0 ? Buffer.concat([remainder, chunkBuffer]) : chunkBuffer;
|
|
395
|
+
let lineStart = 0;
|
|
396
|
+
for (let index = 0; index < combined.length; index += 1) {
|
|
397
|
+
if (combined[index] !== 10) continue;
|
|
398
|
+
const lineBuffer = combined.subarray(lineStart, index);
|
|
399
|
+
const line = decodeLineBuffer(lineBuffer);
|
|
400
|
+
const parsed = parseOpenClawJsonLine(line);
|
|
401
|
+
if (parsed) {
|
|
402
|
+
messages.push(parsed);
|
|
403
|
+
}
|
|
404
|
+
nextOffset += index - lineStart + 1;
|
|
405
|
+
lineStart = index + 1;
|
|
406
|
+
}
|
|
407
|
+
remainder = combined.subarray(lineStart);
|
|
408
|
+
}
|
|
409
|
+
if (remainder.length > 0) {
|
|
410
|
+
const trailing = decodeLineBuffer(remainder);
|
|
411
|
+
if (trailing) {
|
|
412
|
+
const parsed = parseOpenClawJsonLine(trailing);
|
|
413
|
+
if (parsed) {
|
|
414
|
+
messages.push(parsed);
|
|
415
|
+
nextOffset += remainder.length;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { messages, nextOffset };
|
|
420
|
+
}
|
|
421
|
+
function parseSessionSourceLabel(sessionKey) {
|
|
422
|
+
const parts = sessionKey.split(":");
|
|
423
|
+
if (parts.length < 3 || parts[0] !== "agent") {
|
|
424
|
+
return "session";
|
|
425
|
+
}
|
|
426
|
+
const scope = parts.slice(2);
|
|
427
|
+
if (scope[0] === "main") {
|
|
428
|
+
return "main";
|
|
429
|
+
}
|
|
430
|
+
if (scope[0] === "telegram" && scope[1] === "dm") {
|
|
431
|
+
return "telegram-dm";
|
|
432
|
+
}
|
|
433
|
+
if (scope[0] === "telegram" && scope[1] === "group") {
|
|
434
|
+
return "telegram-group";
|
|
435
|
+
}
|
|
436
|
+
if (scope[0] === "discord") {
|
|
437
|
+
return "discord";
|
|
438
|
+
}
|
|
439
|
+
if (scope[0] === "telegram") {
|
|
440
|
+
return "telegram";
|
|
441
|
+
}
|
|
442
|
+
if (scope[0] === "slack") {
|
|
443
|
+
return "slack";
|
|
444
|
+
}
|
|
445
|
+
return scope[0] || "session";
|
|
446
|
+
}
|
|
447
|
+
function createDefaultObserver(vaultPath, options) {
|
|
448
|
+
return new Observer(vaultPath, options);
|
|
449
|
+
}
|
|
450
|
+
function selectCandidates(descriptors, cursors, minNewBytes) {
|
|
451
|
+
const candidates = [];
|
|
452
|
+
for (const descriptor of descriptors) {
|
|
453
|
+
let stat;
|
|
454
|
+
try {
|
|
455
|
+
stat = fs2.statSync(descriptor.filePath);
|
|
456
|
+
} catch {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (!stat.isFile()) continue;
|
|
460
|
+
const fileSize = stat.size;
|
|
461
|
+
const cursor = cursors[descriptor.sessionId];
|
|
462
|
+
const previousOffset = cursor && isFiniteNonNegative(cursor.lastObservedOffset) ? cursor.lastObservedOffset : 0;
|
|
463
|
+
const startOffset = previousOffset <= fileSize ? previousOffset : 0;
|
|
464
|
+
const newBytes = Math.max(0, fileSize - startOffset);
|
|
465
|
+
const thresholdBytes = minNewBytes ?? getScaledObservationThresholdBytes(fileSize);
|
|
466
|
+
if (newBytes < thresholdBytes) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
candidates.push({
|
|
470
|
+
sessionId: descriptor.sessionId,
|
|
471
|
+
sessionKey: descriptor.sessionKey,
|
|
472
|
+
sourceLabel: parseSessionSourceLabel(descriptor.sessionKey),
|
|
473
|
+
filePath: descriptor.filePath,
|
|
474
|
+
fileSize,
|
|
475
|
+
startOffset,
|
|
476
|
+
newBytes,
|
|
477
|
+
thresholdBytes
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return candidates;
|
|
481
|
+
}
|
|
482
|
+
async function observeActiveSessions(options, dependencies = {}) {
|
|
483
|
+
const vaultPath = path2.resolve(options.vaultPath);
|
|
484
|
+
const agentId = normalizeAgentId(options.agentId);
|
|
485
|
+
const sessionsDir = resolveSessionsDirectory(agentId, options.sessionsDir);
|
|
486
|
+
const dryRun = Boolean(options.dryRun);
|
|
487
|
+
if (!fs2.existsSync(sessionsDir) || !fs2.statSync(sessionsDir).isDirectory()) {
|
|
488
|
+
return {
|
|
489
|
+
agentId,
|
|
490
|
+
sessionsDir,
|
|
491
|
+
checkedSessions: 0,
|
|
492
|
+
candidateSessions: 0,
|
|
493
|
+
observedSessions: 0,
|
|
494
|
+
cursorUpdates: 0,
|
|
495
|
+
dryRun,
|
|
496
|
+
totalNewBytes: 0,
|
|
497
|
+
candidates: []
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
|
|
501
|
+
const cursors = loadObserveCursorStore(vaultPath);
|
|
502
|
+
const descriptors = discoverSessionDescriptors(sessionsDir, agentId);
|
|
503
|
+
const candidates = selectCandidates(descriptors, cursors, options.minNewBytes);
|
|
504
|
+
if (dryRun || candidates.length === 0) {
|
|
505
|
+
return {
|
|
506
|
+
agentId,
|
|
507
|
+
sessionsDir,
|
|
508
|
+
checkedSessions: descriptors.length,
|
|
509
|
+
candidateSessions: candidates.length,
|
|
510
|
+
observedSessions: 0,
|
|
511
|
+
cursorUpdates: 0,
|
|
512
|
+
dryRun,
|
|
513
|
+
totalNewBytes: candidates.reduce((sum, candidate) => sum + candidate.newBytes, 0),
|
|
514
|
+
candidates
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const observerFactory = dependencies.createObserver ?? createDefaultObserver;
|
|
518
|
+
const observer = observerFactory(vaultPath, {
|
|
519
|
+
tokenThreshold: options.threshold,
|
|
520
|
+
reflectThreshold: options.reflectThreshold,
|
|
521
|
+
model: options.model
|
|
522
|
+
});
|
|
523
|
+
let observedSessions = 0;
|
|
524
|
+
let cursorUpdates = 0;
|
|
525
|
+
for (const candidate of candidates) {
|
|
526
|
+
const { messages, nextOffset } = await readIncrementalMessages(candidate.filePath, candidate.startOffset);
|
|
527
|
+
const taggedMessages = messages.map((message) => `[${candidate.sourceLabel}] ${message}`);
|
|
528
|
+
if (taggedMessages.length > 0) {
|
|
529
|
+
await observer.processMessages(taggedMessages);
|
|
530
|
+
await observer.flush();
|
|
531
|
+
observedSessions += 1;
|
|
532
|
+
}
|
|
533
|
+
if (nextOffset > candidate.startOffset) {
|
|
534
|
+
cursors[candidate.sessionId] = {
|
|
535
|
+
lastObservedOffset: nextOffset,
|
|
536
|
+
lastObservedAt: now().toISOString(),
|
|
537
|
+
sessionKey: candidate.sessionKey,
|
|
538
|
+
lastFileSize: candidate.fileSize
|
|
539
|
+
};
|
|
540
|
+
cursorUpdates += 1;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (cursorUpdates > 0) {
|
|
544
|
+
saveObserveCursorStore(vaultPath, cursors);
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
agentId,
|
|
548
|
+
sessionsDir,
|
|
549
|
+
checkedSessions: descriptors.length,
|
|
550
|
+
candidateSessions: candidates.length,
|
|
551
|
+
observedSessions,
|
|
552
|
+
cursorUpdates,
|
|
553
|
+
dryRun,
|
|
554
|
+
totalNewBytes: candidates.reduce((sum, candidate) => sum + candidate.newBytes, 0),
|
|
555
|
+
candidates
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/commands/observe.ts
|
|
560
|
+
function parsePositiveInteger(raw, optionName) {
|
|
561
|
+
const parsed = Number.parseInt(raw, 10);
|
|
562
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
563
|
+
throw new Error(`Invalid ${optionName}: ${raw}`);
|
|
564
|
+
}
|
|
565
|
+
return parsed;
|
|
566
|
+
}
|
|
567
|
+
function buildDaemonArgs(options) {
|
|
568
|
+
const cliPath = process.argv[1];
|
|
569
|
+
if (!cliPath) {
|
|
570
|
+
throw new Error("Unable to resolve CLI script path for daemon mode.");
|
|
571
|
+
}
|
|
572
|
+
const args = [cliPath, "observe"];
|
|
573
|
+
if (options.watch) {
|
|
574
|
+
args.push("--watch", options.watch);
|
|
575
|
+
}
|
|
576
|
+
if (options.threshold) {
|
|
577
|
+
args.push("--threshold", String(options.threshold));
|
|
578
|
+
}
|
|
579
|
+
if (options.reflectThreshold) {
|
|
580
|
+
args.push("--reflect-threshold", String(options.reflectThreshold));
|
|
581
|
+
}
|
|
582
|
+
if (options.model) {
|
|
583
|
+
args.push("--model", options.model);
|
|
584
|
+
}
|
|
585
|
+
if (options.vaultPath) {
|
|
586
|
+
args.push("--vault", options.vaultPath);
|
|
587
|
+
}
|
|
588
|
+
return args;
|
|
589
|
+
}
|
|
590
|
+
async function runOneShotCompression(observer, sourceFile, vaultPath) {
|
|
591
|
+
const resolved = path3.resolve(sourceFile);
|
|
592
|
+
if (!fs3.existsSync(resolved) || !fs3.statSync(resolved).isFile()) {
|
|
593
|
+
throw new Error(`Conversation file not found: ${resolved}`);
|
|
594
|
+
}
|
|
595
|
+
const messages = parseSessionFile(resolved);
|
|
596
|
+
await observer.processMessages(messages);
|
|
597
|
+
const { observations, routingSummary } = await observer.flush();
|
|
598
|
+
const datePart = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
599
|
+
const outputPath = path3.join(vaultPath, "observations", `${datePart}.md`);
|
|
600
|
+
console.log(`Observations updated: ${outputPath}`);
|
|
601
|
+
if (routingSummary) {
|
|
602
|
+
console.log(routingSummary);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function watchSessions(observer, watchPath) {
|
|
606
|
+
const watcher = new SessionWatcher(watchPath, observer);
|
|
607
|
+
await watcher.start();
|
|
608
|
+
console.log(`Watching session updates: ${watchPath}`);
|
|
609
|
+
await new Promise((resolve4) => {
|
|
610
|
+
const shutdown = async () => {
|
|
611
|
+
process.off("SIGINT", onSigInt);
|
|
612
|
+
process.off("SIGTERM", onSigTerm);
|
|
613
|
+
await watcher.stop();
|
|
614
|
+
resolve4();
|
|
615
|
+
};
|
|
616
|
+
const onSigInt = () => {
|
|
617
|
+
void shutdown();
|
|
618
|
+
};
|
|
619
|
+
const onSigTerm = () => {
|
|
620
|
+
void shutdown();
|
|
621
|
+
};
|
|
622
|
+
process.once("SIGINT", onSigInt);
|
|
623
|
+
process.once("SIGTERM", onSigTerm);
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
async function observeCommand(options) {
|
|
627
|
+
if (options.active && (options.watch || options.compress || options.daemon)) {
|
|
628
|
+
throw new Error("--active cannot be combined with --watch, --compress, or --daemon.");
|
|
629
|
+
}
|
|
630
|
+
if (options.compress && options.daemon) {
|
|
631
|
+
throw new Error("--compress cannot be combined with --daemon.");
|
|
632
|
+
}
|
|
633
|
+
const vaultPath = resolveVaultPath({ explicitPath: options.vaultPath });
|
|
634
|
+
if (options.active) {
|
|
635
|
+
const result = await observeActiveSessions({
|
|
636
|
+
vaultPath,
|
|
637
|
+
agentId: options.agent,
|
|
638
|
+
minNewBytes: options.minNew,
|
|
639
|
+
sessionsDir: options.sessionsDir,
|
|
640
|
+
dryRun: options.dryRun,
|
|
641
|
+
threshold: options.threshold,
|
|
642
|
+
reflectThreshold: options.reflectThreshold,
|
|
643
|
+
model: options.model
|
|
644
|
+
});
|
|
645
|
+
if (result.candidateSessions === 0) {
|
|
646
|
+
console.log(`No active sessions crossed threshold (${result.checkedSessions} checked).`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (result.dryRun) {
|
|
650
|
+
console.log(
|
|
651
|
+
`Dry run: ${result.candidateSessions} session(s) would be observed (${result.totalNewBytes} new bytes).`
|
|
652
|
+
);
|
|
653
|
+
for (const candidate of result.candidates) {
|
|
654
|
+
console.log(
|
|
655
|
+
`- ${candidate.sessionKey} [${candidate.sourceLabel}] \u0394${candidate.newBytes}B (threshold ${candidate.thresholdBytes}B)`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
console.log(
|
|
661
|
+
`Active observation complete: ${result.observedSessions}/${result.candidateSessions} session(s) observed.`
|
|
662
|
+
);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const observer = new Observer(vaultPath, {
|
|
666
|
+
tokenThreshold: options.threshold,
|
|
667
|
+
reflectThreshold: options.reflectThreshold,
|
|
668
|
+
model: options.model
|
|
669
|
+
});
|
|
670
|
+
if (options.compress) {
|
|
671
|
+
await runOneShotCompression(observer, options.compress, vaultPath);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
let watchPath = options.watch ? path3.resolve(options.watch) : "";
|
|
675
|
+
if (!watchPath && options.daemon) {
|
|
676
|
+
watchPath = path3.join(vaultPath, "sessions");
|
|
677
|
+
}
|
|
678
|
+
if (!watchPath) {
|
|
679
|
+
throw new Error("Either --watch or --compress must be provided.");
|
|
680
|
+
}
|
|
681
|
+
if (!fs3.existsSync(watchPath)) {
|
|
682
|
+
if (options.daemon && !options.watch) {
|
|
683
|
+
fs3.mkdirSync(watchPath, { recursive: true });
|
|
684
|
+
} else {
|
|
685
|
+
throw new Error(`Watch path does not exist: ${watchPath}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (options.daemon) {
|
|
689
|
+
const daemonArgs = buildDaemonArgs({ ...options, watch: watchPath, vaultPath });
|
|
690
|
+
const child = spawn(process.execPath, daemonArgs, {
|
|
691
|
+
detached: true,
|
|
692
|
+
stdio: "ignore"
|
|
693
|
+
});
|
|
694
|
+
child.unref();
|
|
695
|
+
console.log(`Observer daemon started (pid: ${child.pid})`);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
await watchSessions(observer, watchPath);
|
|
699
|
+
}
|
|
700
|
+
function registerObserveCommand(program) {
|
|
701
|
+
program.command("observe").description("Observe session files and build observational memory").option("--watch <path>", "Watch session file or directory").option("--active", "Observe active OpenClaw sessions incrementally").option("--agent <id>", "OpenClaw agent ID (default: OPENCLAW_AGENT_ID or clawdious)").option("--min-new <bytes>", "Override minimum new-content threshold in bytes").option("--sessions-dir <path>", "Override OpenClaw sessions directory").option("--dry-run", "Show active observation candidates without compressing").option("--threshold <n>", "Compression token threshold", "30000").option("--reflect-threshold <n>", "Reflection token threshold", "40000").option("--model <model>", "LLM model override").option("--compress <file>", "One-shot compression for a conversation file").option("--daemon", "Run in detached background mode").option("-v, --vault <path>", "Vault path").action(async (rawOptions) => {
|
|
702
|
+
await observeCommand({
|
|
703
|
+
watch: rawOptions.watch,
|
|
704
|
+
active: rawOptions.active,
|
|
705
|
+
agent: rawOptions.agent,
|
|
706
|
+
minNew: rawOptions.minNew ? parsePositiveInteger(rawOptions.minNew, "min-new") : void 0,
|
|
707
|
+
sessionsDir: rawOptions.sessionsDir,
|
|
708
|
+
dryRun: rawOptions.dryRun,
|
|
709
|
+
threshold: parsePositiveInteger(rawOptions.threshold, "threshold"),
|
|
710
|
+
reflectThreshold: parsePositiveInteger(rawOptions.reflectThreshold, "reflect-threshold"),
|
|
711
|
+
model: rawOptions.model,
|
|
712
|
+
compress: rawOptions.compress,
|
|
713
|
+
daemon: rawOptions.daemon,
|
|
714
|
+
vaultPath: rawOptions.vault
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export {
|
|
720
|
+
SessionWatcher,
|
|
721
|
+
observeCommand,
|
|
722
|
+
registerObserveCommand
|
|
723
|
+
};
|