vibeusage 0.3.1 → 0.3.3
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/README.md +7 -5
- package/README.zh-CN.md +10 -8
- package/package.json +1 -1
- package/src/commands/status.js +0 -5
- package/src/commands/sync.js +67 -175
- package/src/commands/uninstall.js +0 -11
- package/src/lib/diagnostics.js +0 -9
- package/src/lib/doctor.js +1 -15
- package/src/lib/integrations/context.js +0 -6
- package/src/lib/integrations/index.js +0 -2
- package/src/lib/openclaw-session-plugin.js +48 -138
- package/src/lib/openclaw-usage-ledger.js +237 -0
- package/src/lib/rollout.js +6 -156
- package/src/lib/integrations/openclaw-legacy.js +0 -123
- package/src/lib/openclaw-hook.js +0 -420
|
@@ -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
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
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('
|
|
351
|
+
` api.on('llm_output', async (event, ctx) => {\n` +
|
|
360
352
|
` try {\n` +
|
|
361
|
-
` const
|
|
362
|
-
` if (!
|
|
363
|
-
|
|
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
|
|
401
|
-
` const
|
|
402
|
-
`
|
|
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
|
-
`
|
|
437
|
-
`
|
|
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
|
-
`
|
|
456
|
-
`
|
|
457
|
-
`
|
|
458
|
-
`
|
|
459
|
-
`
|
|
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
|
|
463
|
-
` const
|
|
464
|
-
` const
|
|
465
|
-
`
|
|
466
|
-
`
|
|
467
|
-
`
|
|
468
|
-
`
|
|
469
|
-
`
|
|
470
|
-
`
|
|
471
|
-
`
|
|
472
|
-
`
|
|
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 ?? usage.total)\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
|
|
487
|
-
` const
|
|
488
|
-
` if (!
|
|
489
|
-
`
|
|
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
|
|
493
|
-
`
|
|
494
|
-
`
|
|
495
|
-
`
|
|
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
|
+
};
|
package/src/lib/rollout.js
CHANGED
|
@@ -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
|
-
|
|
2301
|
+
normalizeHourlyState,
|
|
2302
|
+
getHourlyBucket,
|
|
2303
|
+
addTotals,
|
|
2304
|
+
bucketKey,
|
|
2305
|
+
enqueueTouchedBuckets,
|
|
2306
|
+
toUtcHalfHourStart,
|
|
2457
2307
|
};
|