Spaces:
Running
Running
Parse OpenClaw traces for the Overview digest; single hairline on empty cards
Browse files- server/src/traces.js +73 -0
- web/src/styles.css +2 -0
server/src/traces.js
CHANGED
|
@@ -188,6 +188,71 @@ function parseCodex(txt) {
|
|
| 188 |
return { stats: st, digest: dg };
|
| 189 |
}
|
| 190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
async function statsFor(p, parser) {
|
| 192 |
let m;
|
| 193 |
try { m = await fsp.stat(p); } catch { return null; }
|
|
@@ -286,6 +351,14 @@ async function build() {
|
|
| 286 |
attribute(session, parsed);
|
| 287 |
}
|
| 288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
return { perSession, digests, other, totals, sessions };
|
| 290 |
}
|
| 291 |
|
|
|
|
| 188 |
return { stats: st, digest: dg };
|
| 189 |
}
|
| 190 |
|
| 191 |
+
// ---------- OpenClaw sessions (~/.openclaw/agents/*/sessions/<uuid>.jsonl) ----------
|
| 192 |
+
// Line shape: { type: 'message', timestamp, message: { role, content: [{type:'text',text}],
|
| 193 |
+
// usage: { input, output, cacheRead, ... } } } — other line types are metadata.
|
| 194 |
+
const ocText = (m) => Array.isArray(m.content)
|
| 195 |
+
? m.content.filter((c) => c && c.type === 'text' && c.text).map((c) => c.text).join(' ')
|
| 196 |
+
: (typeof m.content === 'string' ? m.content : '');
|
| 197 |
+
|
| 198 |
+
function parseOpenClaw(txt) {
|
| 199 |
+
const st = emptyStats();
|
| 200 |
+
const dg = emptyDigest();
|
| 201 |
+
st.files = 1;
|
| 202 |
+
for (const line of txt.split('\n')) {
|
| 203 |
+
if (!line) continue;
|
| 204 |
+
let j; try { j = JSON.parse(line); } catch { continue; }
|
| 205 |
+
if (j.timestamp) addTs(st, j.timestamp);
|
| 206 |
+
if (j.type !== 'message' || !j.message) continue;
|
| 207 |
+
const m = j.message;
|
| 208 |
+
const text = ocText(m);
|
| 209 |
+
if (m.role === 'user') {
|
| 210 |
+
st.prompts++;
|
| 211 |
+
if (text.trim() && !text.trim().startsWith('<')) digestPrompt(dg, text, j.timestamp);
|
| 212 |
+
} else if (m.role === 'assistant') {
|
| 213 |
+
st.turns++;
|
| 214 |
+
dg.sinceTurns++;
|
| 215 |
+
if (text.trim()) digestAssistant(dg, text, j.timestamp);
|
| 216 |
+
const u = m.usage;
|
| 217 |
+
if (u) {
|
| 218 |
+
const cached = u.cacheRead || 0;
|
| 219 |
+
st.tokensIn += Math.max(0, (u.input || 0) - cached);
|
| 220 |
+
st.cacheRead += cached;
|
| 221 |
+
st.tokensOut += u.output || 0;
|
| 222 |
+
dg.sinceTokens += (u.input || 0) + (u.output || 0);
|
| 223 |
+
}
|
| 224 |
+
if (Array.isArray(m.content)) {
|
| 225 |
+
for (const cb of m.content) {
|
| 226 |
+
if (cb && typeof cb.type === 'string' && /tool/i.test(cb.type)) {
|
| 227 |
+
st.toolCalls++;
|
| 228 |
+
const name = cb.name || cb.toolName || 'tool';
|
| 229 |
+
st.tools[name] = (st.tools[name] || 0) + 1;
|
| 230 |
+
digestTool(dg, name, null);
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
return { stats: st, digest: dg };
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
async function openclawFiles() {
|
| 240 |
+
const root = path.join(process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || '', '.openclaw'), 'agents');
|
| 241 |
+
const out = [];
|
| 242 |
+
let agents = [];
|
| 243 |
+
try { agents = await fsp.readdir(root, { withFileTypes: true }); } catch { return out; }
|
| 244 |
+
for (const a of agents) {
|
| 245 |
+
if (!a.isDirectory()) continue;
|
| 246 |
+
const dir = path.join(root, a.name, 'sessions');
|
| 247 |
+
let files = [];
|
| 248 |
+
try { files = await fsp.readdir(dir); } catch { continue; }
|
| 249 |
+
for (const f of files) {
|
| 250 |
+
if (f.endsWith('.jsonl') && !f.includes('.trajectory')) out.push(path.join(dir, f));
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
return out;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
async function statsFor(p, parser) {
|
| 257 |
let m;
|
| 258 |
try { m = await fsp.stat(p); } catch { return null; }
|
|
|
|
| 351 |
attribute(session, parsed);
|
| 352 |
}
|
| 353 |
|
| 354 |
+
// OpenClaw: every pane shares the ONE embedded agent (agent "main"), so its
|
| 355 |
+
// traces belong to the single OpenClaw session when unambiguous, else other.
|
| 356 |
+
const ocSessions = sessions.filter((s) => s.cli === 'openclaw');
|
| 357 |
+
const ocOwner = ocSessions.length === 1 ? ocSessions[0] : null;
|
| 358 |
+
for (const p of await openclawFiles()) {
|
| 359 |
+
attribute(ocOwner, await statsFor(p, parseOpenClaw));
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
return { perSession, digests, other, totals, sessions };
|
| 363 |
}
|
| 364 |
|
web/src/styles.css
CHANGED
|
@@ -219,6 +219,8 @@ body {
|
|
| 219 |
.ov-card:hover .ov-go, .ov-id:focus-visible .ov-go { opacity: 1; }
|
| 220 |
|
| 221 |
.ov-prompt { font-size: 12.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
|
|
|
|
|
|
|
| 222 |
.ov-prompt::before { content: '❯ '; color: var(--accent); font-weight: 700; }
|
| 223 |
.ov-prompt-none { font-style: italic; }
|
| 224 |
.ov-prompt-none::before { color: var(--border-strong); }
|
|
|
|
| 219 |
.ov-card:hover .ov-go, .ov-id:focus-visible .ov-go { opacity: 1; }
|
| 220 |
|
| 221 |
.ov-prompt { font-size: 12.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
|
| 222 |
+
/* nothing between prompt and reply → one hairline, not two */
|
| 223 |
+
.ov-prompt + .ov-ghost, .ov-prompt + .ov-live { border-top: none; }
|
| 224 |
.ov-prompt::before { content: '❯ '; color: var(--accent); font-weight: 700; }
|
| 225 |
.ov-prompt-none { font-style: italic; }
|
| 226 |
.ov-prompt-none::before { color: var(--border-strong); }
|