vibeusage 0.3.1 → 0.3.2

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.
@@ -3,6 +3,7 @@ const path = require("node:path");
3
3
  const fs = require("node:fs/promises");
4
4
  const fssync = require("node:fs");
5
5
  const cp = require("node:child_process");
6
+ const { pathToFileURL } = require("node:url");
6
7
 
7
8
  const OPENCLAW_SESSION_PLUGIN_ID = "openclaw-session-sync";
8
9
  const OPENCLAW_SESSION_PLUGIN_DIRNAME = "openclaw-plugin";
@@ -103,7 +104,6 @@ async function ensureOpenclawSessionPluginFiles({
103
104
  buildSessionPluginIndex({
104
105
  trackerDir,
105
106
  packageName,
106
- openclawHome: openclawHome || path.join(os.homedir(), ".openclaw"),
107
107
  }),
108
108
  "utf8",
109
109
  );
@@ -337,144 +337,58 @@ function buildSessionPluginMeta() {
337
337
  )}\n`;
338
338
  }
339
339
 
340
- function buildSessionPluginIndex({ trackerDir, packageName = "vibeusage", openclawHome }) {
341
- const trackerBinPath = path.join(trackerDir, "app", "bin", "tracker.js");
342
- const fallbackPkg = packageName || "vibeusage";
343
- const safeOpenclawHome = openclawHome || path.join(os.homedir(), ".openclaw");
340
+ function buildSessionPluginIndex({ trackerDir }) {
341
+ const ledgerModuleUrl = pathToFileURL(
342
+ path.join(trackerDir, "app", "src", "lib", "openclaw-usage-ledger.js"),
343
+ ).href;
344
344
 
345
345
  return (
346
- `import fs from 'node:fs';\n` +
347
- `import path from 'node:path';\n` +
348
- `import cp from 'node:child_process';\n` +
346
+ `import { appendOpenclawUsageEvent } from ${JSON.stringify(ledgerModuleUrl)};\n` +
349
347
  `\n` +
350
348
  `const trackerDir = ${JSON.stringify(trackerDir)};\n` +
351
- `const trackerBinPath = ${JSON.stringify(trackerBinPath)};\n` +
352
- `const fallbackPkg = ${JSON.stringify(fallbackPkg)};\n` +
353
- `const openclawHome = ${JSON.stringify(safeOpenclawHome)};\n` +
354
- `const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');\n` +
355
- `const triggerStatePath = path.join(trackerDir, 'openclaw.session-sync.trigger-state.json');\n` +
356
- `const SESSION_TRIGGER_THROTTLE_MS = 15_000;\n` +
357
349
  `\n` +
358
350
  `export default function register(api) {\n` +
359
- ` api.on('agent_end', async (_event, ctx) => {\n` +
351
+ ` api.on('llm_output', async (event, ctx) => {\n` +
360
352
  ` try {\n` +
361
- ` const sessionKey = normalize(ctx && ctx.sessionKey);\n` +
362
- ` if (!sessionKey) return;\n` +
363
- `\n` +
364
- ` const agentId = normalize(ctx && ctx.agentId) || parseAgentId(sessionKey);\n` +
365
- ` if (!agentId) return;\n` +
366
- `\n` +
367
- ` const sessionInfo = resolveSessionInfo(agentId, sessionKey);\n` +
368
- ` const sessionId = normalize(sessionInfo && sessionInfo.sessionId);\n` +
369
- ` if (!sessionId) return;\n` +
370
- `\n` +
371
- ` if (!allowTrigger('agent_end', agentId, sessionId)) return;\n` +
372
- `\n` +
373
- ` spawnSync({\n` +
374
- ` args: ['sync', '--auto', '--from-openclaw'],\n` +
375
- ` env: buildSessionEnv({\n` +
376
- ` agentId,\n` +
377
- ` sessionId,\n` +
378
- ` sessionKey,\n` +
379
- ` sessionEntry: sessionInfo && sessionInfo.entry\n` +
380
- ` })\n` +
381
- ` });\n` +
382
- ` } catch (_) {}\n` +
383
- ` });\n` +
384
- `\n` +
385
- ` api.on('gateway_start', async () => {\n` +
386
- ` try {\n` +
387
- ` if (!allowTrigger('gateway_start', 'gateway', 'startup')) return;\n` +
388
- ` spawnSync({ args: ['sync', '--auto'] });\n` +
389
- ` } catch (_) {}\n` +
390
- ` });\n` +
391
- `\n` +
392
- ` api.on('gateway_stop', async () => {\n` +
393
- ` try {\n` +
394
- ` if (!allowTrigger('gateway_stop', 'gateway', 'stop')) return;\n` +
395
- ` spawnSync({ args: ['sync', '--auto'] });\n` +
353
+ ` const payload = buildPayload(event, ctx);\n` +
354
+ ` if (!payload) return;\n` +
355
+ ` await appendOpenclawUsageEvent({ trackerDir, payload });\n` +
396
356
  ` } catch (_) {}\n` +
397
357
  ` });\n` +
398
358
  `}\n` +
399
359
  `\n` +
400
- `function spawnSync({ args, env = {} }) {\n` +
401
- ` const hasLocalRuntime = fs.existsSync(trackerBinPath);\n` +
402
- ` const hasLocalDeps = fs.existsSync(depsMarkerPath);\n` +
403
- ` const argv = Array.isArray(args) && args.length > 0 ? args : ['sync', '--auto'];\n` +
404
- ` const cmd = hasLocalRuntime && hasLocalDeps\n` +
405
- ` ? [process.execPath, trackerBinPath, ...argv]\n` +
406
- ` : ['npx', '--yes', fallbackPkg, ...argv];\n` +
407
- ` const child = cp.spawn(cmd[0], cmd.slice(1), {\n` +
408
- ` detached: true,\n` +
409
- ` stdio: 'ignore',\n` +
410
- ` env: { ...process.env, ...env }\n` +
411
- ` });\n` +
412
- ` child.unref();\n` +
413
- `}\n` +
414
- `\n` +
415
- `function buildSessionEnv({ agentId, sessionId, sessionKey, sessionEntry }) {\n` +
416
- ` const out = {\n` +
417
- ` VIBEUSAGE_OPENCLAW_AGENT_ID: agentId,\n` +
418
- ` VIBEUSAGE_OPENCLAW_PREV_SESSION_ID: sessionId,\n` +
419
- ` VIBEUSAGE_OPENCLAW_HOME: openclawHome\n` +
420
- ` };\n` +
421
- ` const key = normalize(sessionKey);\n` +
422
- ` if (key) out.VIBEUSAGE_OPENCLAW_SESSION_KEY = key;\n` +
423
- ` const prevTotalTokens = toNonNegativeInt(sessionEntry && sessionEntry.totalTokens);\n` +
424
- ` const prevInputTokens = toNonNegativeInt(sessionEntry && sessionEntry.inputTokens);\n` +
425
- ` const prevOutputTokens = toNonNegativeInt(sessionEntry && sessionEntry.outputTokens);\n` +
426
- ` const prevModel = normalize(sessionEntry && sessionEntry.model);\n` +
427
- ` const prevUpdatedAt = toIso(sessionEntry && sessionEntry.updatedAt);\n` +
428
- ` if (prevTotalTokens != null) out.VIBEUSAGE_OPENCLAW_PREV_TOTAL_TOKENS = String(prevTotalTokens);\n` +
429
- ` if (prevInputTokens != null) out.VIBEUSAGE_OPENCLAW_PREV_INPUT_TOKENS = String(prevInputTokens);\n` +
430
- ` if (prevOutputTokens != null) out.VIBEUSAGE_OPENCLAW_PREV_OUTPUT_TOKENS = String(prevOutputTokens);\n` +
431
- ` if (prevModel) out.VIBEUSAGE_OPENCLAW_PREV_MODEL = prevModel;\n` +
432
- ` if (prevUpdatedAt) out.VIBEUSAGE_OPENCLAW_PREV_UPDATED_AT = prevUpdatedAt;\n` +
433
- ` return out;\n` +
434
- `}\n` +
360
+ `function buildPayload(event, ctx) {\n` +
361
+ ` const usage = normalizeUsage(event);\n` +
362
+ ` if (!usage) return null;\n` +
435
363
  `\n` +
436
- `function resolveSessionInfo(agentId, sessionKey) {\n` +
437
- ` const key = normalize(sessionKey);\n` +
438
- ` if (!key) return null;\n` +
439
- ` const sessionsPath = path.join(openclawHome, 'agents', agentId, 'sessions', 'sessions.json');\n` +
440
- ` try {\n` +
441
- ` const raw = fs.readFileSync(sessionsPath, 'utf8');\n` +
442
- ` const parsed = JSON.parse(raw);\n` +
443
- ` if (!parsed || typeof parsed !== 'object') return null;\n` +
444
- ` const entry = parsed[key];\n` +
445
- ` if (!entry || typeof entry !== 'object') return null;\n` +
446
- ` return {\n` +
447
- ` sessionKey: key,\n` +
448
- ` sessionId: normalize(entry.sessionId),\n` +
449
- ` entry\n` +
450
- ` };\n` +
451
- ` } catch (_) {}\n` +
452
- ` return null;\n` +
453
- `}\n` +
364
+ ` const sessionKey = normalize(ctx && ctx.sessionKey);\n` +
365
+ ` if (!sessionKey) return null;\n` +
454
366
  `\n` +
455
- `function parseAgentId(sessionKey) {\n` +
456
- ` const s = normalize(sessionKey);\n` +
457
- ` if (!s || !s.startsWith('agent:')) return null;\n` +
458
- ` const parts = s.split(':');\n` +
459
- ` return parts.length >= 2 ? normalize(parts[1]) : null;\n` +
367
+ ` return {\n` +
368
+ ` emittedAt: normalizeIso(event && (event.emittedAt || event.timestamp)) || new Date().toISOString(),\n` +
369
+ ` source: 'openclaw',\n` +
370
+ ` agentId: normalize(ctx && ctx.agentId),\n` +
371
+ ` sessionKey,\n` +
372
+ ` provider: normalize(event && event.provider) || normalize(ctx && ctx.provider),\n` +
373
+ ` model: normalize(event && event.model) || normalize(ctx && ctx.model),\n` +
374
+ ` channel: normalize(ctx && ctx.channel),\n` +
375
+ ` chatType: normalize(ctx && ctx.chatType),\n` +
376
+ ` trigger: normalize(ctx && ctx.trigger) || 'llm_output',\n` +
377
+ ` ...usage\n` +
378
+ ` };\n` +
460
379
  `}\n` +
461
380
  `\n` +
462
- `function allowTrigger(kind, scope, target) {\n` +
463
- ` const key = [kind, scope || 'na', target || 'na'].join(':');\n` +
464
- ` const now = Date.now();\n` +
465
- ` let state = {};\n` +
466
- ` try {\n` +
467
- ` state = JSON.parse(fs.readFileSync(triggerStatePath, 'utf8'));\n` +
468
- ` if (!state || typeof state !== 'object') state = {};\n` +
469
- ` } catch (_) {}\n` +
470
- ` const last = Number(state[key] || 0);\n` +
471
- ` if (Number.isFinite(last) && now - last < SESSION_TRIGGER_THROTTLE_MS) return false;\n` +
472
- ` state[key] = now;\n` +
473
- ` try {\n` +
474
- ` fs.mkdirSync(path.dirname(triggerStatePath), { recursive: true });\n` +
475
- ` fs.writeFileSync(triggerStatePath, JSON.stringify(state), 'utf8');\n` +
476
- ` } catch (_) {}\n` +
477
- ` return true;\n` +
381
+ `function normalizeUsage(event) {\n` +
382
+ ` const usage = event && typeof event.usage === 'object' ? event.usage : event || {};\n` +
383
+ ` const normalized = {\n` +
384
+ ` inputTokens: toNonNegativeInt(usage.inputTokens ?? usage.input_tokens ?? usage.input),\n` +
385
+ ` cachedInputTokens: toNonNegativeInt(usage.cachedInputTokens ?? usage.cached_input_tokens ?? usage.cacheRead ?? 0) + toNonNegativeInt(usage.cacheWrite ?? 0),\n` +
386
+ ` outputTokens: toNonNegativeInt(usage.outputTokens ?? usage.output_tokens ?? usage.output),\n` +
387
+ ` reasoningOutputTokens: toNonNegativeInt(usage.reasoningOutputTokens ?? usage.reasoning_output_tokens),\n` +
388
+ ` totalTokens: toNonNegativeInt(usage.totalTokens ?? usage.total_tokens)\n` +
389
+ ` };\n` +
390
+ ` const sum = normalized.inputTokens + normalized.cachedInputTokens + normalized.outputTokens + normalized.reasoningOutputTokens + normalized.totalTokens;\n` +
391
+ ` return sum > 0 ? normalized : null;\n` +
478
392
  `}\n` +
479
393
  `\n` +
480
394
  `function normalize(v) {\n` +
@@ -483,22 +397,18 @@ function buildSessionPluginIndex({ trackerDir, packageName = "vibeusage", opencl
483
397
  ` return s.length > 0 ? s : null;\n` +
484
398
  `}\n` +
485
399
  `\n` +
486
- `function toNonNegativeInt(v) {\n` +
487
- ` const n = Number(v);\n` +
488
- ` if (!Number.isFinite(n) || n < 0) return null;\n` +
489
- ` return Math.floor(n);\n` +
400
+ `function normalizeIso(v) {\n` +
401
+ ` const s = normalize(v);\n` +
402
+ ` if (!s) return null;\n` +
403
+ ` const ms = Date.parse(s);\n` +
404
+ ` if (!Number.isFinite(ms)) return null;\n` +
405
+ ` return new Date(ms).toISOString();\n` +
490
406
  `}\n` +
491
407
  `\n` +
492
- `function toIso(v) {\n` +
493
- ` if (typeof v === 'string') {\n` +
494
- ` const s = normalize(v);\n` +
495
- ` if (s && !Number.isNaN(Date.parse(s))) return s;\n` +
496
- ` }\n` +
497
- ` const n = Number(v);\n` +
498
- ` if (!Number.isFinite(n) || n <= 0) return null;\n` +
499
- ` const ms = n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);\n` +
500
- ` const d = new Date(ms);\n` +
501
- ` return Number.isNaN(d.getTime()) ? null : d.toISOString();\n` +
408
+ `function toNonNegativeInt(v) {\n` +
409
+ ` const n = Number(v || 0);\n` +
410
+ ` if (!Number.isFinite(n) || n < 0) return 0;\n` +
411
+ ` return Math.floor(n);\n` +
502
412
  `}\n`
503
413
  );
504
414
  }
@@ -0,0 +1,237 @@
1
+ const crypto = require("node:crypto");
2
+ const fs = require("node:fs/promises");
3
+ const path = require("node:path");
4
+
5
+ const { ensureDir, readJson, writeJson } = require("./fs");
6
+
7
+ const OPENCLAW_SOURCE = "openclaw";
8
+ const ALLOWED_EVENT_FIELDS = [
9
+ "eventId",
10
+ "emittedAt",
11
+ "source",
12
+ "agentId",
13
+ "sessionRef",
14
+ "provider",
15
+ "model",
16
+ "channel",
17
+ "chatType",
18
+ "trigger",
19
+ "inputTokens",
20
+ "cachedInputTokens",
21
+ "outputTokens",
22
+ "reasoningOutputTokens",
23
+ "totalTokens",
24
+ ];
25
+
26
+ function resolveOpenclawUsageLedgerPaths({ trackerDir } = {}) {
27
+ if (!trackerDir) throw new Error("trackerDir is required");
28
+ return {
29
+ ledgerPath: path.join(trackerDir, "openclaw-usage-ledger.jsonl"),
30
+ statePath: path.join(trackerDir, "openclaw-usage-ledger.state.json"),
31
+ saltPath: path.join(trackerDir, "openclaw-usage-ledger.salt"),
32
+ };
33
+ }
34
+
35
+ async function buildOpenclawUsageEvent({ trackerDir, payload } = {}) {
36
+ if (!payload || typeof payload !== "object") throw new Error("payload is required");
37
+
38
+ const source = normalizeString(payload.source) || OPENCLAW_SOURCE;
39
+ const emittedAt = normalizeIso(payload.emittedAt) || new Date().toISOString();
40
+ const agentId = normalizeString(payload.agentId);
41
+ const provider = normalizeString(payload.provider);
42
+ const model = normalizeString(payload.model);
43
+ const channel = normalizeString(payload.channel);
44
+ const chatType = normalizeString(payload.chatType);
45
+ const trigger = normalizeString(payload.trigger);
46
+ const sessionKey = normalizeString(payload.sessionKey);
47
+ const existingSessionRef = normalizeHex(payload.sessionRef);
48
+ const sessionRef = sessionKey
49
+ ? await hashOpenclawSessionRef({ trackerDir, sessionKey })
50
+ : existingSessionRef;
51
+
52
+ const usage = payload.usage && typeof payload.usage === "object" ? payload.usage : payload;
53
+ const event = {
54
+ eventId: normalizeString(payload.eventId),
55
+ emittedAt,
56
+ source,
57
+ agentId,
58
+ sessionRef,
59
+ provider,
60
+ model,
61
+ channel,
62
+ chatType,
63
+ trigger,
64
+ inputTokens: toNonNegativeInt(usage.inputTokens ?? usage.input_tokens),
65
+ cachedInputTokens: toNonNegativeInt(usage.cachedInputTokens ?? usage.cached_input_tokens),
66
+ outputTokens: toNonNegativeInt(usage.outputTokens ?? usage.output_tokens),
67
+ reasoningOutputTokens: toNonNegativeInt(
68
+ usage.reasoningOutputTokens ?? usage.reasoning_output_tokens,
69
+ ),
70
+ totalTokens: toNonNegativeInt(usage.totalTokens ?? usage.total_tokens),
71
+ };
72
+
73
+ if (!event.eventId) {
74
+ event.eventId = deriveOpenclawEventId(event);
75
+ }
76
+
77
+ return stripEmptyAllowedFields(event);
78
+ }
79
+
80
+ async function appendOpenclawUsageEvent({ trackerDir, event, payload } = {}) {
81
+ const nextEvent = event
82
+ ? await buildOpenclawUsageEvent({ trackerDir, payload: event })
83
+ : await buildOpenclawUsageEvent({ trackerDir, payload });
84
+ const { ledgerPath, statePath } = resolveOpenclawUsageLedgerPaths({ trackerDir });
85
+
86
+ await ensureDir(path.dirname(ledgerPath));
87
+
88
+ const state = (await readJson(statePath)) || { version: 1, seenEventIds: {}, updatedAt: null };
89
+ if (!state.seenEventIds || typeof state.seenEventIds !== "object") {
90
+ state.seenEventIds = {};
91
+ }
92
+
93
+ if (state.seenEventIds[nextEvent.eventId]) {
94
+ return { appended: false, duplicate: true, event: nextEvent };
95
+ }
96
+
97
+ await fs.appendFile(ledgerPath, `${JSON.stringify(nextEvent)}\n`, "utf8");
98
+ state.version = 1;
99
+ state.seenEventIds[nextEvent.eventId] = nextEvent.emittedAt || new Date().toISOString();
100
+ state.updatedAt = new Date().toISOString();
101
+ await writeJson(statePath, state);
102
+
103
+ return { appended: true, duplicate: false, event: nextEvent };
104
+ }
105
+
106
+ async function readOpenclawUsageLedger({ trackerDir, offset = 0 } = {}) {
107
+ const { ledgerPath } = resolveOpenclawUsageLedgerPaths({ trackerDir });
108
+ const buffer = await fs.readFile(ledgerPath).catch((err) => {
109
+ if (err && err.code === "ENOENT") return null;
110
+ throw err;
111
+ });
112
+
113
+ if (!buffer) {
114
+ return { events: [], endOffset: 0 };
115
+ }
116
+
117
+ const startOffset = Math.max(0, Number(offset || 0));
118
+ const endOffset = buffer.length;
119
+ if (startOffset >= endOffset) {
120
+ return { events: [], endOffset };
121
+ }
122
+
123
+ const raw = buffer.subarray(startOffset).toString("utf8");
124
+ const events = raw
125
+ .split(/\r?\n/)
126
+ .map((line) => line.trim())
127
+ .filter(Boolean)
128
+ .map((line) => {
129
+ try {
130
+ return JSON.parse(line);
131
+ } catch (_err) {
132
+ return null;
133
+ }
134
+ })
135
+ .filter(Boolean)
136
+ .map(stripToAllowedEventFields);
137
+
138
+ return { events, endOffset };
139
+ }
140
+
141
+ async function hashOpenclawSessionRef({ trackerDir, sessionKey } = {}) {
142
+ const normalized = normalizeString(sessionKey);
143
+ if (!normalized) return null;
144
+
145
+ const salt = await ensureOpenclawLedgerSalt({ trackerDir });
146
+ return crypto.createHmac("sha256", salt).update(normalized).digest("hex");
147
+ }
148
+
149
+ async function ensureOpenclawLedgerSalt({ trackerDir } = {}) {
150
+ const { saltPath } = resolveOpenclawUsageLedgerPaths({ trackerDir });
151
+ await ensureDir(path.dirname(saltPath));
152
+
153
+ try {
154
+ const existing = (await fs.readFile(saltPath, "utf8")).trim();
155
+ if (existing) return existing;
156
+ } catch (err) {
157
+ if (err?.code !== "ENOENT") throw err;
158
+ }
159
+
160
+ const salt = crypto.randomBytes(32).toString("hex");
161
+ await fs.writeFile(saltPath, `${salt}\n`, { encoding: "utf8", mode: 0o600 });
162
+ return salt;
163
+ }
164
+
165
+ function deriveOpenclawEventId(event) {
166
+ const stable = JSON.stringify({
167
+ emittedAt: normalizeIso(event.emittedAt),
168
+ source: normalizeString(event.source) || OPENCLAW_SOURCE,
169
+ agentId: normalizeString(event.agentId),
170
+ sessionRef: normalizeHex(event.sessionRef),
171
+ provider: normalizeString(event.provider),
172
+ model: normalizeString(event.model),
173
+ channel: normalizeString(event.channel),
174
+ chatType: normalizeString(event.chatType),
175
+ trigger: normalizeString(event.trigger),
176
+ inputTokens: toNonNegativeInt(event.inputTokens),
177
+ cachedInputTokens: toNonNegativeInt(event.cachedInputTokens),
178
+ outputTokens: toNonNegativeInt(event.outputTokens),
179
+ reasoningOutputTokens: toNonNegativeInt(event.reasoningOutputTokens),
180
+ totalTokens: toNonNegativeInt(event.totalTokens),
181
+ });
182
+ return crypto.createHash("sha256").update(stable).digest("hex");
183
+ }
184
+
185
+ function stripEmptyAllowedFields(event) {
186
+ const out = {};
187
+ for (const field of ALLOWED_EVENT_FIELDS) {
188
+ const value = event[field];
189
+ if (value == null) continue;
190
+ out[field] = value;
191
+ }
192
+ return out;
193
+ }
194
+
195
+ function stripToAllowedEventFields(event) {
196
+ const out = {};
197
+ for (const field of ALLOWED_EVENT_FIELDS) {
198
+ if (!Object.prototype.hasOwnProperty.call(event, field)) continue;
199
+ out[field] = event[field];
200
+ }
201
+ return out;
202
+ }
203
+
204
+ function normalizeString(value) {
205
+ if (typeof value !== "string") return null;
206
+ const trimmed = value.trim();
207
+ return trimmed.length > 0 ? trimmed : null;
208
+ }
209
+
210
+ function normalizeIso(value) {
211
+ const normalized = normalizeString(value);
212
+ if (!normalized) return null;
213
+ const time = Date.parse(normalized);
214
+ if (!Number.isFinite(time)) return null;
215
+ return new Date(time).toISOString();
216
+ }
217
+
218
+ function normalizeHex(value) {
219
+ const normalized = normalizeString(value);
220
+ if (!normalized) return null;
221
+ return /^[a-f0-9]{64}$/i.test(normalized) ? normalized.toLowerCase() : null;
222
+ }
223
+
224
+ function toNonNegativeInt(value) {
225
+ const numeric = Number(value || 0);
226
+ if (!Number.isFinite(numeric) || numeric < 0) return 0;
227
+ return Math.floor(numeric);
228
+ }
229
+
230
+ module.exports = {
231
+ ALLOWED_EVENT_FIELDS,
232
+ buildOpenclawUsageEvent,
233
+ appendOpenclawUsageEvent,
234
+ readOpenclawUsageLedger,
235
+ hashOpenclawSessionRef,
236
+ resolveOpenclawUsageLedgerPaths,
237
+ };
@@ -637,161 +637,6 @@ async function parseOpencodeIncremental({
637
637
  };
638
638
  }
639
639
 
640
- async function parseOpenclawIncremental({
641
- sessionFiles,
642
- cursors,
643
- queuePath,
644
- projectQueuePath,
645
- onProgress,
646
- source,
647
- }) {
648
- await ensureDir(path.dirname(queuePath));
649
- let filesProcessed = 0;
650
- let eventsAggregated = 0;
651
-
652
- const cb = typeof onProgress === "function" ? onProgress : null;
653
- const files = Array.isArray(sessionFiles) ? sessionFiles : [];
654
- const totalFiles = files.length;
655
- const hourlyState = normalizeHourlyState(cursors?.hourly);
656
- const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
657
- const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
658
- const projectTouchedBuckets = projectEnabled ? new Set() : null;
659
- const touchedBuckets = new Set();
660
- const defaultSource = normalizeSourceInput(source) || "openclaw";
661
-
662
- if (!cursors.files || typeof cursors.files !== "object") {
663
- cursors.files = {};
664
- }
665
-
666
- for (let idx = 0; idx < files.length; idx++) {
667
- const entry = files[idx];
668
- const filePath = typeof entry === "string" ? entry : entry?.path;
669
- if (!filePath) continue;
670
- const fileSource =
671
- typeof entry === "string"
672
- ? defaultSource
673
- : normalizeSourceInput(entry?.source) || defaultSource;
674
- const st = await fs.stat(filePath).catch(() => null);
675
- if (!st || !st.isFile()) continue;
676
-
677
- const key = filePath;
678
- const prev = cursors.files[key] || null;
679
- const inode = st.ino || 0;
680
- const startOffset = prev && prev.inode === inode ? prev.offset || 0 : 0;
681
-
682
- const result = await parseOpenclawSessionFile({
683
- filePath,
684
- startOffset,
685
- hourlyState,
686
- touchedBuckets,
687
- source: fileSource,
688
- projectState,
689
- projectTouchedBuckets,
690
- });
691
-
692
- cursors.files[key] = {
693
- inode,
694
- offset: result.endOffset,
695
- updatedAt: new Date().toISOString(),
696
- };
697
-
698
- filesProcessed += 1;
699
- eventsAggregated += result.eventsAggregated;
700
-
701
- if (cb) {
702
- cb({
703
- index: idx + 1,
704
- total: totalFiles,
705
- filePath,
706
- filesProcessed,
707
- eventsAggregated,
708
- bucketsQueued: touchedBuckets.size,
709
- });
710
- }
711
- }
712
-
713
- const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
714
- const projectBucketsQueued = projectEnabled
715
- ? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
716
- : 0;
717
- hourlyState.updatedAt = new Date().toISOString();
718
- cursors.hourly = hourlyState;
719
- if (projectState) {
720
- projectState.updatedAt = new Date().toISOString();
721
- cursors.projectHourly = projectState;
722
- }
723
-
724
- return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
725
- }
726
-
727
- async function parseOpenclawSessionFile({
728
- filePath,
729
- startOffset,
730
- hourlyState,
731
- touchedBuckets,
732
- source,
733
- projectState,
734
- projectTouchedBuckets,
735
- }) {
736
- const st = await fs.stat(filePath);
737
- const endOffset = st.size;
738
- if (startOffset >= endOffset) return { endOffset, eventsAggregated: 0 };
739
-
740
- const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
741
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
742
-
743
- let eventsAggregated = 0;
744
- for await (const line of rl) {
745
- if (!line) continue;
746
- // Fast-path filter: OpenClaw assistant messages include message.usage.totalTokens.
747
- if (!line.includes('"usage"') || !line.includes("totalTokens")) continue;
748
-
749
- let obj;
750
- try {
751
- obj = JSON.parse(line);
752
- } catch (_e) {
753
- continue;
754
- }
755
-
756
- if (obj?.type !== "message") continue;
757
- const msg = obj?.message;
758
- if (!msg || typeof msg !== "object") continue;
759
-
760
- const usage = msg.usage;
761
- if (!usage || typeof usage !== "object") continue;
762
-
763
- const tokenTimestamp = typeof obj?.timestamp === "string" ? obj.timestamp : null;
764
- if (!tokenTimestamp) continue;
765
-
766
- const model = normalizeModelInput(msg.model) || DEFAULT_MODEL;
767
-
768
- const delta = {
769
- input_tokens: Number(usage.input || 0),
770
- cached_input_tokens: Number((usage.cacheRead || 0) + (usage.cacheWrite || 0)),
771
- output_tokens: Number(usage.output || 0),
772
- reasoning_output_tokens: 0,
773
- total_tokens: Number(usage.totalTokens || 0),
774
- };
775
-
776
- if (isAllZeroUsage(delta)) continue;
777
-
778
- const bucketStart = toUtcHalfHourStart(tokenTimestamp);
779
- if (!bucketStart) continue;
780
-
781
- const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
782
- addTotals(bucket.totals, delta);
783
- touchedBuckets.add(bucketKey(source, model, bucketStart));
784
-
785
- // Project-level OpenClaw attribution is not supported yet (no stable cwd info).
786
- // If OpenClaw later records cwd per event, we can mirror rollout's project logic.
787
- eventsAggregated += 1;
788
- }
789
-
790
- rl.close();
791
- stream.close?.();
792
- return { endOffset, eventsAggregated };
793
- }
794
-
795
640
  async function parseRolloutFile({
796
641
  filePath,
797
642
  startOffset,
@@ -2453,5 +2298,10 @@ module.exports = {
2453
2298
  parseClaudeIncremental,
2454
2299
  parseGeminiIncremental,
2455
2300
  parseOpencodeIncremental,
2456
- parseOpenclawIncremental,
2301
+ normalizeHourlyState,
2302
+ getHourlyBucket,
2303
+ addTotals,
2304
+ bucketKey,
2305
+ enqueueTouchedBuckets,
2306
+ toUtcHalfHourStart,
2457
2307
  };