condenclaw 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VVeeVal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # CondenClaw
2
+
3
+ `condenclaw` is a log condenser for OpenClaw agents. It reconstructs a clear timeline without noise, saving up to 99.9% (yes really) in token costs during debugging.
4
+
5
+ Even for simple failures default tool tail pulls all the logs resulting in hundreds of thousands of tokens polluting your context. Condenclaw gives exactly the same outcome in under a thousand.
6
+
7
+ ## Installation
8
+
9
+ Install globally via npm:
10
+ ```bash
11
+ npm install -g condenclaw
12
+ ```
13
+
14
+ ## OpenClaw Configuration
15
+
16
+ To save massively on tokens, add this to your `AGENTS.md` or system rules:
17
+
18
+ > "NEVER use `tail` to read your own session logs. When debugging or explaining a failure, ALWAYS run `condenclaw --limit -1` first for a structured overview. Use `condenclaw --limit -1 -v` for detailed event payloads, and `condenclaw --limit -1 --raw-event <n>` when you need the exact raw event that looks suspicious."
19
+
20
+ ## Usage
21
+
22
+ Analyze the latest agent activity:
23
+ ```bash
24
+ # Debug the latest run for your default OpenClaw agent
25
+ condenclaw --limit -1
26
+
27
+ # Debug the latest run for a specific agent
28
+ condenclaw --agent worker --limit -1
29
+
30
+ # Show the first 3 runs with detailed tool inputs and outputs
31
+ condenclaw --limit 3 -v
32
+
33
+ # Pull the exact raw payload for suspicious events 3 and 7
34
+ condenclaw --limit -1 --raw-event 3,7
35
+
36
+ ```
37
+
38
+ ## Layered Debugging
39
+
40
+ `condenclaw` is designed to be used in layers so agents can stay token-efficient until they need exact evidence:
41
+
42
+ 1. Condensed timeline: `condenclaw --limit -1`
43
+ 2. Detailed event view: `condenclaw --limit -1 -v`
44
+ 3. Exact raw event payloads: `condenclaw --limit -1 --raw-event 3` or `--raw-event 3,7`
45
+
46
+ ## Commands & Flags
47
+
48
+ | Flag | Description |
49
+ | :--- | :--- |
50
+ | `[file]` | Path to a log file or session JSONL. Defaults to the latest session for your default OpenClaw agent. |
51
+ | `--limit <N>` | Limit runs shown. Use negative numbers for latest (e.g. -1 is latest). |
52
+ | `--agent <id>` | Read sessions for a specific OpenClaw agent instead of the default agent. |
53
+ | `--session-dir <path>` | Override the session directory used for local session discovery. |
54
+ | `--session <path>`| Explicitly analyze a specific OpenClaw session JSONL file. |
55
+ | `--raw-event <numbers>` | Show exact raw payloads for specific 1-based event numbers, e.g. `3` or `3,7`. |
56
+ | `-v, --verbose` | Show the detailed event layer with expanded tool inputs and outputs. |
57
+ | `--json` | Output the behavioral timeline in machine-readable JSON. |
package/dist/index.js ADDED
@@ -0,0 +1,449 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { spawnSync } from 'child_process';
5
+ import { Command } from 'commander';
6
+ const ISO_TS_RE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?/;
7
+ const SUMMARY_PREVIEW = 100;
8
+ const DETAIL_PREVIEW = 2000;
9
+ const SUGGESTIONS = {
10
+ tool_loop: ["Avoid repeating identical tool calls.", "Limit retries to 2.", "Add state tracking to detect duplicate tool inputs."],
11
+ no_output: ["Ensure the agent produces a final response.", "Check termination conditions.", "Verify output is not overwritten by later steps."],
12
+ error: ["Inspect tool or model errors in the logs.", "Validate tool inputs before calling them.", "Add error handling or controlled retries."],
13
+ no_model_step: ["Ensure the model is invoked after user input.", "Check routing or planning logic before tool execution."],
14
+ };
15
+ function listSessionFiles(sessionDir) {
16
+ if (!fs.existsSync(sessionDir))
17
+ return [];
18
+ return fs
19
+ .readdirSync(sessionDir)
20
+ .filter(f => f.endsWith('.jsonl'))
21
+ .map(f => path.join(sessionDir, f));
22
+ }
23
+ function pickLatestFile(files) {
24
+ if (!files.length)
25
+ return null;
26
+ return files.sort((a, b) => fs.statSync(b).mtime.getTime() - fs.statSync(a).mtime.getTime())[0] || null;
27
+ }
28
+ function getOpenClawSessionListing(agent) {
29
+ const args = ['sessions'];
30
+ if (agent)
31
+ args.push('--agent', agent);
32
+ args.push('--json');
33
+ const result = spawnSync('openclaw', args, { encoding: 'utf-8' });
34
+ if (result.status !== 0 || !result.stdout.trim())
35
+ return null;
36
+ try {
37
+ return JSON.parse(result.stdout);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ function resolveSessionDirFromListing(listing, agent) {
44
+ if (listing.path)
45
+ return path.dirname(listing.path);
46
+ if (agent && listing.stores?.length) {
47
+ const store = listing.stores.find(s => s.agentId === agent && s.path);
48
+ if (store?.path)
49
+ return path.dirname(store.path);
50
+ }
51
+ return null;
52
+ }
53
+ function resolveSessionFileFromListing(listing) {
54
+ const sessions = [...(listing.sessions || [])];
55
+ if (!sessions.length)
56
+ return null;
57
+ sessions.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
58
+ const latest = sessions[0];
59
+ if (!latest)
60
+ return null;
61
+ if (latest.sessionFile)
62
+ return latest.sessionFile;
63
+ const sessionDir = resolveSessionDirFromListing(listing, latest.agentId);
64
+ if (!sessionDir || !latest.sessionId)
65
+ return null;
66
+ return path.join(sessionDir, `${latest.sessionId}.jsonl`);
67
+ }
68
+ function resolveDefaultSessionFile(agent, sessionDir) {
69
+ if (sessionDir) {
70
+ return pickLatestFile(listSessionFiles(sessionDir));
71
+ }
72
+ const listing = getOpenClawSessionListing(agent);
73
+ const fromListing = listing ? resolveSessionFileFromListing(listing) : null;
74
+ if (fromListing && fs.existsSync(fromListing))
75
+ return fromListing;
76
+ const home = process.env.HOME || process.env.USERPROFILE || '';
77
+ const fallbackDir = agent
78
+ ? path.join(home, '.openclaw', 'agents', agent, 'sessions')
79
+ : path.join(home, '.openclaw', 'agents', 'main', 'sessions');
80
+ return pickLatestFile(listSessionFiles(fallbackDir));
81
+ }
82
+ function parseTimestamp(line) {
83
+ if (typeof line === 'object' && line !== null) {
84
+ for (const key of ["time", "timestamp", "ts", "created_at"]) {
85
+ if (line[key]) {
86
+ const d = new Date(line[key]);
87
+ if (!isNaN(d.getTime()))
88
+ return d;
89
+ }
90
+ }
91
+ }
92
+ const match = String(line).match(ISO_TS_RE);
93
+ if (match) {
94
+ const d = new Date(match[0]);
95
+ if (!isNaN(d.getTime()))
96
+ return d;
97
+ }
98
+ return null;
99
+ }
100
+ function stringifyValue(value) {
101
+ if (typeof value === 'string')
102
+ return value;
103
+ try {
104
+ return JSON.stringify(value, null, 2);
105
+ }
106
+ catch {
107
+ return String(value);
108
+ }
109
+ }
110
+ function previewValue(value, maxChars) {
111
+ const text = stringifyValue(value);
112
+ return text.length > maxChars ? `${text.slice(0, maxChars)}...` : text;
113
+ }
114
+ function parseRawEventSelection(value) {
115
+ if (!value)
116
+ return [];
117
+ return value
118
+ .split(',')
119
+ .map(part => parseInt(part.trim(), 10))
120
+ .filter(n => Number.isInteger(n) && n > 0);
121
+ }
122
+ function extractSessionId(line) {
123
+ if (typeof line === 'object' && line !== null) {
124
+ for (const key of ["runId", "run_id", "sessionId", "session_id", "session"]) {
125
+ if (line[key])
126
+ return String(line[key]);
127
+ }
128
+ if (line._meta) {
129
+ for (const key of ["runId", "run_id", "sessionId", "session_id"]) {
130
+ if (line._meta[key])
131
+ return String(line._meta[key]);
132
+ }
133
+ }
134
+ if (line.data && typeof line.data === 'object') {
135
+ for (const key of ["runId", "run_id", "sessionId", "session_id"]) {
136
+ if (line.data[key])
137
+ return String(line.data[key]);
138
+ }
139
+ }
140
+ for (const argKey of ["0", "1"]) {
141
+ const arg = String(line[argKey] || "");
142
+ const match = arg.match(/runId["': ]+([A-Za-z0-9._-]+)/);
143
+ if (match)
144
+ return match[1];
145
+ }
146
+ }
147
+ return null;
148
+ }
149
+ function classifyEvent(line) {
150
+ const text = JSON.stringify(line).toLowerCase();
151
+ if (!text.includes("error") && !text.includes("failed")) {
152
+ const noiseMarkers = [
153
+ "heartbeat", "websocket", "ws connected", "ws disconnected",
154
+ "res ✓", "plugins", "starting provider", "canvas host mounted",
155
+ "device pairing auto-approved", "watchdog detected", "ready (", "log file:",
156
+ "node.list", "models.list", "chat.history", "status", "cron.list", "sessions.usage",
157
+ "openclaw:bootstrap-context"
158
+ ];
159
+ if (noiseMarkers.some(m => text.includes(m))) {
160
+ const behavioralIndicators = ["toolcall", "toolresult", "role\": \"user", "final response"];
161
+ if (!behavioralIndicators.some(b => text.includes(b)))
162
+ return "noise";
163
+ }
164
+ }
165
+ if (typeof line === 'object' && line !== null) {
166
+ if (line.role === "user")
167
+ return "user";
168
+ if (line.role === "toolResult")
169
+ return "tool_result";
170
+ }
171
+ if (text.includes("error") || text.includes("exception") || text.includes("traceback") || text.includes("failed"))
172
+ return "error";
173
+ if (text.includes("tool result") || text.includes("function result") || (text.includes("tool") && text.includes("output")))
174
+ return "tool_result";
175
+ if (text.includes("calling tool") || text.includes("tool call") || (text.includes("tool") && text.includes("input")))
176
+ return "tool_call";
177
+ if (text.includes("sent response") || text.includes("final response") || text.includes("reply sent"))
178
+ return "final_output";
179
+ if (text.includes("completion") || text.includes("model") || text.includes("llm step"))
180
+ return "llm";
181
+ if (text.includes("\"role\": \"user\"") || text.includes("received message"))
182
+ return "user";
183
+ return "other";
184
+ }
185
+ function extractSummary(line, eventType) {
186
+ if (typeof line === 'object' && line !== null) {
187
+ const msg = String(line.message || "");
188
+ const content = String(line.content || "");
189
+ const tool = line.tool || line.toolName || line.name || "unknown";
190
+ const arg0 = String(line["0"] || "");
191
+ const arg1 = String(line["1"] || "");
192
+ if (eventType === "tool_call") {
193
+ const inp = line.input || line.arguments || line.args || line["0"];
194
+ return `Tool call: ${tool} | input=${previewValue(inp || "", SUMMARY_PREVIEW)}`;
195
+ }
196
+ if (eventType === "tool_result") {
197
+ const res = line.output || line.result || line["1"];
198
+ return `Tool result: ${tool} | output=${previewValue(res || "", SUMMARY_PREVIEW)}`;
199
+ }
200
+ if (eventType === "llm")
201
+ return line.model ? `LLM call: ${line.model}` : (msg || content.slice(0, 120) || "LLM step");
202
+ if (eventType === "error") {
203
+ const err = line.error || line.cause || line["0"] || msg || content;
204
+ return `ERROR: ${String(err).slice(0, 120)}`;
205
+ }
206
+ if (eventType === "user") {
207
+ if (arg0.startsWith('{')) {
208
+ try {
209
+ const p = JSON.parse(arg0);
210
+ if (p.subsystem)
211
+ return `System (${p.subsystem}): ${arg1.slice(0, 120)}`;
212
+ }
213
+ catch { }
214
+ }
215
+ return `User: ${previewValue(content || msg || arg0 || line, 120)}`;
216
+ }
217
+ if (eventType === "final_output")
218
+ return `Final output: ${previewValue(content || msg || arg1 || "response sent", 120)}`;
219
+ }
220
+ return String(line).slice(0, 120);
221
+ }
222
+ function parseLines(rawLines) {
223
+ return rawLines.map((raw, idx) => {
224
+ if (!raw.trim())
225
+ return null;
226
+ try {
227
+ const parsed = JSON.parse(raw);
228
+ if (parsed.type === 'message' && parsed.message) {
229
+ const msg = parsed.message;
230
+ const pseudo = {
231
+ time: parsed.timestamp,
232
+ runId: parsed.runId,
233
+ sessionId: parsed.sessionId,
234
+ role: msg.role,
235
+ data: parsed.data
236
+ };
237
+ if (msg.role === 'assistant') {
238
+ const content = msg.content;
239
+ const call = Array.isArray(content) ? content.find((c) => c.type === 'toolCall') : null;
240
+ if (call) {
241
+ pseudo.type = 'tool_call';
242
+ pseudo.tool = call.name;
243
+ pseudo.input = call.arguments;
244
+ }
245
+ else {
246
+ pseudo.type = 'llm';
247
+ pseudo.content = Array.isArray(content) ? content.find((c) => c.type === 'text')?.text : content;
248
+ }
249
+ }
250
+ else if (msg.role === 'toolResult') {
251
+ pseudo.type = 'tool_result';
252
+ pseudo.tool = msg.toolName;
253
+ pseudo.output = msg.content;
254
+ }
255
+ else if (msg.role === 'user') {
256
+ const content = msg.content;
257
+ pseudo.type = 'user';
258
+ pseudo.content = Array.isArray(content) ? content.find((c) => c.type === 'text')?.text : content;
259
+ }
260
+ return { _line_no: idx + 1, _raw: raw, _parsed: pseudo };
261
+ }
262
+ return { _line_no: idx + 1, _raw: raw, _parsed: parsed };
263
+ }
264
+ catch {
265
+ if (raw.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
266
+ const parts = raw.split(" ", 4);
267
+ if (parts.length >= 4) {
268
+ return { _line_no: idx + 1, _raw: raw, _parsed: { time: parts[0], level: parts[1], subsystem: parts[2].replace(/[\[\]]/g, ''), "0": raw.split(" ").slice(4).join(" ") } };
269
+ }
270
+ }
271
+ return { _line_no: idx + 1, _raw: raw, _parsed: raw };
272
+ }
273
+ }).filter(l => l !== null);
274
+ }
275
+ function buildEvents(parsedLines) {
276
+ return parsedLines.map(item => {
277
+ const obj = item._parsed;
278
+ const type = classifyEvent(obj);
279
+ return {
280
+ line_no: item._line_no,
281
+ timestamp: parseTimestamp(obj),
282
+ session_id: extractSessionId(obj),
283
+ type,
284
+ summary: extractSummary(obj, type),
285
+ tool: obj.tool,
286
+ input: obj.input,
287
+ output: obj.output,
288
+ raw: obj
289
+ };
290
+ }).filter(e => e.type !== 'noise');
291
+ }
292
+ function splitRuns(events, gapSeconds = 60) {
293
+ const bySession = {};
294
+ const noSession = [];
295
+ for (const e of events) {
296
+ if (e.session_id) {
297
+ if (!bySession[e.session_id])
298
+ bySession[e.session_id] = [];
299
+ bySession[e.session_id].push(e);
300
+ }
301
+ else {
302
+ noSession.push(e);
303
+ }
304
+ }
305
+ const runs = Object.entries(bySession).map(([sid, evs]) => ({
306
+ session_id: sid,
307
+ events: evs.sort((a, b) => a.line_no - b.line_no),
308
+ max_line: Math.max(...evs.map(e => e.line_no))
309
+ }));
310
+ if (noSession.length) {
311
+ noSession.sort((a, b) => a.line_no - b.line_no);
312
+ let current = [];
313
+ for (const e of noSession) {
314
+ if (current.length && e.timestamp && current[current.length - 1].timestamp) {
315
+ const gap = (e.timestamp.getTime() - current[current.length - 1].timestamp.getTime()) / 1000;
316
+ if (gap > gapSeconds) {
317
+ runs.push({ session_id: 'other/system', events: current, max_line: Math.max(...current.map(ev => ev.line_no)) });
318
+ current = [];
319
+ }
320
+ }
321
+ current.push(e);
322
+ }
323
+ if (current.length)
324
+ runs.push({ session_id: 'other/system', events: current, max_line: Math.max(...current.map(ev => ev.line_no)) });
325
+ }
326
+ return runs.sort((a, b) => a.max_line - b.max_line);
327
+ }
328
+ function detectFailure(run) {
329
+ const types = run.events.map(e => e.type);
330
+ if (types.includes('error'))
331
+ return 'error';
332
+ const tcalls = run.events.filter(e => e.type === 'tool_call').map(e => e.tool || e.summary);
333
+ if (tcalls.length >= 3 && new Set(tcalls.slice(-3)).size === 1)
334
+ return 'tool_loop';
335
+ if (types.includes('user') && types.includes('llm') && !types.includes('final_output'))
336
+ return 'no_output';
337
+ if (types.includes('user') && !types.includes('llm') && !run.events.some(e => e.type === 'other'))
338
+ return 'no_model_step';
339
+ return 'ok';
340
+ }
341
+ function printRawEvents(run, rawEventNumbers) {
342
+ const selected = rawEventNumbers
343
+ .map(n => ({ eventNumber: n, event: run.events[n - 1] }))
344
+ .filter(item => item.event);
345
+ if (!selected.length)
346
+ return;
347
+ console.log("\nRaw events:");
348
+ selected.forEach(({ eventNumber, event }) => {
349
+ console.log(`\n--- Event ${eventNumber} (line ${event.line_no}) ---`);
350
+ console.log(stringifyValue(event.raw));
351
+ });
352
+ }
353
+ function printRun(run, idx, verbose, rawEventNumbers) {
354
+ const start = run.events[0].timestamp;
355
+ const end = run.events[run.events.length - 1].timestamp;
356
+ const duration = (start && end) ? `${((end.getTime() - start.getTime()) / 1000).toFixed(1)}s` : '';
357
+ console.log(`\n=== Run #${idx} | session=${run.session_id || 'no-session'} (${duration}) ===`);
358
+ run.events.forEach((e, i) => {
359
+ const ts = e.timestamp ? e.timestamp.toISOString().split('T')[1].split('.')[0] : '??:??:??';
360
+ console.log(`${String(i + 1).padStart(2, '0')}. [${ts}] line=${e.line_no} ${e.summary}`);
361
+ if (verbose) {
362
+ if (e.type === 'tool_call') {
363
+ console.log(` 👉 Input:\n${previewValue(e.input || "", DETAIL_PREVIEW)}`);
364
+ }
365
+ if (e.type === 'tool_result') {
366
+ console.log(` 👈 Output:\n${previewValue(e.output || "", DETAIL_PREVIEW)}`);
367
+ }
368
+ }
369
+ });
370
+ const ftype = detectFailure(run);
371
+ console.log(`\nStatus: ${ftype}`);
372
+ if (SUGGESTIONS[ftype]) {
373
+ console.log("\nSuggestions:");
374
+ SUGGESTIONS[ftype].forEach(s => console.log(`- ${s}`));
375
+ }
376
+ printRawEvents(run, rawEventNumbers);
377
+ }
378
+ async function main() {
379
+ const program = new Command();
380
+ program
381
+ .argument('[file]', 'Log file path')
382
+ .option('--json', 'Output JSON')
383
+ .option('--limit <n>', 'Limit total runs (e.g., -1 for latest)', (v) => parseInt(v))
384
+ .option('--agent <id>', 'Read sessions for a specific OpenClaw agent')
385
+ .option('--session-dir <path>', 'Directory containing OpenClaw session JSONL files')
386
+ .option('--session <path>', 'Specific session file')
387
+ .option('--raw-event <numbers>', 'Show exact raw payloads for specific event numbers, e.g. 3 or 3,7')
388
+ .option('-v, --verbose', 'Verbose tool data')
389
+ .parse(process.argv);
390
+ const options = program.opts();
391
+ let targetFile = program.args[0];
392
+ let inputLines = [];
393
+ if (options.session)
394
+ targetFile = options.session;
395
+ if (!targetFile) {
396
+ targetFile = resolveDefaultSessionFile(options.agent, options.sessionDir);
397
+ }
398
+ if (!targetFile) {
399
+ console.error('No OpenClaw session file found. Use --agent, --session-dir, or --session to target a transcript explicitly.');
400
+ process.exit(1);
401
+ }
402
+ const resolvedTargetFile = targetFile;
403
+ if (fs.existsSync(resolvedTargetFile)) {
404
+ inputLines = fs.readFileSync(resolvedTargetFile, 'utf-8').split('\n');
405
+ }
406
+ else {
407
+ console.error(`File not found: ${resolvedTargetFile}`);
408
+ process.exit(1);
409
+ }
410
+ const events = buildEvents(parseLines(inputLines));
411
+ const allRuns = splitRuns(events);
412
+ let displayRuns = allRuns;
413
+ const rawEventNumbers = parseRawEventSelection(options.rawEvent);
414
+ if (options.limit !== undefined) {
415
+ const limit = options.limit;
416
+ if (limit < 0) {
417
+ displayRuns = allRuns.slice(limit);
418
+ }
419
+ else if (limit > 0) {
420
+ displayRuns = allRuns.slice(0, limit);
421
+ }
422
+ }
423
+ if (options.json) {
424
+ const out = displayRuns.map(r => ({
425
+ run_number: allRuns.indexOf(r) + 1,
426
+ session_id: r.session_id,
427
+ failure_type: detectFailure(r),
428
+ suggestions: SUGGESTIONS[detectFailure(r)] || [],
429
+ events: r.events.map((e, i) => ({
430
+ event_number: i + 1,
431
+ line_no: e.line_no,
432
+ timestamp: e.timestamp,
433
+ type: e.type,
434
+ summary: e.summary,
435
+ detail: options.verbose ? {
436
+ tool: e.tool,
437
+ input: e.input,
438
+ output: e.output
439
+ } : undefined,
440
+ raw: rawEventNumbers.includes(i + 1) ? e.raw : undefined
441
+ }))
442
+ }));
443
+ console.log(JSON.stringify({ runs: out }, null, 2));
444
+ }
445
+ else {
446
+ displayRuns.forEach(r => printRun(r, allRuns.indexOf(r) + 1, !!options.verbose, rawEventNumbers));
447
+ }
448
+ }
449
+ main();
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "condenclaw",
3
+ "version": "0.1.0",
4
+ "description": "Log condenser for OpenClaw agents",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "condenclaw": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "ts-node src/index.ts"
14
+ },
15
+ "dependencies": {
16
+ "commander": "^12.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.4.0",
20
+ "@types/node": "^20.0.0"
21
+ },
22
+ "type": "module"
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { spawnSync } from 'child_process';
5
+ import { Command } from 'commander';
6
+
7
+ interface LogEvent {
8
+ line_no: number;
9
+ timestamp: Date | null;
10
+ session_id: string | null;
11
+ type: string;
12
+ summary: string;
13
+ tool?: string;
14
+ input?: any;
15
+ output?: any;
16
+ raw: any;
17
+ }
18
+
19
+ interface Run {
20
+ session_id: string | null;
21
+ events: LogEvent[];
22
+ max_line: number;
23
+ }
24
+
25
+ interface SessionListing {
26
+ path?: string | null;
27
+ stores?: Array<{ agentId?: string; path?: string | null }>;
28
+ sessions?: Array<{ sessionId?: string; sessionFile?: string; updatedAt?: number; agentId?: string }>;
29
+ }
30
+
31
+ const ISO_TS_RE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?/;
32
+ const SUMMARY_PREVIEW = 100;
33
+ const DETAIL_PREVIEW = 2000;
34
+ const SUGGESTIONS: Record<string, string[]> = {
35
+ tool_loop: ["Avoid repeating identical tool calls.", "Limit retries to 2.", "Add state tracking to detect duplicate tool inputs."],
36
+ no_output: ["Ensure the agent produces a final response.", "Check termination conditions.", "Verify output is not overwritten by later steps."],
37
+ error: ["Inspect tool or model errors in the logs.", "Validate tool inputs before calling them.", "Add error handling or controlled retries."],
38
+ no_model_step: ["Ensure the model is invoked after user input.", "Check routing or planning logic before tool execution."],
39
+ };
40
+
41
+ function listSessionFiles(sessionDir: string): string[] {
42
+ if (!fs.existsSync(sessionDir)) return [];
43
+ return fs
44
+ .readdirSync(sessionDir)
45
+ .filter(f => f.endsWith('.jsonl'))
46
+ .map(f => path.join(sessionDir, f));
47
+ }
48
+
49
+ function pickLatestFile(files: string[]): string | null {
50
+ if (!files.length) return null;
51
+ return files.sort((a, b) => fs.statSync(b).mtime.getTime() - fs.statSync(a).mtime.getTime())[0] || null;
52
+ }
53
+
54
+ function getOpenClawSessionListing(agent?: string): SessionListing | null {
55
+ const args = ['sessions'];
56
+ if (agent) args.push('--agent', agent);
57
+ args.push('--json');
58
+
59
+ const result = spawnSync('openclaw', args, { encoding: 'utf-8' });
60
+ if (result.status !== 0 || !result.stdout.trim()) return null;
61
+
62
+ try {
63
+ return JSON.parse(result.stdout) as SessionListing;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ function resolveSessionDirFromListing(listing: SessionListing, agent?: string): string | null {
70
+ if (listing.path) return path.dirname(listing.path);
71
+ if (agent && listing.stores?.length) {
72
+ const store = listing.stores.find(s => s.agentId === agent && s.path);
73
+ if (store?.path) return path.dirname(store.path);
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function resolveSessionFileFromListing(listing: SessionListing): string | null {
79
+ const sessions = [...(listing.sessions || [])];
80
+ if (!sessions.length) return null;
81
+
82
+ sessions.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
83
+ const latest = sessions[0];
84
+ if (!latest) return null;
85
+ if (latest.sessionFile) return latest.sessionFile;
86
+
87
+ const sessionDir = resolveSessionDirFromListing(listing, latest.agentId);
88
+ if (!sessionDir || !latest.sessionId) return null;
89
+
90
+ return path.join(sessionDir, `${latest.sessionId}.jsonl`);
91
+ }
92
+
93
+ function resolveDefaultSessionFile(agent?: string, sessionDir?: string): string | null {
94
+ if (sessionDir) {
95
+ return pickLatestFile(listSessionFiles(sessionDir));
96
+ }
97
+
98
+ const listing = getOpenClawSessionListing(agent);
99
+ const fromListing = listing ? resolveSessionFileFromListing(listing) : null;
100
+ if (fromListing && fs.existsSync(fromListing)) return fromListing;
101
+
102
+ const home = process.env.HOME || process.env.USERPROFILE || '';
103
+ const fallbackDir = agent
104
+ ? path.join(home, '.openclaw', 'agents', agent, 'sessions')
105
+ : path.join(home, '.openclaw', 'agents', 'main', 'sessions');
106
+
107
+ return pickLatestFile(listSessionFiles(fallbackDir));
108
+ }
109
+
110
+ function parseTimestamp(line: any): Date | null {
111
+ if (typeof line === 'object' && line !== null) {
112
+ for (const key of ["time", "timestamp", "ts", "created_at"]) {
113
+ if (line[key]) {
114
+ const d = new Date(line[key]);
115
+ if (!isNaN(d.getTime())) return d;
116
+ }
117
+ }
118
+ }
119
+ const match = String(line).match(ISO_TS_RE);
120
+ if (match) {
121
+ const d = new Date(match[0]);
122
+ if (!isNaN(d.getTime())) return d;
123
+ }
124
+ return null;
125
+ }
126
+
127
+ function stringifyValue(value: any): string {
128
+ if (typeof value === 'string') return value;
129
+ try {
130
+ return JSON.stringify(value, null, 2);
131
+ } catch {
132
+ return String(value);
133
+ }
134
+ }
135
+
136
+ function previewValue(value: any, maxChars: number): string {
137
+ const text = stringifyValue(value);
138
+ return text.length > maxChars ? `${text.slice(0, maxChars)}...` : text;
139
+ }
140
+
141
+ function parseRawEventSelection(value?: string): number[] {
142
+ if (!value) return [];
143
+
144
+ return value
145
+ .split(',')
146
+ .map(part => parseInt(part.trim(), 10))
147
+ .filter(n => Number.isInteger(n) && n > 0);
148
+ }
149
+
150
+ function extractSessionId(line: any): string | null {
151
+ if (typeof line === 'object' && line !== null) {
152
+ for (const key of ["runId", "run_id", "sessionId", "session_id", "session"]) {
153
+ if (line[key]) return String(line[key]);
154
+ }
155
+ if (line._meta) {
156
+ for (const key of ["runId", "run_id", "sessionId", "session_id"]) {
157
+ if (line._meta[key]) return String(line._meta[key]);
158
+ }
159
+ }
160
+ if (line.data && typeof line.data === 'object') {
161
+ for (const key of ["runId", "run_id", "sessionId", "session_id"]) {
162
+ if (line.data[key]) return String(line.data[key]);
163
+ }
164
+ }
165
+ for (const argKey of ["0", "1"]) {
166
+ const arg = String(line[argKey] || "");
167
+ const match = arg.match(/runId["': ]+([A-Za-z0-9._-]+)/);
168
+ if (match) return match[1];
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+
174
+ function classifyEvent(line: any): string {
175
+ const text = JSON.stringify(line).toLowerCase();
176
+
177
+ if (!text.includes("error") && !text.includes("failed")) {
178
+ const noiseMarkers = [
179
+ "heartbeat", "websocket", "ws connected", "ws disconnected",
180
+ "res ✓", "plugins", "starting provider", "canvas host mounted",
181
+ "device pairing auto-approved", "watchdog detected", "ready (", "log file:",
182
+ "node.list", "models.list", "chat.history", "status", "cron.list", "sessions.usage",
183
+ "openclaw:bootstrap-context"
184
+ ];
185
+ if (noiseMarkers.some(m => text.includes(m))) {
186
+ const behavioralIndicators = ["toolcall", "toolresult", "role\": \"user", "final response"];
187
+ if (!behavioralIndicators.some(b => text.includes(b))) return "noise";
188
+ }
189
+ }
190
+
191
+ if (typeof line === 'object' && line !== null) {
192
+ if (line.role === "user") return "user";
193
+ if (line.role === "toolResult") return "tool_result";
194
+ }
195
+
196
+ if (text.includes("error") || text.includes("exception") || text.includes("traceback") || text.includes("failed")) return "error";
197
+ if (text.includes("tool result") || text.includes("function result") || (text.includes("tool") && text.includes("output"))) return "tool_result";
198
+ if (text.includes("calling tool") || text.includes("tool call") || (text.includes("tool") && text.includes("input"))) return "tool_call";
199
+ if (text.includes("sent response") || text.includes("final response") || text.includes("reply sent")) return "final_output";
200
+ if (text.includes("completion") || text.includes("model") || text.includes("llm step")) return "llm";
201
+ if (text.includes("\"role\": \"user\"") || text.includes("received message")) return "user";
202
+
203
+ return "other";
204
+ }
205
+
206
+ function extractSummary(line: any, eventType: string): string {
207
+ if (typeof line === 'object' && line !== null) {
208
+ const msg = String(line.message || "");
209
+ const content = String(line.content || "");
210
+ const tool = line.tool || line.toolName || line.name || "unknown";
211
+ const arg0 = String(line["0"] || "");
212
+ const arg1 = String(line["1"] || "");
213
+
214
+ if (eventType === "tool_call") {
215
+ const inp = line.input || line.arguments || line.args || line["0"];
216
+ return `Tool call: ${tool} | input=${previewValue(inp || "", SUMMARY_PREVIEW)}`;
217
+ }
218
+ if (eventType === "tool_result") {
219
+ const res = line.output || line.result || line["1"];
220
+ return `Tool result: ${tool} | output=${previewValue(res || "", SUMMARY_PREVIEW)}`;
221
+ }
222
+ if (eventType === "llm") return line.model ? `LLM call: ${line.model}` : (msg || content.slice(0, 120) || "LLM step");
223
+ if (eventType === "error") {
224
+ const err = line.error || line.cause || line["0"] || msg || content;
225
+ return `ERROR: ${String(err).slice(0, 120)}`;
226
+ }
227
+ if (eventType === "user") {
228
+ if (arg0.startsWith('{')) {
229
+ try {
230
+ const p = JSON.parse(arg0);
231
+ if (p.subsystem) return `System (${p.subsystem}): ${arg1.slice(0, 120)}`;
232
+ } catch {}
233
+ }
234
+ return `User: ${previewValue(content || msg || arg0 || line, 120)}`;
235
+ }
236
+ if (eventType === "final_output") return `Final output: ${previewValue(content || msg || arg1 || "response sent", 120)}`;
237
+ }
238
+ return String(line).slice(0, 120);
239
+ }
240
+
241
+ function parseLines(rawLines: string[]): any[] {
242
+ return rawLines.map((raw, idx) => {
243
+ if (!raw.trim()) return null;
244
+ try {
245
+ const parsed = JSON.parse(raw);
246
+ if (parsed.type === 'message' && parsed.message) {
247
+ const msg = parsed.message;
248
+ const pseudo: any = {
249
+ time: parsed.timestamp,
250
+ runId: parsed.runId,
251
+ sessionId: parsed.sessionId,
252
+ role: msg.role,
253
+ data: parsed.data
254
+ };
255
+ if (msg.role === 'assistant') {
256
+ const content = msg.content;
257
+ const call = Array.isArray(content) ? content.find((c: any) => c.type === 'toolCall') : null;
258
+ if (call) {
259
+ pseudo.type = 'tool_call';
260
+ pseudo.tool = call.name;
261
+ pseudo.input = call.arguments;
262
+ } else {
263
+ pseudo.type = 'llm';
264
+ pseudo.content = Array.isArray(content) ? content.find((c: any) => c.type === 'text')?.text : content;
265
+ }
266
+ } else if (msg.role === 'toolResult') {
267
+ pseudo.type = 'tool_result';
268
+ pseudo.tool = msg.toolName;
269
+ pseudo.output = msg.content;
270
+ } else if (msg.role === 'user') {
271
+ const content = msg.content;
272
+ pseudo.type = 'user';
273
+ pseudo.content = Array.isArray(content) ? content.find((c: any) => c.type === 'text')?.text : content;
274
+ }
275
+ return { _line_no: idx + 1, _raw: raw, _parsed: pseudo };
276
+ }
277
+ return { _line_no: idx + 1, _raw: raw, _parsed: parsed };
278
+ } catch {
279
+ if (raw.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
280
+ const parts = raw.split(" ", 4);
281
+ if (parts.length >= 4) {
282
+ return { _line_no: idx + 1, _raw: raw, _parsed: { time: parts[0], level: parts[1], subsystem: parts[2].replace(/[\[\]]/g, ''), "0": raw.split(" ").slice(4).join(" ") } };
283
+ }
284
+ }
285
+ return { _line_no: idx + 1, _raw: raw, _parsed: raw };
286
+ }
287
+ }).filter(l => l !== null);
288
+ }
289
+
290
+ function buildEvents(parsedLines: any[]): LogEvent[] {
291
+ return parsedLines.map(item => {
292
+ const obj = item._parsed;
293
+ const type = classifyEvent(obj);
294
+ return {
295
+ line_no: item._line_no,
296
+ timestamp: parseTimestamp(obj),
297
+ session_id: extractSessionId(obj),
298
+ type,
299
+ summary: extractSummary(obj, type),
300
+ tool: obj.tool,
301
+ input: obj.input,
302
+ output: obj.output,
303
+ raw: obj
304
+ };
305
+ }).filter(e => e.type !== 'noise');
306
+ }
307
+
308
+ function splitRuns(events: LogEvent[], gapSeconds = 60): Run[] {
309
+ const bySession: Record<string, LogEvent[]> = {};
310
+ const noSession: LogEvent[] = [];
311
+
312
+ for (const e of events) {
313
+ if (e.session_id) {
314
+ if (!bySession[e.session_id]) bySession[e.session_id] = [];
315
+ bySession[e.session_id].push(e);
316
+ } else {
317
+ noSession.push(e);
318
+ }
319
+ }
320
+
321
+ const runs: Run[] = Object.entries(bySession).map(([sid, evs]) => ({
322
+ session_id: sid,
323
+ events: evs.sort((a, b) => a.line_no - b.line_no),
324
+ max_line: Math.max(...evs.map(e => e.line_no))
325
+ }));
326
+
327
+ if (noSession.length) {
328
+ noSession.sort((a, b) => a.line_no - b.line_no);
329
+ let current: LogEvent[] = [];
330
+ for (const e of noSession) {
331
+ if (current.length && e.timestamp && current[current.length - 1].timestamp) {
332
+ const gap = (e.timestamp.getTime() - current[current.length - 1].timestamp!.getTime()) / 1000;
333
+ if (gap > gapSeconds) {
334
+ runs.push({ session_id: 'other/system', events: current, max_line: Math.max(...current.map(ev => ev.line_no)) });
335
+ current = [];
336
+ }
337
+ }
338
+ current.push(e);
339
+ }
340
+ if (current.length) runs.push({ session_id: 'other/system', events: current, max_line: Math.max(...current.map(ev => ev.line_no)) });
341
+ }
342
+
343
+ return runs.sort((a, b) => a.max_line - b.max_line);
344
+ }
345
+
346
+ function detectFailure(run: Run): string {
347
+ const types = run.events.map(e => e.type);
348
+ if (types.includes('error')) return 'error';
349
+ const tcalls = run.events.filter(e => e.type === 'tool_call').map(e => e.tool || e.summary);
350
+ if (tcalls.length >= 3 && new Set(tcalls.slice(-3)).size === 1) return 'tool_loop';
351
+ if (types.includes('user') && types.includes('llm') && !types.includes('final_output')) return 'no_output';
352
+ if (types.includes('user') && !types.includes('llm') && !run.events.some(e => e.type === 'other')) return 'no_model_step';
353
+ return 'ok';
354
+ }
355
+
356
+ function printRawEvents(run: Run, rawEventNumbers: number[]) {
357
+ const selected = rawEventNumbers
358
+ .map(n => ({ eventNumber: n, event: run.events[n - 1] }))
359
+ .filter(item => item.event);
360
+
361
+ if (!selected.length) return;
362
+
363
+ console.log("\nRaw events:");
364
+ selected.forEach(({ eventNumber, event }) => {
365
+ console.log(`\n--- Event ${eventNumber} (line ${event!.line_no}) ---`);
366
+ console.log(stringifyValue(event!.raw));
367
+ });
368
+ }
369
+
370
+ function printRun(run: Run, idx: number, verbose: boolean, rawEventNumbers: number[]) {
371
+ const start = run.events[0].timestamp;
372
+ const end = run.events[run.events.length - 1].timestamp;
373
+ const duration = (start && end) ? `${((end.getTime() - start.getTime()) / 1000).toFixed(1)}s` : '';
374
+
375
+ console.log(`\n=== Run #${idx} | session=${run.session_id || 'no-session'} (${duration}) ===`);
376
+ run.events.forEach((e, i) => {
377
+ const ts = e.timestamp ? e.timestamp.toISOString().split('T')[1].split('.')[0] : '??:??:??';
378
+ console.log(`${String(i + 1).padStart(2, '0')}. [${ts}] line=${e.line_no} ${e.summary}`);
379
+ if (verbose) {
380
+ if (e.type === 'tool_call') {
381
+ console.log(` 👉 Input:\n${previewValue(e.input || "", DETAIL_PREVIEW)}`);
382
+ }
383
+ if (e.type === 'tool_result') {
384
+ console.log(` 👈 Output:\n${previewValue(e.output || "", DETAIL_PREVIEW)}`);
385
+ }
386
+ }
387
+ });
388
+
389
+ const ftype = detectFailure(run);
390
+ console.log(`\nStatus: ${ftype}`);
391
+ if (SUGGESTIONS[ftype]) {
392
+ console.log("\nSuggestions:");
393
+ SUGGESTIONS[ftype].forEach(s => console.log(`- ${s}`));
394
+ }
395
+
396
+ printRawEvents(run, rawEventNumbers);
397
+ }
398
+
399
+ async function main() {
400
+ const program = new Command();
401
+ program
402
+ .argument('[file]', 'Log file path')
403
+ .option('--json', 'Output JSON')
404
+ .option('--limit <n>', 'Limit total runs (e.g., -1 for latest)', (v: string) => parseInt(v))
405
+ .option('--agent <id>', 'Read sessions for a specific OpenClaw agent')
406
+ .option('--session-dir <path>', 'Directory containing OpenClaw session JSONL files')
407
+ .option('--session <path>', 'Specific session file')
408
+ .option('--raw-event <numbers>', 'Show exact raw payloads for specific event numbers, e.g. 3 or 3,7')
409
+ .option('-v, --verbose', 'Verbose tool data')
410
+ .parse(process.argv);
411
+
412
+ const options = program.opts();
413
+ let targetFile: string | null | undefined = program.args[0];
414
+ let inputLines: string[] = [];
415
+
416
+ if (options.session) targetFile = options.session;
417
+ if (!targetFile) {
418
+ targetFile = resolveDefaultSessionFile(options.agent, options.sessionDir);
419
+ }
420
+ if (!targetFile) {
421
+ console.error('No OpenClaw session file found. Use --agent, --session-dir, or --session to target a transcript explicitly.');
422
+ process.exit(1);
423
+ }
424
+ const resolvedTargetFile = targetFile;
425
+ if (fs.existsSync(resolvedTargetFile)) {
426
+ inputLines = fs.readFileSync(resolvedTargetFile, 'utf-8').split('\n');
427
+ } else {
428
+ console.error(`File not found: ${resolvedTargetFile}`);
429
+ process.exit(1);
430
+ }
431
+
432
+ const events = buildEvents(parseLines(inputLines));
433
+ const allRuns = splitRuns(events);
434
+ let displayRuns = allRuns;
435
+ const rawEventNumbers = parseRawEventSelection(options.rawEvent);
436
+
437
+ if (options.limit !== undefined) {
438
+ const limit = options.limit;
439
+ if (limit < 0) {
440
+ displayRuns = allRuns.slice(limit);
441
+ } else if (limit > 0) {
442
+ displayRuns = allRuns.slice(0, limit);
443
+ }
444
+ }
445
+
446
+ if (options.json) {
447
+ const out = displayRuns.map(r => ({
448
+ run_number: allRuns.indexOf(r) + 1,
449
+ session_id: r.session_id,
450
+ failure_type: detectFailure(r),
451
+ suggestions: SUGGESTIONS[detectFailure(r)] || [],
452
+ events: r.events.map((e, i) => ({
453
+ event_number: i + 1,
454
+ line_no: e.line_no,
455
+ timestamp: e.timestamp,
456
+ type: e.type,
457
+ summary: e.summary,
458
+ detail: options.verbose ? {
459
+ tool: e.tool,
460
+ input: e.input,
461
+ output: e.output
462
+ } : undefined,
463
+ raw: rawEventNumbers.includes(i + 1) ? e.raw : undefined
464
+ }))
465
+ }));
466
+ console.log(JSON.stringify({ runs: out }, null, 2));
467
+ } else {
468
+ displayRuns.forEach(r => printRun(r, allRuns.indexOf(r) + 1, !!options.verbose, rawEventNumbers));
469
+ }
470
+ }
471
+
472
+ main();
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true
12
+ },
13
+ "include": ["src/**/*"]
14
+ }