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 +21 -0
- package/README.md +57 -0
- package/dist/index.js +449 -0
- package/package.json +23 -0
- package/src/index.ts +472 -0
- package/tsconfig.json +14 -0
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
|
+
}
|