agent-dealer 1.1.10 → 1.2.1
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/bundle/server/dist/capacity/claude-events.js +368 -0
- package/bundle/server/dist/capacity/claude-events.test.js +322 -0
- package/bundle/server/dist/capacity/codex-app-server.js +602 -0
- package/bundle/server/dist/capacity/codex-app-server.test.js +303 -0
- package/bundle/server/dist/capacity/cursor-team.js +663 -0
- package/bundle/server/dist/capacity/cursor-team.test.js +556 -0
- package/bundle/server/dist/capacity/muse.js +597 -0
- package/bundle/server/dist/capacity/muse.test.js +282 -0
- package/bundle/server/dist/coordinator/developer-effect.js +24 -7
- package/bundle/server/dist/coordinator/developer-effect.test.js +142 -5
- package/bundle/server/dist/coordinator/prompts.js +1 -1
- package/bundle/server/dist/coordinator/prompts.test.js +4 -2
- package/bundle/server/dist/coordinator/reviewer-effect.js +12 -0
- package/bundle/server/dist/db/index.js +44 -0
- package/bundle/server/dist/db/schema.sql +25 -0
- package/bundle/server/dist/repository/cursor-team-billing.js +103 -0
- package/bundle/server/dist/repository/runtime-capacity.js +16 -0
- package/bundle/server/dist/routes/cursor-team-billing.test.js +88 -0
- package/bundle/server/dist/routes/index.js +53 -2
- package/bundle/server/dist/routes/runtime-capacity.test.js +71 -1
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/{index-DMhdyiFQ.css → index-ChnI0F02.css} +1 -1
- package/bundle/server/static-ui/assets/index-CyB1573Q.js +60 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/runtime-capacity.d.ts +96 -0
- package/bundle/shared/dist/runtime-capacity.js +77 -0
- package/bundle/shared/dist/runtime-capacity.test.js +50 -1
- package/bundle/shared/package.json +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-C2UigoIF.js +0 -60
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// packages/server/src/capacity/claude-events.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-248: Claude capacity adapter over naturally emitted structured
|
|
4
|
+
// rate-limit events from Dealer-managed Claude sessions.
|
|
5
|
+
//
|
|
6
|
+
// Observational only. This module parses `rate_limit_event.rate_limit_info.
|
|
7
|
+
// unifiedWindows` out of session NDJSON that Claude Code already emitted
|
|
8
|
+
// during real work and persists the windows as normalized capacity snapshots
|
|
9
|
+
// (source `observed_event`). It never launches `claude -p`, `/usage`, or any
|
|
10
|
+
// other synthetic model call, never touches Anthropic's undocumented OAuth
|
|
11
|
+
// usage endpoint, and never reads plan names, token totals, or session cost.
|
|
12
|
+
//
|
|
13
|
+
// Separation from NOT-111 hard caps: this module never reads or writes
|
|
14
|
+
// `runtime_availability`. A rejected rate-limit event still flows through
|
|
15
|
+
// `runners/usage-cap.ts` for defer/admission; a session whose events carry
|
|
16
|
+
// unified windows additionally flows through here for capacity — the two
|
|
17
|
+
// states coexist on independent rows.
|
|
18
|
+
//
|
|
19
|
+
// Observed-at semantics (repair round 2 — a session-end stamp is a lie for a
|
|
20
|
+
// long session, so each event keeps its own time):
|
|
21
|
+
// - When the event carries its own timestamp it is used (clamped to `nowMs` —
|
|
22
|
+
// an event cannot be newer than the session end that is ingesting it).
|
|
23
|
+
// - Otherwise the caller passes the session's spawn start
|
|
24
|
+
// (`fallbackObservedAtMs`); the event happened no earlier than spawn, so
|
|
25
|
+
// this understates freshness rather than overstating it.
|
|
26
|
+
// - Only when neither exists does ingestion time (`nowMs`) apply.
|
|
27
|
+
// - A reading never overwrites a stored row whose `observed_at` is newer, so
|
|
28
|
+
// a later-finishing long session cannot clobber a concurrent session's
|
|
29
|
+
// fresher reading.
|
|
30
|
+
//
|
|
31
|
+
// Provider shape (tolerant — only the dotted path above is contractual):
|
|
32
|
+
// - `unifiedWindows` may be an array of window objects or a map of
|
|
33
|
+
// bucket-name → window object. Array entries name their bucket via
|
|
34
|
+
// `window`, `bucket`, `name`, `id`, `key`, `limitType`, `rateLimitType`,
|
|
35
|
+
// or `type` (first present string wins).
|
|
36
|
+
// - Utilization is a 0–1 fraction (`utilization`, `used`, `usedFraction`,
|
|
37
|
+
// `used_fraction`); an explicit 0–100 scale (`usedPercent`,
|
|
38
|
+
// `used_percent`, `utilizationPercent`, `utilization_percent`) takes
|
|
39
|
+
// precedence when both are present. A bare finite number as a map value
|
|
40
|
+
// reads as a utilization fraction. Entries without a usable scale are
|
|
41
|
+
// skipped — never fabricated.
|
|
42
|
+
// - Reset time accepts epoch seconds, epoch milliseconds, or ISO-8601
|
|
43
|
+
// (`resetsAt`, `resets_at`, `resetAt`, `reset_at`); missing/unparsable
|
|
44
|
+
// resets persist as null.
|
|
45
|
+
// - Duration accepts explicit minute fields when present; otherwise it is
|
|
46
|
+
// inferred from well-known bucket names only (`five_hour` → 300,
|
|
47
|
+
// `seven_day*`/`weekly` → 10080). Anything else keeps duration null and
|
|
48
|
+
// the provider label verbatim — never guessed.
|
|
49
|
+
// - Every distinct provider bucket keeps its identity: `providerBucket` is
|
|
50
|
+
// the raw bucket string and `windowKey` is namespaced from it, so
|
|
51
|
+
// five-hour, seven-day, and model/overage-specific seven-day buckets
|
|
52
|
+
// persist as independent rows.
|
|
53
|
+
import fs from "node:fs";
|
|
54
|
+
import { parseNdjson } from "../runners/stream-json.js";
|
|
55
|
+
import { listCapacitySnapshots, recordCapacitySnapshots, } from "../repository/runtime-capacity.js";
|
|
56
|
+
import { normalizeAdapterWindow, } from "./adapter.js";
|
|
57
|
+
export const CLAUDE_RUNTIME = "claude_code";
|
|
58
|
+
/** Evidence refs are static pointers — never credentials or raw payloads. */
|
|
59
|
+
export const CLAUDE_UNIFIED_WINDOWS_EVIDENCE_REF = "claude-session:unified-windows";
|
|
60
|
+
function asFiniteNumber(value) {
|
|
61
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
62
|
+
}
|
|
63
|
+
function pickNumber(candidates) {
|
|
64
|
+
for (const c of candidates) {
|
|
65
|
+
const n = asFiniteNumber(c);
|
|
66
|
+
if (n !== null)
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function pickString(candidates) {
|
|
72
|
+
for (const c of candidates) {
|
|
73
|
+
if (typeof c === "string" && c.length > 0)
|
|
74
|
+
return c;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** resetsAt arrives as epoch seconds, epoch ms, or ISO-8601 — normalize to ISO. */
|
|
79
|
+
export function normalizeClaudeResetsAt(value) {
|
|
80
|
+
if (value === null || value === undefined)
|
|
81
|
+
return null;
|
|
82
|
+
if (typeof value === "number") {
|
|
83
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
84
|
+
return null;
|
|
85
|
+
const ms = value > 1e12 ? value : value * 1000;
|
|
86
|
+
const d = new Date(ms);
|
|
87
|
+
return Number.isFinite(d.getTime()) ? d.toISOString() : null;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === "string") {
|
|
90
|
+
const trimmed = value.trim();
|
|
91
|
+
if (!trimmed)
|
|
92
|
+
return null;
|
|
93
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed))
|
|
94
|
+
return normalizeClaudeResetsAt(Number(trimmed));
|
|
95
|
+
const ms = Date.parse(trimmed);
|
|
96
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Parse an event timestamp (ISO-8601, epoch seconds, or epoch milliseconds)
|
|
102
|
+
* to epoch ms. Returns null when the value carries no usable time — the
|
|
103
|
+
* caller then falls back conservatively to the session spawn start.
|
|
104
|
+
*/
|
|
105
|
+
export function parseClaudeEventTimestamp(value) {
|
|
106
|
+
if (typeof value === "number") {
|
|
107
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
108
|
+
return null;
|
|
109
|
+
return value > 1e12 ? value : value * 1000;
|
|
110
|
+
}
|
|
111
|
+
if (typeof value === "string") {
|
|
112
|
+
const trimmed = value.trim();
|
|
113
|
+
if (!trimmed)
|
|
114
|
+
return null;
|
|
115
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed))
|
|
116
|
+
return parseClaudeEventTimestamp(Number(trimmed));
|
|
117
|
+
const ms = Date.parse(trimmed);
|
|
118
|
+
return Number.isFinite(ms) ? ms : null;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const EVENT_TIMESTAMP_KEYS = [
|
|
123
|
+
"timestamp",
|
|
124
|
+
"eventTimestamp",
|
|
125
|
+
"event_timestamp",
|
|
126
|
+
"emittedAt",
|
|
127
|
+
"emitted_at",
|
|
128
|
+
"observedAt",
|
|
129
|
+
"observed_at",
|
|
130
|
+
"ts",
|
|
131
|
+
"time",
|
|
132
|
+
];
|
|
133
|
+
/**
|
|
134
|
+
* Per-event observed time: the event's own timestamp when present and valid
|
|
135
|
+
* (clamped to `nowMs`), otherwise the session spawn start passed by the
|
|
136
|
+
* caller, otherwise ingestion time. Stream-json lines carry no documented
|
|
137
|
+
* timestamp field, so several spellings are accepted — an unknown shape
|
|
138
|
+
* simply falls through to the conservative spawn-start fallback.
|
|
139
|
+
*/
|
|
140
|
+
export function claudeEventObservedAt(event, nowMs, fallbackObservedAtMs) {
|
|
141
|
+
const fallback = new Date(Math.min(fallbackObservedAtMs, nowMs)).toISOString();
|
|
142
|
+
for (const key of EVENT_TIMESTAMP_KEYS) {
|
|
143
|
+
if (event[key] === undefined || event[key] === null)
|
|
144
|
+
continue;
|
|
145
|
+
const parsed = parseClaudeEventTimestamp(event[key]);
|
|
146
|
+
if (parsed === null)
|
|
147
|
+
continue;
|
|
148
|
+
return new Date(Math.min(parsed, nowMs)).toISOString();
|
|
149
|
+
}
|
|
150
|
+
return fallback;
|
|
151
|
+
}
|
|
152
|
+
function sanitizeKeySegment(name) {
|
|
153
|
+
const s = name
|
|
154
|
+
.toLowerCase()
|
|
155
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
156
|
+
.replace(/^_+|_+$/g, "");
|
|
157
|
+
return s || "window";
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Duration inference from well-known provider bucket names only. Model and
|
|
161
|
+
* overage variants (e.g. `seven_day_sonnet`) share the seven-day length but
|
|
162
|
+
* keep their own bucket identity — only the duration is shared, never the
|
|
163
|
+
* row.
|
|
164
|
+
*/
|
|
165
|
+
export function inferClaudeDurationMinutes(bucket) {
|
|
166
|
+
const b = bucket.toLowerCase();
|
|
167
|
+
if (/five[\s_-]*hour/.test(b))
|
|
168
|
+
return 300;
|
|
169
|
+
if (/seven[\s_-]*day/.test(b) || b === "weekly")
|
|
170
|
+
return 10080;
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
function windowReadingFromEntry(bucket, entry, observedAt) {
|
|
174
|
+
const cleanBucket = bucket.trim();
|
|
175
|
+
if (!cleanBucket)
|
|
176
|
+
return null;
|
|
177
|
+
let usedFraction = null;
|
|
178
|
+
let usedPercent = null;
|
|
179
|
+
let resetRaw = null;
|
|
180
|
+
let durationRaw = null;
|
|
181
|
+
if (typeof entry === "number") {
|
|
182
|
+
// Bare map value (`"five_hour": 0.17`) reads as a utilization fraction.
|
|
183
|
+
usedFraction = asFiniteNumber(entry);
|
|
184
|
+
}
|
|
185
|
+
else if (entry && typeof entry === "object") {
|
|
186
|
+
const w = entry;
|
|
187
|
+
usedPercent = pickNumber([
|
|
188
|
+
w.usedPercent,
|
|
189
|
+
w.used_percent,
|
|
190
|
+
w.utilizationPercent,
|
|
191
|
+
w.utilization_percent,
|
|
192
|
+
]);
|
|
193
|
+
usedFraction = pickNumber([w.utilization, w.used, w.usedFraction, w.used_fraction]);
|
|
194
|
+
resetRaw = w.resetsAt ?? w.resets_at ?? w.resetAt ?? w.reset_at ?? null;
|
|
195
|
+
durationRaw = pickNumber([
|
|
196
|
+
w.durationMinutes,
|
|
197
|
+
w.duration_minutes,
|
|
198
|
+
w.windowDurationMins,
|
|
199
|
+
w.window_duration_mins,
|
|
200
|
+
w.windowDurationMinutes,
|
|
201
|
+
]);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (usedPercent === null && usedFraction === null)
|
|
207
|
+
return null;
|
|
208
|
+
const durationMinutes = durationRaw !== null ? Math.round(durationRaw) : inferClaudeDurationMinutes(cleanBucket);
|
|
209
|
+
return {
|
|
210
|
+
windowKey: `claude_unified_${sanitizeKeySegment(cleanBucket)}`,
|
|
211
|
+
providerBucket: cleanBucket,
|
|
212
|
+
durationMinutes,
|
|
213
|
+
providerLabel: cleanBucket,
|
|
214
|
+
usedValue: usedPercent ?? usedFraction,
|
|
215
|
+
usedUnit: usedPercent !== null ? "percent" : "fraction",
|
|
216
|
+
...(usedPercent !== null ? { usedPercent } : { usedFraction: usedFraction }),
|
|
217
|
+
resetAt: normalizeClaudeResetsAt(resetRaw),
|
|
218
|
+
observedAt,
|
|
219
|
+
source: "observed_event",
|
|
220
|
+
evidenceRef: CLAUDE_UNIFIED_WINDOWS_EVIDENCE_REF,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Normalize one `rate_limit_info` object's `unifiedWindows` into adapter
|
|
225
|
+
* readings. Returns null when nothing usable is present (missing field,
|
|
226
|
+
* wrong shape, or no entry with a usable scale) — the caller records
|
|
227
|
+
* nothing rather than fabricating a value.
|
|
228
|
+
*/
|
|
229
|
+
export function claudeUnifiedWindowsToReadings(rateLimitInfo, observedAt = new Date().toISOString()) {
|
|
230
|
+
if (!rateLimitInfo || typeof rateLimitInfo !== "object")
|
|
231
|
+
return null;
|
|
232
|
+
const raw = rateLimitInfo.unifiedWindows;
|
|
233
|
+
if (!raw || typeof raw !== "object")
|
|
234
|
+
return null;
|
|
235
|
+
// Last entry wins per window key; identities never collapse across
|
|
236
|
+
// distinct buckets.
|
|
237
|
+
const byKey = new Map();
|
|
238
|
+
const push = (bucket, entry) => {
|
|
239
|
+
if (typeof bucket !== "string" || !bucket.trim())
|
|
240
|
+
return;
|
|
241
|
+
const reading = windowReadingFromEntry(bucket, entry, observedAt);
|
|
242
|
+
if (reading)
|
|
243
|
+
byKey.set(reading.windowKey, reading);
|
|
244
|
+
};
|
|
245
|
+
if (Array.isArray(raw)) {
|
|
246
|
+
for (const item of raw) {
|
|
247
|
+
if (!item || typeof item !== "object")
|
|
248
|
+
continue;
|
|
249
|
+
const w = item;
|
|
250
|
+
push(pickString([
|
|
251
|
+
w.window,
|
|
252
|
+
w.bucket,
|
|
253
|
+
w.name,
|
|
254
|
+
w.id,
|
|
255
|
+
w.key,
|
|
256
|
+
w.limitType,
|
|
257
|
+
w.limit_type,
|
|
258
|
+
w.rateLimitType,
|
|
259
|
+
w.rate_limit_type,
|
|
260
|
+
w.type,
|
|
261
|
+
]), item);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
for (const [bucket, entry] of Object.entries(raw)) {
|
|
266
|
+
push(bucket, entry);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return byKey.size > 0 ? { windows: [...byKey.values()] } : null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Scan Claude session events for `rate_limit_event`s carrying usable
|
|
273
|
+
* `unifiedWindows`. Status is irrelevant — allowed events carry the
|
|
274
|
+
* utilization signal; rejected ones additionally drive NOT-111 elsewhere.
|
|
275
|
+
* Each window keeps the freshest reading seen for its key (ties break toward
|
|
276
|
+
* the later event), and every reading is stamped with its own event time —
|
|
277
|
+
* never a single session-end stamp. Returns null when no event yields a
|
|
278
|
+
* reading.
|
|
279
|
+
*/
|
|
280
|
+
export function extractClaudeCapacityFromEvents(events, nowMs = Date.now(), fallbackObservedAtMs = nowMs) {
|
|
281
|
+
const byKey = new Map();
|
|
282
|
+
for (const e of events) {
|
|
283
|
+
if (e.type !== "rate_limit_event")
|
|
284
|
+
continue;
|
|
285
|
+
const info = e.rate_limit_info;
|
|
286
|
+
if (!info || typeof info !== "object")
|
|
287
|
+
continue;
|
|
288
|
+
const observedAt = claudeEventObservedAt(e, nowMs, fallbackObservedAtMs);
|
|
289
|
+
const parsed = claudeUnifiedWindowsToReadings(info, observedAt);
|
|
290
|
+
if (!parsed)
|
|
291
|
+
continue;
|
|
292
|
+
for (const w of parsed.windows) {
|
|
293
|
+
const prev = byKey.get(w.windowKey);
|
|
294
|
+
// observedAt is always set by windowReadingFromEntry; `?? ""` only
|
|
295
|
+
// satisfies the optional field type (NaN never wins a comparison).
|
|
296
|
+
if (!prev || Date.parse(w.observedAt ?? "") >= Date.parse(prev.observedAt ?? "")) {
|
|
297
|
+
byKey.set(w.windowKey, w);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (byKey.size === 0)
|
|
302
|
+
return null;
|
|
303
|
+
return { runtime: CLAUDE_RUNTIME, windows: [...byKey.values()], unavailable: [] };
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Persist naturally observed unified windows as normalized capacity
|
|
307
|
+
* snapshots (per-window upserts — siblings not in this observation are left
|
|
308
|
+
* untouched, and a stored row newer than the incoming reading is never
|
|
309
|
+
* overwritten). Returns the number of windows persisted, or null when the
|
|
310
|
+
* runtime is not Claude or no usable windows were observed. Never touches
|
|
311
|
+
* `runtime_availability`.
|
|
312
|
+
*/
|
|
313
|
+
export function recordClaudeCapacityFromEvents(events, runtime, nowMs = Date.now(), fallbackObservedAtMs = nowMs) {
|
|
314
|
+
if (runtime !== CLAUDE_RUNTIME)
|
|
315
|
+
return null;
|
|
316
|
+
const result = extractClaudeCapacityFromEvents(events, nowMs, fallbackObservedAtMs);
|
|
317
|
+
if (!result)
|
|
318
|
+
return null;
|
|
319
|
+
const normalized = result.windows.map((w) => normalizeAdapterWindow(runtime, w, nowMs));
|
|
320
|
+
const persistable = normalized.filter((n) => n.unavailableReason === null && n.remainingPercent !== null);
|
|
321
|
+
if (persistable.length === 0)
|
|
322
|
+
return null;
|
|
323
|
+
const storedObservedAt = new Map(listCapacitySnapshots(runtime).map((row) => [row.windowKey, row.observedAt]));
|
|
324
|
+
const fresh = persistable.filter((w) => {
|
|
325
|
+
const prev = storedObservedAt.get(w.windowKey);
|
|
326
|
+
if (!prev)
|
|
327
|
+
return true;
|
|
328
|
+
const prevMs = Date.parse(prev);
|
|
329
|
+
const nextMs = Date.parse(w.observedAt);
|
|
330
|
+
if (!Number.isFinite(prevMs))
|
|
331
|
+
return true;
|
|
332
|
+
if (!Number.isFinite(nextMs))
|
|
333
|
+
return false;
|
|
334
|
+
return nextMs >= prevMs;
|
|
335
|
+
});
|
|
336
|
+
if (fresh.length === 0)
|
|
337
|
+
return 0;
|
|
338
|
+
recordCapacitySnapshots(runtime, fresh.map((w) => ({
|
|
339
|
+
windowKey: w.windowKey,
|
|
340
|
+
providerBucket: w.providerBucket,
|
|
341
|
+
durationMinutes: w.durationMinutes,
|
|
342
|
+
displayLabel: w.displayLabel,
|
|
343
|
+
usedValue: w.usedValue,
|
|
344
|
+
usedUnit: w.usedUnit,
|
|
345
|
+
remainingPercent: w.remainingPercent,
|
|
346
|
+
resetAt: w.resetAt,
|
|
347
|
+
observedAt: w.observedAt,
|
|
348
|
+
freshUntil: w.freshUntil,
|
|
349
|
+
expiresAt: w.expiresAt,
|
|
350
|
+
source: w.source,
|
|
351
|
+
unavailableReason: w.unavailableReason,
|
|
352
|
+
evidenceRef: w.evidenceRef,
|
|
353
|
+
})));
|
|
354
|
+
return fresh.length;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* File variant for session-end ingestion from a Dealer-managed Claude log.
|
|
358
|
+
* `fallbackObservedAtMs` is the session's spawn start. Returns the persisted
|
|
359
|
+
* window count, or null when the file is missing, the runtime is not Claude,
|
|
360
|
+
* or no usable windows were observed.
|
|
361
|
+
*/
|
|
362
|
+
export function recordClaudeCapacityFromLog(logPath, runtime, nowMs = Date.now(), fallbackObservedAtMs = nowMs) {
|
|
363
|
+
if (runtime !== CLAUDE_RUNTIME)
|
|
364
|
+
return null;
|
|
365
|
+
if (!fs.existsSync(logPath))
|
|
366
|
+
return null;
|
|
367
|
+
return recordClaudeCapacityFromEvents(parseNdjson(fs.readFileSync(logPath, "utf8")), runtime, nowMs, fallbackObservedAtMs);
|
|
368
|
+
}
|