lvwerra HF Staff commited on
Commit
ad9c300
·
verified ·
1 Parent(s): fd5db99

Parse OpenClaw traces for the Overview digest; single hairline on empty cards

Browse files
Files changed (2) hide show
  1. server/src/traces.js +73 -0
  2. 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); }