idlewatch 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/.env.example +73 -0
- package/.github/workflows/ci.yml +99 -0
- package/.github/workflows/release-macos-trusted.yml +103 -0
- package/README.md +336 -0
- package/bin/idlewatch-agent.js +1053 -0
- package/docs/onboarding-external.md +58 -0
- package/docs/packaging/macos-dmg.md +199 -0
- package/docs/packaging/macos-launch-agent.md +70 -0
- package/docs/qa/archive/mac-qa-log-2026-02-17.md +5838 -0
- package/docs/qa/mac-qa-log.md +2864 -0
- package/docs/telemetry/idle-stale-policy.md +57 -0
- package/docs/telemetry/openclaw-mapping.md +80 -0
- package/package.json +76 -0
- package/scripts/build-dmg.sh +65 -0
- package/scripts/install-macos-launch-agent.sh +78 -0
- package/scripts/lib/telemetry-row-parser.mjs +100 -0
- package/scripts/package-macos.sh +228 -0
- package/scripts/uninstall-macos-launch-agent.sh +30 -0
- package/scripts/validate-all.sh +142 -0
- package/scripts/validate-bin.mjs +25 -0
- package/scripts/validate-dmg-checksum.sh +37 -0
- package/scripts/validate-dmg-install.sh +155 -0
- package/scripts/validate-dry-run-schema.mjs +257 -0
- package/scripts/validate-onboarding.mjs +63 -0
- package/scripts/validate-openclaw-cache-recovery-e2e.mjs +113 -0
- package/scripts/validate-openclaw-release-gates.mjs +51 -0
- package/scripts/validate-openclaw-stats-ingestion.mjs +372 -0
- package/scripts/validate-openclaw-usage-health.mjs +95 -0
- package/scripts/validate-packaged-artifact.mjs +233 -0
- package/scripts/validate-packaged-bundled-runtime.sh +191 -0
- package/scripts/validate-packaged-metadata.sh +43 -0
- package/scripts/validate-packaged-openclaw-cache-recovery-e2e.mjs +153 -0
- package/scripts/validate-packaged-openclaw-release-gates.mjs +72 -0
- package/scripts/validate-packaged-openclaw-stats-ingestion.mjs +402 -0
- package/scripts/validate-packaged-sourcemaps.mjs +82 -0
- package/scripts/validate-packaged-usage-alert-rate-e2e.mjs +98 -0
- package/scripts/validate-packaged-usage-probe-noise-e2e.mjs +87 -0
- package/scripts/validate-packaged-usage-recovery-e2e.mjs +90 -0
- package/scripts/validate-trusted-prereqs.sh +44 -0
- package/scripts/validate-usage-alert-rate-e2e.mjs +91 -0
- package/scripts/validate-usage-freshness-e2e.mjs +81 -0
- package/skill/SKILL.md +43 -0
- package/src/config.js +100 -0
- package/src/enrollment.js +176 -0
- package/src/gpu.js +115 -0
- package/src/memory.js +67 -0
- package/src/openclaw-cache.js +51 -0
- package/src/openclaw-usage.js +1020 -0
- package/src/telemetry-mapping.js +54 -0
- package/src/usage-alert.js +41 -0
- package/src/usage-freshness.js +31 -0
- package/test/config.test.mjs +112 -0
- package/test/fixtures/gpu-agx.txt +2 -0
- package/test/fixtures/gpu-iogpu.txt +2 -0
- package/test/fixtures/gpu-top-grep.txt +2 -0
- package/test/fixtures/openclaw-fleet-sample-v1.json +68 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-iso-ts.txt +2 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-newest.txt +2 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-string-ts.txt +2 -0
- package/test/fixtures/openclaw-mixed-status-then-generic-output.txt +2 -0
- package/test/fixtures/openclaw-stats-current-wrapper.json +12 -0
- package/test/fixtures/openclaw-stats-current-wrapper2.json +15 -0
- package/test/fixtures/openclaw-stats-data-wrapper.json +21 -0
- package/test/fixtures/openclaw-stats-nested-session-wrapper.json +23 -0
- package/test/fixtures/openclaw-stats-payload-wrapper.json +1 -0
- package/test/fixtures/openclaw-stats-status-current-wrapper.json +19 -0
- package/test/fixtures/openclaw-stats.json +17 -0
- package/test/fixtures/openclaw-status-ansi-complex-noise.txt +3 -0
- package/test/fixtures/openclaw-status-ansi-noise.txt +2 -0
- package/test/fixtures/openclaw-status-control-noise.txt +1 -0
- package/test/fixtures/openclaw-status-data-wrapper.json +20 -0
- package/test/fixtures/openclaw-status-dcs-noise.txt +1 -0
- package/test/fixtures/openclaw-status-epoch-seconds.json +15 -0
- package/test/fixtures/openclaw-status-mixed-noise.txt +1 -0
- package/test/fixtures/openclaw-status-multi-json.txt +3 -0
- package/test/fixtures/openclaw-status-nested-recent.json +19 -0
- package/test/fixtures/openclaw-status-noisy-default-then-usage.txt +2 -0
- package/test/fixtures/openclaw-status-noisy.txt +3 -0
- package/test/fixtures/openclaw-status-osc-noise.txt +1 -0
- package/test/fixtures/openclaw-status-result-session.json +15 -0
- package/test/fixtures/openclaw-status-session-map-with-defaults.json +23 -0
- package/test/fixtures/openclaw-status-session-map.json +28 -0
- package/test/fixtures/openclaw-status-session-model-name.json +18 -0
- package/test/fixtures/openclaw-status-snake-session-wrapper.json +13 -0
- package/test/fixtures/openclaw-status-stats-current-sessions-snake-tokens.json +25 -0
- package/test/fixtures/openclaw-status-stats-current-sessions.json +28 -0
- package/test/fixtures/openclaw-status-stats-current-usage-time-camelcase.json +19 -0
- package/test/fixtures/openclaw-status-stats-session-default-model.json +27 -0
- package/test/fixtures/openclaw-status-status-wrapper.json +13 -0
- package/test/fixtures/openclaw-status-strings.json +38 -0
- package/test/fixtures/openclaw-status-ts-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-updated-at-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-usage-timestamp-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-usage-ts-alias.json +14 -0
- package/test/fixtures/openclaw-status-wrap-session-object.json +24 -0
- package/test/fixtures/openclaw-status.json +41 -0
- package/test/fixtures/openclaw-usage-model-name-generic.json +9 -0
- package/test/gpu.test.mjs +58 -0
- package/test/memory.test.mjs +35 -0
- package/test/openclaw-cache.test.mjs +48 -0
- package/test/openclaw-env.test.mjs +365 -0
- package/test/openclaw-usage.test.mjs +555 -0
- package/test/telemetry-mapping.test.mjs +69 -0
- package/test/telemetry-row-parser.test.mjs +44 -0
- package/test/usage-alert.test.mjs +73 -0
- package/test/usage-freshness.test.mjs +63 -0
- package/test/validate-dry-run-schema.test.mjs +146 -0
- package/tui/Cargo.lock +801 -0
- package/tui/Cargo.toml +11 -0
- package/tui/src/main.rs +368 -0
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
function pickNumber(...vals) {
|
|
2
|
+
for (const val of vals) {
|
|
3
|
+
if (typeof val === 'number' && Number.isFinite(val)) return val
|
|
4
|
+
if (typeof val === 'string') {
|
|
5
|
+
const normalized = val.trim()
|
|
6
|
+
if (!normalized) continue
|
|
7
|
+
const parsed = Number(normalized)
|
|
8
|
+
if (Number.isFinite(parsed)) return parsed
|
|
9
|
+
|
|
10
|
+
const asDate = Date.parse(normalized)
|
|
11
|
+
if (!Number.isNaN(asDate)) return asDate
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pickTimestamp(...vals) {
|
|
18
|
+
for (const val of vals) {
|
|
19
|
+
if (val === null || typeof val === 'undefined') continue
|
|
20
|
+
|
|
21
|
+
if (typeof val === 'number' && Number.isFinite(val)) {
|
|
22
|
+
const numeric = val
|
|
23
|
+
if (Number.isInteger(numeric) && numeric > 0 && numeric < 1_000_000_000_000) return numeric * 1000
|
|
24
|
+
return numeric
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof val === 'string') {
|
|
28
|
+
const normalized = val.trim()
|
|
29
|
+
if (!normalized) continue
|
|
30
|
+
|
|
31
|
+
const parsed = Number(normalized)
|
|
32
|
+
if (Number.isFinite(parsed)) {
|
|
33
|
+
if (Number.isInteger(parsed) && parsed > 0 && parsed < 1_000_000_000_000) return parsed * 1000
|
|
34
|
+
return parsed
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const asDate = Date.parse(normalized)
|
|
38
|
+
if (!Number.isNaN(asDate)) return asDate
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pickString(...vals) {
|
|
45
|
+
for (const val of vals) {
|
|
46
|
+
if (typeof val === 'string' && val.trim()) return val
|
|
47
|
+
if (val instanceof String && val.toString().trim()) return val.toString().trim()
|
|
48
|
+
}
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isFreshTokenMarker(value) {
|
|
53
|
+
if (value === false || value === 0) return false
|
|
54
|
+
if (value === null || typeof value === 'undefined') return true
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
const normalized = value.trim().toLowerCase()
|
|
57
|
+
if (!normalized) return true
|
|
58
|
+
if (['false', '0', 'no', 'off', 'stale'].includes(normalized)) return false
|
|
59
|
+
if (['true', '1', 'yes', 'on', 'fresh'].includes(normalized)) return true
|
|
60
|
+
}
|
|
61
|
+
return Boolean(value)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pickNestedTotalsTokens(candidate) {
|
|
65
|
+
if (!candidate || typeof candidate !== 'object') return null
|
|
66
|
+
|
|
67
|
+
const totalsRoot = candidate.totals || candidate.summary || candidate.usageTotals || candidate.usage?.totals || candidate.usage?.summary
|
|
68
|
+
const usageTotals = candidate.usage || {}
|
|
69
|
+
|
|
70
|
+
return pickNumber(
|
|
71
|
+
totalsRoot?.totalTokens,
|
|
72
|
+
totalsRoot?.total_tokens,
|
|
73
|
+
totalsRoot?.total,
|
|
74
|
+
totalsRoot?.tokens?.total,
|
|
75
|
+
totalsRoot?.tokens?.sum,
|
|
76
|
+
totalsRoot?.tokenCount,
|
|
77
|
+
totalsRoot?.token_count,
|
|
78
|
+
totalsRoot?.count,
|
|
79
|
+
totalsRoot?.cumulativeTokens,
|
|
80
|
+
totalsRoot?.usageTokens,
|
|
81
|
+
totalsRoot?.cumulative,
|
|
82
|
+
candidate.totalTokens,
|
|
83
|
+
candidate.total_tokens,
|
|
84
|
+
candidate.total_tokens_count,
|
|
85
|
+
usageTotals.totalTokens,
|
|
86
|
+
usageTotals.total_tokens,
|
|
87
|
+
usageTotals.tokenCount,
|
|
88
|
+
usageTotals.total,
|
|
89
|
+
usageTotals.tokens?.total,
|
|
90
|
+
usageTotals.tokens?.sum,
|
|
91
|
+
usageTotals.inputTokens && usageTotals.outputTokens ? usageTotals.inputTokens + usageTotals.outputTokens : null,
|
|
92
|
+
candidate.inputTokens && candidate.outputTokens ? candidate.inputTokens + candidate.outputTokens : null,
|
|
93
|
+
candidate.tokenUsage?.total,
|
|
94
|
+
candidate.token_usage?.total
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function deriveTokensPerMinute(session) {
|
|
99
|
+
const totalTokens = pickNumber(session?.totalTokens, session?.total_tokens, pickNestedTotalsTokens(session))
|
|
100
|
+
const ageMs = pickNumber(session?.ageMs, session?.age)
|
|
101
|
+
if (totalTokens === null || ageMs === null || ageMs <= 0) return null
|
|
102
|
+
|
|
103
|
+
const minutes = ageMs / 60000
|
|
104
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return null
|
|
105
|
+
return Number((totalTokens / minutes).toFixed(2))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function pickNewestSession(sessions = []) {
|
|
109
|
+
return sessions.reduce((best, candidate) => {
|
|
110
|
+
if (!best) return candidate
|
|
111
|
+
|
|
112
|
+
const pickSessionTs = (item) => {
|
|
113
|
+
const age = pickNumber(item?.age, item?.ageMs)
|
|
114
|
+
const absolute = pickTimestamp(item?.updatedAt, item?.updated_at, item?.updatedAtMs, item?.createdAt, item?.created_at, item?.ts, item?.time)
|
|
115
|
+
return absolute ?? (Number.isFinite(age) && age >= 0 ? Date.now() - age : null)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const bestTs = pickSessionTs(best)
|
|
119
|
+
const candidateTs = pickSessionTs(candidate)
|
|
120
|
+
|
|
121
|
+
if (candidateTs === null) return best
|
|
122
|
+
if (bestTs === null || candidateTs > bestTs) return candidate
|
|
123
|
+
return best
|
|
124
|
+
}, null)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function pickBestRecentSession(recent = []) {
|
|
128
|
+
if (!Array.isArray(recent) || recent.length === 0) return null
|
|
129
|
+
|
|
130
|
+
const withFreshTokens = recent.filter((session) => {
|
|
131
|
+
const totalTokens = pickNumber(session?.totalTokens, session?.total_tokens, pickNestedTotalsTokens(session))
|
|
132
|
+
return totalTokens !== null && isFreshTokenMarker(session?.totalTokensFresh)
|
|
133
|
+
})
|
|
134
|
+
const freshestWithTokens = pickNewestSession(withFreshTokens)
|
|
135
|
+
if (freshestWithTokens) return freshestWithTokens
|
|
136
|
+
|
|
137
|
+
const anyWithTokens = recent.filter((session) => pickNumber(session?.totalTokens, session?.total_tokens, pickNestedTotalsTokens(session)) !== null)
|
|
138
|
+
const newestWithTokens = pickNewestSession(anyWithTokens)
|
|
139
|
+
if (newestWithTokens) return newestWithTokens
|
|
140
|
+
|
|
141
|
+
return pickNewestSession(recent) || recent[0]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractOpenClawNoise(raw) {
|
|
145
|
+
const esc = String.fromCharCode(0x1b)
|
|
146
|
+
return String(raw)
|
|
147
|
+
.replace(/\x1b\][^\x07\x1b]*\x07/g, '')
|
|
148
|
+
.replace(/\x1b\][^\x07\x1b]*\x1b\\/g, '')
|
|
149
|
+
.replace(/\x9b\][^\x07\x1b]*\x07/g, '')
|
|
150
|
+
.replace(/\x9b[^\x07\x1b]*\x1b\\/g, '')
|
|
151
|
+
.replace(/\x1b[PXZ^_].*?(?:\x1b\\|\x9c)/gs, '')
|
|
152
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
153
|
+
.replace(new RegExp(`${esc}\[[0-9;?]*[ -/]*[@-~]`, 'g'), '')
|
|
154
|
+
.replace(new RegExp(`${esc}[^m]*m`, 'g'), '')
|
|
155
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
156
|
+
.replace(/\r/g, '')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
function extractJsonCandidates(raw) {
|
|
161
|
+
const text = extractOpenClawNoise(raw)
|
|
162
|
+
const candidates = []
|
|
163
|
+
|
|
164
|
+
for (let start = 0; start < text.length; start += 1) {
|
|
165
|
+
const open = text[start]
|
|
166
|
+
if (open !== '{' && open !== '[') continue
|
|
167
|
+
|
|
168
|
+
const close = open === '{' ? '}' : ']'
|
|
169
|
+
let depth = 0
|
|
170
|
+
let inString = false
|
|
171
|
+
let escaped = false
|
|
172
|
+
let end = -1
|
|
173
|
+
|
|
174
|
+
for (let i = start; i < text.length; i++) {
|
|
175
|
+
const ch = text[i]
|
|
176
|
+
|
|
177
|
+
if (inString) {
|
|
178
|
+
if (escaped) {
|
|
179
|
+
escaped = false
|
|
180
|
+
} else if (ch === '\\') {
|
|
181
|
+
escaped = true
|
|
182
|
+
} else if (ch === '"') {
|
|
183
|
+
inString = false
|
|
184
|
+
}
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (ch === '"') {
|
|
189
|
+
inString = true
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (ch === open) {
|
|
194
|
+
depth += 1
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (ch === close) {
|
|
199
|
+
depth -= 1
|
|
200
|
+
if (depth === 0) {
|
|
201
|
+
end = i
|
|
202
|
+
break
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (depth < 0) {
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (end > start) {
|
|
212
|
+
candidates.push(text.slice(start, end + 1))
|
|
213
|
+
start = end
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return candidates
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const OPENCLAW_ALIAS_KEY_MAP = {
|
|
221
|
+
current_session: 'currentSession',
|
|
222
|
+
active_session: 'activeSession',
|
|
223
|
+
recent_sessions: 'recentSessions',
|
|
224
|
+
active_sessions: 'activeSessions',
|
|
225
|
+
default_model: 'defaultModel',
|
|
226
|
+
model_name: 'modelName',
|
|
227
|
+
session_id: 'sessionId',
|
|
228
|
+
agent_id: 'agentId',
|
|
229
|
+
usage_ts: 'usageTs',
|
|
230
|
+
usage_ts_ms: 'usageTsMs',
|
|
231
|
+
usage_timestamp: 'usageTimestamp',
|
|
232
|
+
usage_timestamp_ms: 'usageTsMs',
|
|
233
|
+
usageTimestamp: 'usageTimestamp',
|
|
234
|
+
usageTimestampMs: 'usageTsMs',
|
|
235
|
+
usageTime: 'usageTimestamp',
|
|
236
|
+
usageTimeMs: 'usageTsMs',
|
|
237
|
+
usage_time: 'usageTimestamp',
|
|
238
|
+
updated_at_ms: 'updatedAtMs',
|
|
239
|
+
ts_ms: 'tsMs'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizeOpenClawAliases(value) {
|
|
243
|
+
if (Array.isArray(value)) return value.map((item) => normalizeOpenClawAliases(item))
|
|
244
|
+
if (!value || typeof value !== 'object') return value
|
|
245
|
+
|
|
246
|
+
const normalized = {}
|
|
247
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
248
|
+
const child = normalizeOpenClawAliases(rawValue)
|
|
249
|
+
const alias = OPENCLAW_ALIAS_KEY_MAP[key]
|
|
250
|
+
|
|
251
|
+
if (alias && typeof normalized[alias] === 'undefined') {
|
|
252
|
+
normalized[alias] = child
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (typeof normalized[key] === 'undefined') {
|
|
257
|
+
normalized[key] = child
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return normalized
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function hasAnySessionSignal(value) {
|
|
265
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
266
|
+
|
|
267
|
+
const directSignals = [
|
|
268
|
+
value.sessionId,
|
|
269
|
+
value.session_id,
|
|
270
|
+
value.id,
|
|
271
|
+
value.agentId,
|
|
272
|
+
value.agent_id,
|
|
273
|
+
value.model,
|
|
274
|
+
value.modelName,
|
|
275
|
+
value.model_name,
|
|
276
|
+
value.totalTokens,
|
|
277
|
+
value.total_tokens,
|
|
278
|
+
value.inputTokens,
|
|
279
|
+
value.outputTokens,
|
|
280
|
+
value.age,
|
|
281
|
+
value.ageMs,
|
|
282
|
+
value.updatedAt,
|
|
283
|
+
value.updated_at,
|
|
284
|
+
value.updatedAtMs,
|
|
285
|
+
value.updated_at_ms,
|
|
286
|
+
value.ts,
|
|
287
|
+
value.time,
|
|
288
|
+
value.timestamp,
|
|
289
|
+
value.timestampMs,
|
|
290
|
+
value.tsMs,
|
|
291
|
+
value.usageTs,
|
|
292
|
+
value.usageTsMs,
|
|
293
|
+
value.usage_timestamp,
|
|
294
|
+
value.usage_timestamp_ms,
|
|
295
|
+
value.usageTimestamp,
|
|
296
|
+
value.usageTimestampMs,
|
|
297
|
+
value.usage_time
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
if (directSignals.some((field) => Number.isFinite(Number(field)) || (typeof field === 'string' && field.trim().length > 0) || field === true || field === false)) {
|
|
301
|
+
return true
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
hasAnySessionSignal(value?.usage) ||
|
|
306
|
+
hasAnySessionSignal(value?.usageTotals) ||
|
|
307
|
+
hasAnySessionSignal(value?.result) ||
|
|
308
|
+
hasAnySessionSignal(value?.session)
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function extractUsageEnvelope(value, depth = 0) {
|
|
313
|
+
if (!value || typeof value !== 'object') return null
|
|
314
|
+
if (Array.isArray(value)) return value.length > 0 ? value : null
|
|
315
|
+
if (depth > 6) return null
|
|
316
|
+
|
|
317
|
+
const nextKeys = [
|
|
318
|
+
'current',
|
|
319
|
+
'currentSession',
|
|
320
|
+
'activeSession',
|
|
321
|
+
'session',
|
|
322
|
+
'active',
|
|
323
|
+
'recent',
|
|
324
|
+
'recentSessions',
|
|
325
|
+
'activeSessions',
|
|
326
|
+
'sessions',
|
|
327
|
+
'stats',
|
|
328
|
+
'sessionUsage',
|
|
329
|
+
'usage',
|
|
330
|
+
'data',
|
|
331
|
+
'result',
|
|
332
|
+
'status',
|
|
333
|
+
'payload',
|
|
334
|
+
'defaultModel',
|
|
335
|
+
'default_model',
|
|
336
|
+
'session_id',
|
|
337
|
+
'agent_id'
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
for (const key of nextKeys) {
|
|
341
|
+
const next = value[key]
|
|
342
|
+
const normalized = extractUsageEnvelope(next, depth + 1)
|
|
343
|
+
if (normalized) return normalized
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (hasAnySessionSignal(value)) return value
|
|
347
|
+
|
|
348
|
+
return null
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function coerceSessionCandidates(value, options = {}) {
|
|
352
|
+
const skipKeys = new Set(options.skipKeys || [])
|
|
353
|
+
|
|
354
|
+
if (Array.isArray(value)) {
|
|
355
|
+
return value.length > 0 ? value : null
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (value && typeof value === 'object') {
|
|
359
|
+
if (hasAnySessionSignal(value)) return [value]
|
|
360
|
+
|
|
361
|
+
const fromObject = Object.entries(value)
|
|
362
|
+
.filter(([key, entry]) => !skipKeys.has(key))
|
|
363
|
+
.map((entry) => entry[1])
|
|
364
|
+
.filter((entry) => {
|
|
365
|
+
if (!entry || typeof entry !== 'object') return false
|
|
366
|
+
if (Array.isArray(entry)) return entry.length > 0
|
|
367
|
+
if (!hasAnySessionSignal(entry)) return false
|
|
368
|
+
return true
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
if (fromObject.length > 0) {
|
|
372
|
+
// Flatten nested arrays (e.g. when a sessions array is a value in an object map)
|
|
373
|
+
const flattened = fromObject.flatMap((entry) => (Array.isArray(entry) ? entry : [entry]))
|
|
374
|
+
return flattened.length > 0 ? flattened : fromObject
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return null
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Resolve a dot-separated path on an object, returning undefined if any
|
|
383
|
+
* segment is nullish. E.g. deepGet(obj, 'a.b.c') === obj?.a?.b?.c
|
|
384
|
+
*/
|
|
385
|
+
function deepGet(obj, path) {
|
|
386
|
+
let cur = obj
|
|
387
|
+
const segments = path.split('.')
|
|
388
|
+
for (let i = 0; i < segments.length; i++) {
|
|
389
|
+
if (cur == null || typeof cur !== 'object') return undefined
|
|
390
|
+
cur = cur[segments[i]]
|
|
391
|
+
}
|
|
392
|
+
return cur
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Check whether any combination of wrapper-prefix + leaf-key is truthy on
|
|
397
|
+
* `value`. `prefixes` are dot-separated paths (empty string = root).
|
|
398
|
+
*/
|
|
399
|
+
function hasTruthyAtAnyPath(value, prefixes, leafKeys) {
|
|
400
|
+
for (const prefix of prefixes) {
|
|
401
|
+
const base = prefix ? deepGet(value, prefix) : value
|
|
402
|
+
if (base == null || typeof base !== 'object') continue
|
|
403
|
+
for (const leaf of leafKeys) {
|
|
404
|
+
if (base[leaf]) return true
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return false
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Wrapper prefixes used for envelope detection (up to 4 levels deep).
|
|
411
|
+
const SESSION_ENVELOPE_PREFIXES = [
|
|
412
|
+
'', 'result', 'result.data', 'data', 'data.result',
|
|
413
|
+
'status', 'status.result', 'status.result.data', 'status.data',
|
|
414
|
+
'payload', 'payload.result', 'payload.result.data', 'payload.data',
|
|
415
|
+
'payload.data.result', 'payload.data.result.data',
|
|
416
|
+
'payload.status', 'payload.status.result', 'payload.status.result.data'
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
const SESSION_LEAF_KEYS = ['sessions', 'session', 'activeSession', 'currentSession', 'current', 'active', 'recentSessions', 'recent', 'activeSessions']
|
|
420
|
+
|
|
421
|
+
function isExplicitSessionEnvelope(value) {
|
|
422
|
+
if (!value || typeof value !== 'object') return false
|
|
423
|
+
return hasTruthyAtAnyPath(value, SESSION_ENVELOPE_PREFIXES, SESSION_LEAF_KEYS)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const STATS_PREFIXES = [
|
|
427
|
+
'', 'result', 'data', 'data.result', 'status', 'status.result',
|
|
428
|
+
'payload', 'payload.result', 'payload.data', 'payload.data.result',
|
|
429
|
+
'payload.status', 'payload.status.result'
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
const STATS_LEAF_KEYS = ['stats', 'sessionUsage', 'usage', 'current', 'session']
|
|
433
|
+
|
|
434
|
+
function looksLikeStatsOrCurrentPayload(parsed) {
|
|
435
|
+
if (!parsed || typeof parsed !== 'object') return false
|
|
436
|
+
return hasTruthyAtAnyPath(parsed, STATS_PREFIXES, STATS_LEAF_KEYS)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Wrapper prefixes for session candidate collection (ordered by priority).
|
|
440
|
+
const COLLECT_PREFIXES = [
|
|
441
|
+
'sessions', '', 'result.sessions', 'result', 'result.data.sessions', 'result.data',
|
|
442
|
+
'data.result.sessions', 'data.result', 'data.result.data.sessions', 'data.result.data',
|
|
443
|
+
'data', 'status.sessions', 'status', 'status.result.sessions', 'status.result',
|
|
444
|
+
'status.result.data', 'status.data.sessions', 'status.data'
|
|
445
|
+
]
|
|
446
|
+
|
|
447
|
+
// Session leaf keys to probe under each prefix.
|
|
448
|
+
const COLLECT_LEAVES = ['recent', 'recentSessions', 'activeSessions', 'active', 'session', 'activeSession', 'current', 'currentSession']
|
|
449
|
+
|
|
450
|
+
// Additional fallback paths checked as raw roots (no leaf expansion).
|
|
451
|
+
const COLLECT_FALLBACK_ROOTS = [
|
|
452
|
+
'result.data', 'result', 'data.result.data', 'data.result', 'data',
|
|
453
|
+
'status.result.data', 'status.result', 'sessions', 'data'
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
function collectStatusSessionCandidates(parsed) {
|
|
457
|
+
const coerceOpts = { skipKeys: ['defaults', 'metadata', 'config'] }
|
|
458
|
+
|
|
459
|
+
// First pass: prefix + leaf combinations (specific paths only).
|
|
460
|
+
for (const prefix of COLLECT_PREFIXES) {
|
|
461
|
+
const base = prefix ? deepGet(parsed, prefix) : parsed
|
|
462
|
+
if (base == null || typeof base !== 'object') continue
|
|
463
|
+
for (const leaf of COLLECT_LEAVES) {
|
|
464
|
+
const candidate = base[leaf]
|
|
465
|
+
if (candidate == null) continue
|
|
466
|
+
const normalized = coerceSessionCandidates(candidate, coerceOpts)
|
|
467
|
+
if (normalized && normalized.length > 0) return normalized
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Second pass: try wrapper roots themselves as session containers (fallback).
|
|
472
|
+
const fallbackRoots = [
|
|
473
|
+
'result.data', 'result', 'data.result.data', 'data.result',
|
|
474
|
+
'data', 'status.result.data', 'status.result', 'status.data',
|
|
475
|
+
'sessions', 'data'
|
|
476
|
+
]
|
|
477
|
+
for (const path of fallbackRoots) {
|
|
478
|
+
const candidate = deepGet(parsed, path)
|
|
479
|
+
if (candidate == null) continue
|
|
480
|
+
const normalized = coerceSessionCandidates(candidate, coerceOpts)
|
|
481
|
+
if (normalized && normalized.length > 0) return normalized
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return null
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
function parseFromStatusJson(parsed) {
|
|
489
|
+
if (!isExplicitSessionEnvelope(parsed) && looksLikeStatsOrCurrentPayload(parsed)) {
|
|
490
|
+
return null
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const sessionsRoot = collectStatusSessionCandidates(parsed)
|
|
494
|
+
const defaults = parsed?.sessions?.defaults || parsed?.defaults || parsed?.result?.defaults || parsed?.data?.defaults || parsed?.status?.defaults ||
|
|
495
|
+
(parsed?.result?.defaultModel || parsed?.result?.default_model
|
|
496
|
+
? { model: parsed.result.defaultModel || parsed.result.default_model }
|
|
497
|
+
: parsed?.result?.data?.defaultModel || parsed?.result?.data?.default_model
|
|
498
|
+
? { model: parsed.result.data.defaultModel || parsed.result.data.default_model }
|
|
499
|
+
: parsed?.status?.defaultModel || parsed?.status?.default_model
|
|
500
|
+
? { model: parsed.status.defaultModel || parsed.status.default_model }
|
|
501
|
+
: parsed?.status?.data?.defaultModel || parsed?.status?.data?.default_model
|
|
502
|
+
? { model: parsed.status.data.defaultModel || parsed.status.data.default_model }
|
|
503
|
+
: parsed?.config?.defaultModel || parsed?.config?.default_model
|
|
504
|
+
? { model: parsed.config.defaultModel || parsed.config.default_model }
|
|
505
|
+
: {})
|
|
506
|
+
const session = pickBestRecentSession(sessionsRoot)
|
|
507
|
+
|
|
508
|
+
if (!session) {
|
|
509
|
+
if (parsed?.stats || parsed?.usage || parsed?.sessionUsage || parsed?.data?.stats || parsed?.data?.usage || parsed?.data?.sessionUsage) {
|
|
510
|
+
return null
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const defaultsModel = pickString(
|
|
514
|
+
defaults.model,
|
|
515
|
+
defaults.modelName,
|
|
516
|
+
defaults.defaultModel,
|
|
517
|
+
defaults?.default_model,
|
|
518
|
+
defaults?.model_name,
|
|
519
|
+
parsed?.defaultModel,
|
|
520
|
+
parsed?.default_model,
|
|
521
|
+
parsed?.modelName,
|
|
522
|
+
parsed?.model_name,
|
|
523
|
+
parsed?.status?.defaultModel,
|
|
524
|
+
parsed?.status?.default_model,
|
|
525
|
+
parsed?.status?.modelName,
|
|
526
|
+
parsed?.status?.model_name,
|
|
527
|
+
parsed?.status?.data?.defaultModel,
|
|
528
|
+
parsed?.status?.data?.default_model,
|
|
529
|
+
parsed?.result?.defaultModel,
|
|
530
|
+
parsed?.result?.default_model,
|
|
531
|
+
parsed?.result?.modelName,
|
|
532
|
+
parsed?.result?.model_name,
|
|
533
|
+
parsed?.data?.defaultModel,
|
|
534
|
+
parsed?.data?.default_model,
|
|
535
|
+
parsed?.data?.modelName,
|
|
536
|
+
parsed?.data?.model_name
|
|
537
|
+
)
|
|
538
|
+
if (!defaultsModel) return null
|
|
539
|
+
return {
|
|
540
|
+
model: defaultsModel,
|
|
541
|
+
totalTokens: null,
|
|
542
|
+
tokensPerMin: null,
|
|
543
|
+
sessionId: null,
|
|
544
|
+
agentId: null,
|
|
545
|
+
usageTimestampMs: pickTimestamp(
|
|
546
|
+
parsed?.ts,
|
|
547
|
+
parsed?.time,
|
|
548
|
+
parsed?.timestamp,
|
|
549
|
+
parsed?.tsMs,
|
|
550
|
+
parsed?.timestampMs,
|
|
551
|
+
parsed?.usageTs,
|
|
552
|
+
parsed?.usageTsMs,
|
|
553
|
+
parsed?.usageTimestamp,
|
|
554
|
+
parsed?.usageTimestampMs,
|
|
555
|
+
parsed?.usage_timestamp,
|
|
556
|
+
parsed?.usage_timestamp_ms,
|
|
557
|
+
parsed?.usage_time,
|
|
558
|
+
parsed?.updatedAt,
|
|
559
|
+
parsed?.updated_at,
|
|
560
|
+
parsed?.updatedAtMs,
|
|
561
|
+
parsed?.updated_at_ms
|
|
562
|
+
),
|
|
563
|
+
integrationStatus: 'partial'
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const model = pickString(
|
|
568
|
+
session.model,
|
|
569
|
+
session.modelName,
|
|
570
|
+
session.model_name,
|
|
571
|
+
session?.usage?.model,
|
|
572
|
+
session?.usage?.modelName,
|
|
573
|
+
session?.usage?.model_name,
|
|
574
|
+
session?.defaultModel,
|
|
575
|
+
session?.default_model,
|
|
576
|
+
defaults.model,
|
|
577
|
+
defaults.modelName,
|
|
578
|
+
defaults?.model_name,
|
|
579
|
+
parsed?.defaultModel,
|
|
580
|
+
parsed?.default_model,
|
|
581
|
+
parsed?.modelName,
|
|
582
|
+
parsed?.model_name,
|
|
583
|
+
parsed?.status?.model,
|
|
584
|
+
parsed?.status?.modelName,
|
|
585
|
+
parsed?.status?.model_name,
|
|
586
|
+
parsed?.status?.defaultModel,
|
|
587
|
+
parsed?.status?.default_model,
|
|
588
|
+
parsed?.result?.defaultModel,
|
|
589
|
+
parsed?.result?.default_model,
|
|
590
|
+
parsed?.result?.modelName,
|
|
591
|
+
parsed?.result?.model_name,
|
|
592
|
+
parsed?.data?.defaultModel,
|
|
593
|
+
parsed?.data?.default_model,
|
|
594
|
+
parsed?.data?.modelName,
|
|
595
|
+
parsed?.data?.model_name
|
|
596
|
+
)
|
|
597
|
+
const totalTokens = pickNumber(
|
|
598
|
+
session.totalTokens,
|
|
599
|
+
session.total_tokens,
|
|
600
|
+
pickNestedTotalsTokens(session),
|
|
601
|
+
session?.usage?.totalTokens,
|
|
602
|
+
session?.usage?.total_tokens,
|
|
603
|
+
session?.usage?.tokenCount,
|
|
604
|
+
session?.usage?.token_count
|
|
605
|
+
)
|
|
606
|
+
const tokensPerMin = pickNumber(
|
|
607
|
+
session.tokensPerMinute,
|
|
608
|
+
session.tokens_per_minute,
|
|
609
|
+
session.tpm,
|
|
610
|
+
session.rate,
|
|
611
|
+
session?.usage?.tokensPerMinute,
|
|
612
|
+
session?.usage?.tokens_per_minute,
|
|
613
|
+
session?.usage?.tpm,
|
|
614
|
+
session?.usage?.tokenRate,
|
|
615
|
+
deriveTokensPerMinute(session),
|
|
616
|
+
pickNumber(session?.derived?.tokensPerMinute, session?.derived?.tpm)
|
|
617
|
+
)
|
|
618
|
+
const sessionAgeMs = pickNumber(session.age, session.ageMs)
|
|
619
|
+
const usageTimestampMs =
|
|
620
|
+
pickTimestamp(
|
|
621
|
+
session.updatedAt,
|
|
622
|
+
session.updated_at,
|
|
623
|
+
session.updatedAtMs,
|
|
624
|
+
session.updated_at_ms,
|
|
625
|
+
session.timestamp,
|
|
626
|
+
session.time,
|
|
627
|
+
session.usageTs,
|
|
628
|
+
session.usageTimestamp,
|
|
629
|
+
session.usage_timestamp,
|
|
630
|
+
session.usage_timestamp_ms,
|
|
631
|
+
session?.usage_time,
|
|
632
|
+
session.tsMs,
|
|
633
|
+
session?.usage?.updatedAt,
|
|
634
|
+
session?.usage?.updated_at,
|
|
635
|
+
session?.usage?.updatedAtMs,
|
|
636
|
+
session?.usage?.timestamp,
|
|
637
|
+
session?.usage?.ts,
|
|
638
|
+
session?.usage?.tsMs,
|
|
639
|
+
session?.usage?.time,
|
|
640
|
+
session?.usage?.usageTs,
|
|
641
|
+
session?.usage?.usageTimestamp,
|
|
642
|
+
session?.usage?.usage_timestamp,
|
|
643
|
+
session?.usage?.usage_timestamp_ms,
|
|
644
|
+
session?.usage?.usageTsMs,
|
|
645
|
+
parsed?.ts,
|
|
646
|
+
parsed?.time,
|
|
647
|
+
parsed?.timestamp,
|
|
648
|
+
parsed?.tsMs,
|
|
649
|
+
parsed?.timestampMs,
|
|
650
|
+
parsed?.usageTs,
|
|
651
|
+
parsed?.usageTimestamp,
|
|
652
|
+
parsed?.usage_timestamp,
|
|
653
|
+
parsed?.usage_timestamp_ms,
|
|
654
|
+
parsed?.usage_time,
|
|
655
|
+
parsed?.updatedAt,
|
|
656
|
+
parsed?.updated_at,
|
|
657
|
+
parsed?.updatedAtMs,
|
|
658
|
+
parsed?.updated_at_ms,
|
|
659
|
+
parsed?.status?.updatedAt,
|
|
660
|
+
parsed?.status?.updated_at,
|
|
661
|
+
parsed?.status?.updatedAtMs,
|
|
662
|
+
parsed?.status?.updated_at_ms,
|
|
663
|
+
parsed?.status?.timestamp,
|
|
664
|
+
parsed?.status?.time,
|
|
665
|
+
parsed?.status?.ts,
|
|
666
|
+
parsed?.status?.tsMs,
|
|
667
|
+
parsed?.status?.usageTs,
|
|
668
|
+
parsed?.status?.usageTimestamp,
|
|
669
|
+
parsed?.status?.usage_timestamp,
|
|
670
|
+
parsed?.status?.usage_timestamp_ms,
|
|
671
|
+
parsed?.status?.usageTsMs
|
|
672
|
+
) ??
|
|
673
|
+
(Number.isFinite(sessionAgeMs) && sessionAgeMs >= 0 ? Date.now() - sessionAgeMs : pickTimestamp(parsed?.ts, parsed?.time, parsed?.updatedAt, parsed?.updated_at, parsed?.updatedAtMs, parsed?.timestamp))
|
|
674
|
+
|
|
675
|
+
const hasStrongUsage = model !== null || totalTokens !== null || tokensPerMin !== null
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
model,
|
|
679
|
+
totalTokens,
|
|
680
|
+
tokensPerMin,
|
|
681
|
+
sessionId: pickString(session.sessionId, session.id, session?.usage?.sessionId, session?.usage?.id, session?.session_id),
|
|
682
|
+
agentId: pickString(session.agentId, session?.usage?.agentId, session?.agent_id),
|
|
683
|
+
usageTimestampMs,
|
|
684
|
+
integrationStatus: hasStrongUsage ? 'ok' : 'partial'
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function parseGenericUsage(parsed) {
|
|
689
|
+
const usageCandidates = [
|
|
690
|
+
parsed?.usage,
|
|
691
|
+
parsed?.sessionUsage,
|
|
692
|
+
parsed?.stats,
|
|
693
|
+
parsed?.stats?.current,
|
|
694
|
+
parsed?.payload?.usage,
|
|
695
|
+
parsed?.payload?.sessionUsage,
|
|
696
|
+
parsed?.payload?.stats,
|
|
697
|
+
parsed?.payload?.stats?.current,
|
|
698
|
+
parsed?.payload?.stats?.session,
|
|
699
|
+
parsed?.data?.usage,
|
|
700
|
+
parsed?.data?.sessionUsage,
|
|
701
|
+
parsed?.data?.stats,
|
|
702
|
+
parsed?.data?.stats?.current,
|
|
703
|
+
parsed?.data?.stats?.session,
|
|
704
|
+
parsed?.result?.usage,
|
|
705
|
+
parsed?.result?.sessionUsage,
|
|
706
|
+
parsed?.result?.stats,
|
|
707
|
+
parsed?.result?.stats?.current,
|
|
708
|
+
parsed?.result?.stats?.session,
|
|
709
|
+
parsed?.payload?.result?.usage,
|
|
710
|
+
parsed?.payload?.result?.sessionUsage,
|
|
711
|
+
parsed?.payload?.result?.stats,
|
|
712
|
+
parsed?.payload?.result?.stats?.current,
|
|
713
|
+
parsed?.payload?.result?.stats?.session,
|
|
714
|
+
parsed?.data?.result?.usage,
|
|
715
|
+
parsed?.data?.result?.sessionUsage,
|
|
716
|
+
parsed?.data?.result?.stats,
|
|
717
|
+
parsed?.data?.result?.stats?.current,
|
|
718
|
+
parsed?.data?.result?.stats?.session,
|
|
719
|
+
parsed?.status?.usage,
|
|
720
|
+
parsed?.status?.sessionUsage,
|
|
721
|
+
parsed?.status?.stats,
|
|
722
|
+
parsed?.status?.stats?.current,
|
|
723
|
+
parsed?.status?.stats?.session,
|
|
724
|
+
parsed?.status?.result?.usage,
|
|
725
|
+
parsed?.status?.result?.sessionUsage,
|
|
726
|
+
parsed?.status?.result?.stats,
|
|
727
|
+
parsed?.status?.result?.stats?.current,
|
|
728
|
+
parsed?.status?.result?.stats?.session,
|
|
729
|
+
parsed?.current,
|
|
730
|
+
parsed?.session,
|
|
731
|
+
parsed?.result?.current,
|
|
732
|
+
parsed?.result?.session,
|
|
733
|
+
parsed?.result?.data?.current,
|
|
734
|
+
parsed?.result?.data?.session,
|
|
735
|
+
parsed?.result?.data?.stats,
|
|
736
|
+
parsed?.data?.current,
|
|
737
|
+
parsed?.data?.session,
|
|
738
|
+
parsed?.data?.result?.current,
|
|
739
|
+
parsed?.data?.result?.session,
|
|
740
|
+
parsed?.data?.stats,
|
|
741
|
+
parsed?.payload?.current,
|
|
742
|
+
parsed?.payload?.session,
|
|
743
|
+
parsed?.payload?.result?.current,
|
|
744
|
+
parsed?.payload?.result?.session,
|
|
745
|
+
parsed?.payload?.data?.current,
|
|
746
|
+
parsed?.payload?.data?.session,
|
|
747
|
+
parsed?.payload?.data?.stats,
|
|
748
|
+
parsed?.status?.current,
|
|
749
|
+
parsed?.status?.session,
|
|
750
|
+
parsed?.status?.result?.current,
|
|
751
|
+
parsed?.status?.result?.session,
|
|
752
|
+
parsed?.status?.data?.current,
|
|
753
|
+
parsed?.status?.data?.session,
|
|
754
|
+
parsed?.status?.data?.stats,
|
|
755
|
+
parsed
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
const usage = usageCandidates.reduce((found, candidate) => {
|
|
759
|
+
if (found) return found
|
|
760
|
+
const envelope = extractUsageEnvelope(candidate)
|
|
761
|
+
return envelope && typeof envelope === 'object' ? envelope : null
|
|
762
|
+
}, null)
|
|
763
|
+
|
|
764
|
+
const usageRecord = Array.isArray(usage) ? pickBestRecentSession(usage) : usage
|
|
765
|
+
|
|
766
|
+
const usageTotals = usageRecord?.totals || usageRecord?.summary || usageRecord?.usageTotals || usageRecord?.usage?.totals || usageRecord?.usage?.summary
|
|
767
|
+
const model = pickString(
|
|
768
|
+
parsed?.model,
|
|
769
|
+
parsed?.default_model,
|
|
770
|
+
parsed?.modelName,
|
|
771
|
+
parsed?.model_name,
|
|
772
|
+
parsed?.status?.model,
|
|
773
|
+
parsed?.status?.default_model,
|
|
774
|
+
parsed?.status?.modelName,
|
|
775
|
+
parsed?.status?.model_name,
|
|
776
|
+
usageRecord?.model,
|
|
777
|
+
usageRecord?.modelName,
|
|
778
|
+
usageRecord?.model_name,
|
|
779
|
+
usageRecord?.defaultModel,
|
|
780
|
+
usageRecord?.default_model,
|
|
781
|
+
usageTotals?.model,
|
|
782
|
+
usageTotals?.modelName,
|
|
783
|
+
usageTotals?.model_name,
|
|
784
|
+
usageTotals?.defaultModel,
|
|
785
|
+
usageTotals?.default_model,
|
|
786
|
+
parsed?.result?.model,
|
|
787
|
+
parsed?.result?.modelName,
|
|
788
|
+
parsed?.result?.model_name,
|
|
789
|
+
parsed?.data?.model,
|
|
790
|
+
parsed?.data?.modelName,
|
|
791
|
+
parsed?.data?.model_name,
|
|
792
|
+
parsed?.data?.defaultModel,
|
|
793
|
+
parsed?.data?.default_model,
|
|
794
|
+
parsed?.payload?.model,
|
|
795
|
+
parsed?.payload?.modelName,
|
|
796
|
+
parsed?.payload?.model_name,
|
|
797
|
+
parsed?.payload?.defaultModel,
|
|
798
|
+
parsed?.payload?.default_model
|
|
799
|
+
)
|
|
800
|
+
const totalTokens = pickNumber(
|
|
801
|
+
usageRecord?.totalTokens,
|
|
802
|
+
usageRecord?.total_tokens,
|
|
803
|
+
usageRecord?.tokenCount,
|
|
804
|
+
usageRecord?.token_count,
|
|
805
|
+
usageRecord?.tokens,
|
|
806
|
+
usageRecord?.tokenUsage?.total,
|
|
807
|
+
usageRecord?.token_usage?.total,
|
|
808
|
+
usageRecord?.tokens?.total,
|
|
809
|
+
usageRecord?.tokens?.sum,
|
|
810
|
+
usageRecord?.inputTokens && usageRecord?.outputTokens ? usageRecord.inputTokens + usageRecord.outputTokens : null,
|
|
811
|
+
usageRecord?.input_tokens && usageRecord?.output_tokens ? usageRecord.input_tokens + usageRecord.output_tokens : null,
|
|
812
|
+
usageTotals?.total,
|
|
813
|
+
usageTotals?.totalTokens,
|
|
814
|
+
usageTotals?.total_tokens,
|
|
815
|
+
usageTotals?.tokenCount,
|
|
816
|
+
usageTotals?.token_count,
|
|
817
|
+
usageTotals?.tokens?.total,
|
|
818
|
+
usageTotals?.tokens?.sum,
|
|
819
|
+
usageTotals?.inputTokens && usageTotals?.outputTokens ? usageTotals.inputTokens + usageTotals.outputTokens : null,
|
|
820
|
+
usageTotals?.input_tokens && usageTotals?.output_tokens ? usageTotals.input_tokens + usageTotals.output_tokens : null,
|
|
821
|
+
parsed?.totals?.total,
|
|
822
|
+
parsed?.totals?.tokenCount,
|
|
823
|
+
parsed?.totals?.total_tokens
|
|
824
|
+
)
|
|
825
|
+
const tokensPerMin = pickNumber(
|
|
826
|
+
usageRecord?.tokensPerMinute,
|
|
827
|
+
usageRecord?.tokens_per_minute,
|
|
828
|
+
usageRecord?.tpm,
|
|
829
|
+
usageRecord?.tokenRate,
|
|
830
|
+
usageRecord?.requestsPerMinute,
|
|
831
|
+
usageRecord?.rps,
|
|
832
|
+
usageRecord?.tokens?.perMin,
|
|
833
|
+
usageRecord?.tokens?.perMinute,
|
|
834
|
+
usageTotals?.tokensPerMinute,
|
|
835
|
+
usageTotals?.tokens_per_minute,
|
|
836
|
+
usageTotals?.rps,
|
|
837
|
+
parsed?.rps,
|
|
838
|
+
parsed?.requestsPerMinute
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
if (model === null && totalTokens === null && tokensPerMin === null) return null
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
model,
|
|
845
|
+
totalTokens,
|
|
846
|
+
tokensPerMin,
|
|
847
|
+
sessionId: pickString(
|
|
848
|
+
parsed?.sessionId,
|
|
849
|
+
parsed?.session_id,
|
|
850
|
+
parsed?.payload?.sessionId,
|
|
851
|
+
parsed?.payload?.session_id,
|
|
852
|
+
usageRecord?.sessionId,
|
|
853
|
+
usageRecord?.session_id,
|
|
854
|
+
usageRecord?.id
|
|
855
|
+
),
|
|
856
|
+
agentId: pickString(
|
|
857
|
+
parsed?.agentId,
|
|
858
|
+
parsed?.agent_id,
|
|
859
|
+
parsed?.payload?.agentId,
|
|
860
|
+
parsed?.payload?.agent_id,
|
|
861
|
+
usageRecord?.agentId,
|
|
862
|
+
usageRecord?.agent_id
|
|
863
|
+
),
|
|
864
|
+
usageTimestampMs: pickTimestamp(
|
|
865
|
+
usageRecord?.updatedAt,
|
|
866
|
+
usageRecord?.updated_at,
|
|
867
|
+
usageRecord?.updatedAtMs,
|
|
868
|
+
usageRecord?.ts,
|
|
869
|
+
usageRecord?.time,
|
|
870
|
+
usageRecord?.timestamp,
|
|
871
|
+
usageRecord?.tsMs,
|
|
872
|
+
usageRecord?.timestampMs,
|
|
873
|
+
usageRecord?.usageTs,
|
|
874
|
+
usageRecord?.usageTsMs,
|
|
875
|
+
usageRecord?.usageTimestamp,
|
|
876
|
+
usageRecord?.usage_timestamp,
|
|
877
|
+
usageRecord?.usage_timestamp_ms,
|
|
878
|
+
usageRecord?.usage_time,
|
|
879
|
+
usageRecord?.usageTimestampMs,
|
|
880
|
+
usageTotals?.updatedAt,
|
|
881
|
+
usageTotals?.updated_at,
|
|
882
|
+
usageTotals?.updatedAtMs,
|
|
883
|
+
usageTotals?.updated_at_ms,
|
|
884
|
+
usageTotals?.ts,
|
|
885
|
+
usageTotals?.time,
|
|
886
|
+
usageTotals?.timestamp,
|
|
887
|
+
usageTotals?.timestampMs,
|
|
888
|
+
usageTotals?.tsMs,
|
|
889
|
+
usageTotals?.usageTs,
|
|
890
|
+
usageTotals?.usageTsMs,
|
|
891
|
+
usageTotals?.usageTimestamp,
|
|
892
|
+
usageTotals?.usage_timestamp,
|
|
893
|
+
usageTotals?.usage_timestamp_ms,
|
|
894
|
+
usageTotals?.usage_time,
|
|
895
|
+
parsed?.payload?.updatedAt,
|
|
896
|
+
parsed?.payload?.updated_at,
|
|
897
|
+
parsed?.payload?.updatedAtMs,
|
|
898
|
+
parsed?.payload?.ts,
|
|
899
|
+
parsed?.payload?.time,
|
|
900
|
+
parsed?.payload?.timestamp,
|
|
901
|
+
parsed?.payload?.tsMs,
|
|
902
|
+
parsed?.payload?.usageTs,
|
|
903
|
+
parsed?.payload?.usageTsMs,
|
|
904
|
+
parsed?.payload?.usageTimestamp,
|
|
905
|
+
parsed?.payload?.usage_timestamp,
|
|
906
|
+
parsed?.payload?.usage_timestamp_ms,
|
|
907
|
+
parsed?.payload?.usage_time,
|
|
908
|
+
parsed?.payload?.usageTimestampMs,
|
|
909
|
+
parsed?.ts,
|
|
910
|
+
parsed?.time,
|
|
911
|
+
parsed?.timestamp,
|
|
912
|
+
parsed?.tsMs,
|
|
913
|
+
parsed?.timestampMs,
|
|
914
|
+
parsed?.usageTs,
|
|
915
|
+
parsed?.usageTimestamp,
|
|
916
|
+
parsed?.usage_timestamp,
|
|
917
|
+
parsed?.usage_timestamp_ms,
|
|
918
|
+
parsed?.usage_time,
|
|
919
|
+
parsed?.updatedAt,
|
|
920
|
+
parsed?.updated_at,
|
|
921
|
+
parsed?.updatedAtMs,
|
|
922
|
+
parsed?.updated_at_ms,
|
|
923
|
+
parsed?.status?.updatedAt,
|
|
924
|
+
parsed?.status?.updated_at,
|
|
925
|
+
parsed?.status?.updatedAtMs,
|
|
926
|
+
parsed?.status?.updated_at_ms,
|
|
927
|
+
parsed?.status?.timestamp,
|
|
928
|
+
parsed?.status?.time,
|
|
929
|
+
parsed?.status?.ts,
|
|
930
|
+
parsed?.status?.tsMs,
|
|
931
|
+
parsed?.status?.usageTs,
|
|
932
|
+
parsed?.status?.usageTimestamp,
|
|
933
|
+
parsed?.status?.usage_timestamp,
|
|
934
|
+
parsed?.status?.usage_timestamp_ms,
|
|
935
|
+
parsed?.status?.usage_time,
|
|
936
|
+
parsed?.status?.usageTsMs
|
|
937
|
+
),
|
|
938
|
+
integrationStatus: 'ok'
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function usageCandidateScore(usage) {
|
|
943
|
+
if (!usage) return -1
|
|
944
|
+
|
|
945
|
+
let score = usage.integrationStatus === 'ok' ? 8 : 1
|
|
946
|
+
|
|
947
|
+
if (usage.model !== null) score += 2
|
|
948
|
+
if (usage.totalTokens !== null) score += 2
|
|
949
|
+
if (usage.tokensPerMin !== null) score += 1
|
|
950
|
+
if (usage.sessionId !== null) score += 1
|
|
951
|
+
if (usage.agentId !== null) score += 1
|
|
952
|
+
if (usage.usageTimestampMs !== null) score += 1
|
|
953
|
+
|
|
954
|
+
return score
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
export function parseOpenClawUsage(raw) {
|
|
958
|
+
if (!raw) return null
|
|
959
|
+
|
|
960
|
+
const candidates = extractJsonCandidates(raw)
|
|
961
|
+
if (candidates.length === 0) return null
|
|
962
|
+
|
|
963
|
+
let bestMatch = null
|
|
964
|
+
let bestScore = -1
|
|
965
|
+
let bestSource = null
|
|
966
|
+
let hasError = false
|
|
967
|
+
|
|
968
|
+
const compareByRecency = (a, b) => {
|
|
969
|
+
if (!a || !b) return 0
|
|
970
|
+
const aTs = a.usageTimestampMs
|
|
971
|
+
const bTs = b.usageTimestampMs
|
|
972
|
+
if (aTs === null && bTs === null) return 0
|
|
973
|
+
if (aTs === null) return -1
|
|
974
|
+
if (bTs === null) return 1
|
|
975
|
+
if (aTs === bTs) return 0
|
|
976
|
+
return aTs > bTs ? 1 : -1
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
for (const candidate of candidates) {
|
|
980
|
+
let parsed
|
|
981
|
+
try {
|
|
982
|
+
parsed = JSON.parse(candidate)
|
|
983
|
+
} catch {
|
|
984
|
+
hasError = true
|
|
985
|
+
continue
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const normalized = normalizeOpenClawAliases(parsed)
|
|
989
|
+
const fromStatus = parseFromStatusJson(normalized)
|
|
990
|
+
if (fromStatus) {
|
|
991
|
+
const score = usageCandidateScore(fromStatus)
|
|
992
|
+
if (
|
|
993
|
+
score > bestScore ||
|
|
994
|
+
(score === bestScore && compareByRecency(fromStatus, bestMatch) > 0) ||
|
|
995
|
+
(score === bestScore && bestSource === 'generic' && compareByRecency(fromStatus, bestMatch) === 0)
|
|
996
|
+
) {
|
|
997
|
+
bestMatch = fromStatus
|
|
998
|
+
bestScore = score
|
|
999
|
+
bestSource = 'status'
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const fromGeneric = parseGenericUsage(normalized)
|
|
1004
|
+
if (fromGeneric) {
|
|
1005
|
+
const score = usageCandidateScore(fromGeneric)
|
|
1006
|
+
if (
|
|
1007
|
+
score > bestScore ||
|
|
1008
|
+
(score === bestScore && compareByRecency(fromGeneric, bestMatch) > 0) ||
|
|
1009
|
+
(score === bestScore && compareByRecency(fromGeneric, bestMatch) === 0 && bestSource !== 'generic')
|
|
1010
|
+
) {
|
|
1011
|
+
bestMatch = fromGeneric
|
|
1012
|
+
bestScore = score
|
|
1013
|
+
bestSource = 'generic'
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
return hasError && bestMatch === null ? null : bestMatch
|
|
1020
|
+
}
|