@indigoai-us/hq-cli 5.345.92 → 5.345.93
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/CHANGELOG.md +14 -0
- package/dist/command-catalog.generated.d.ts +252 -0
- package/dist/command-catalog.generated.js +306 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/capacity.d.ts +71 -0
- package/dist/commands/capacity.js +666 -0
- package/dist/commands/integrations-connect.js +64 -5
- package/dist/commands/lanes-accounts.d.ts +1 -0
- package/dist/commands/lanes-accounts.js +6 -0
- package/dist/lib/capacity/accounts.d.ts +36 -0
- package/dist/lib/capacity/accounts.js +90 -0
- package/dist/lib/capacity/collect.d.ts +33 -0
- package/dist/lib/capacity/collect.js +164 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +145 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +1211 -0
- package/dist/lib/capacity/collectors/claude-statusline.d.ts +72 -0
- package/dist/lib/capacity/collectors/claude-statusline.js +436 -0
- package/dist/lib/capacity/collectors/codex.d.ts +120 -0
- package/dist/lib/capacity/collectors/codex.js +1134 -0
- package/dist/lib/capacity/collectors/grok.d.ts +86 -0
- package/dist/lib/capacity/collectors/grok.js +588 -0
- package/dist/lib/capacity/lane-login.d.ts +29 -0
- package/dist/lib/capacity/lane-login.js +91 -0
- package/dist/lib/capacity/ledger.d.ts +399 -0
- package/dist/lib/capacity/ledger.js +1441 -0
- package/dist/lib/capacity/project.d.ts +12 -0
- package/dist/lib/capacity/project.js +264 -0
- package/dist/lib/capacity/read.d.ts +62 -0
- package/dist/lib/capacity/read.js +264 -0
- package/dist/lib/capacity/refusal.d.ts +30 -0
- package/dist/lib/capacity/refusal.js +183 -0
- package/dist/lib/capacity/scheduler.d.ts +76 -0
- package/dist/lib/capacity/scheduler.js +800 -0
- package/dist/lib/capacity/sign-in.d.ts +103 -0
- package/dist/lib/capacity/sign-in.js +517 -0
- package/dist/lib/capacity/widget.d.ts +22 -0
- package/dist/lib/capacity/widget.js +418 -0
- package/dist/lib/lanes/spawn.js +12 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +5 -0
- package/dist/lib/mesh/live/daemon/run.js +29 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex capacity collector for the resident daemon (US-008 calls these;
|
|
3
|
+
* this module does not start the daemon).
|
|
4
|
+
*
|
|
5
|
+
* The session JSONL tail is the primary feed: server-reported token_count
|
|
6
|
+
* rate limits, resumed by byte offset. The app-server read is a live
|
|
7
|
+
* account/rateLimits/read after initialize. That method does not refresh
|
|
8
|
+
* tokens. Starting the app-server process can still rewrite
|
|
9
|
+
* ~/.codex/auth.json for a managed ChatGPT login, so the read stays
|
|
10
|
+
* idle-gated. It never starts a thread, never sends a prompt, and never
|
|
11
|
+
* opens the Codex credential file.
|
|
12
|
+
*/
|
|
13
|
+
import * as crypto from "node:crypto";
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { appendReadings, BURN_WINDOW_MS, redactProviderTokens, } from "../ledger.js";
|
|
18
|
+
export const CODEX_PARSER_VERSION = "codex.1";
|
|
19
|
+
export const CODEX_IDLE_MS = 30 * 60 * 1000;
|
|
20
|
+
export const CODEX_APP_SERVER_TIMEOUT_MS = 20_000;
|
|
21
|
+
export const CODEX_APP_SERVER_BACKOFF_MS = 60 * 60 * 1000;
|
|
22
|
+
export const CODEX_PROVIDER = "codex";
|
|
23
|
+
const ALLOWED_METHODS = new Set(["initialize", "initialized", "account/rateLimits/read"]);
|
|
24
|
+
const LOCAL_ACCOUNT = "codex-local";
|
|
25
|
+
/** Bytes before the cursor offset that identify the file content. */
|
|
26
|
+
const CURSOR_MARK_BYTES = 64;
|
|
27
|
+
export function hashAccountId(raw) {
|
|
28
|
+
const digest = crypto.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 16);
|
|
29
|
+
return `acct_${digest}`;
|
|
30
|
+
}
|
|
31
|
+
export function hashCreditId(raw) {
|
|
32
|
+
const digest = crypto.createHash("sha256").update(`credit\0${raw}`, "utf8").digest("hex").slice(0, 16);
|
|
33
|
+
return `credit_${digest}`;
|
|
34
|
+
}
|
|
35
|
+
export function parseCodexCliVersion(text) {
|
|
36
|
+
const match = /\b(\d+\.\d+\.\d+)\b/.exec(text);
|
|
37
|
+
return match?.[1] ?? null;
|
|
38
|
+
}
|
|
39
|
+
/** Idle at least 30 minutes and no codex process. A missing session write counts as idle. */
|
|
40
|
+
export function shouldReadAppServer(input) {
|
|
41
|
+
if (input.codexProcessRunning)
|
|
42
|
+
return false;
|
|
43
|
+
if (input.lastSessionWriteAt === null)
|
|
44
|
+
return true;
|
|
45
|
+
return input.now - input.lastSessionWriteAt >= CODEX_IDLE_MS;
|
|
46
|
+
}
|
|
47
|
+
/** True when a process listing shows a `codex` executable, not a directory named codex. */
|
|
48
|
+
export function isCodexProcessRunning(psOutput) {
|
|
49
|
+
return psOutput.split("\n").some((line) => /(?:^|[\s/])codex(?:\s|$)/.test(line.trim()));
|
|
50
|
+
}
|
|
51
|
+
export function latestCodexSessionWriteAt(sessionsDir, recentDays) {
|
|
52
|
+
let latest = null;
|
|
53
|
+
for (const file of listSessionFiles(sessionsDir, recentDays)) {
|
|
54
|
+
if (latest === null || file.mtimeMs > latest)
|
|
55
|
+
latest = file.mtimeMs;
|
|
56
|
+
}
|
|
57
|
+
return latest;
|
|
58
|
+
}
|
|
59
|
+
export function defaultCodexSpawn(command, args, env) {
|
|
60
|
+
const child = spawn(command, [...args], { stdio: ["pipe", "pipe", "ignore"], env });
|
|
61
|
+
child.on("error", () => undefined);
|
|
62
|
+
child.stdin?.on("error", () => undefined);
|
|
63
|
+
if (!child.stdin || !child.stdout) {
|
|
64
|
+
child.kill("SIGTERM");
|
|
65
|
+
throw new Error("codex stdio unavailable");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
stdin: child.stdin,
|
|
69
|
+
stdout: child.stdout,
|
|
70
|
+
pid: child.pid,
|
|
71
|
+
kill(signal = "SIGTERM") {
|
|
72
|
+
child.kill(signal);
|
|
73
|
+
},
|
|
74
|
+
once(event, listener) {
|
|
75
|
+
if (event === "exit") {
|
|
76
|
+
child.once("exit", (code, signal) => listener(code, signal));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
child.once("error", (error) => listener(error));
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export async function readInstalledCodexVersion(spawnImpl = defaultCodexSpawn) {
|
|
84
|
+
let child;
|
|
85
|
+
try {
|
|
86
|
+
child = spawnImpl("codex", ["--version"]);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return "unknown";
|
|
90
|
+
}
|
|
91
|
+
const text = await new Promise((resolve) => {
|
|
92
|
+
let buf = "";
|
|
93
|
+
let done = false;
|
|
94
|
+
const timer = setTimeout(finish, 5_000);
|
|
95
|
+
timer.unref();
|
|
96
|
+
function finish() {
|
|
97
|
+
if (done)
|
|
98
|
+
return;
|
|
99
|
+
done = true;
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
resolve(buf);
|
|
102
|
+
}
|
|
103
|
+
child.stdout.on("data", (chunk) => {
|
|
104
|
+
buf += String(chunk);
|
|
105
|
+
});
|
|
106
|
+
child.once?.("error", () => finish());
|
|
107
|
+
child.once?.("exit", () => finish());
|
|
108
|
+
});
|
|
109
|
+
shutdownChild(child);
|
|
110
|
+
return parseCodexCliVersion(text) ?? "unknown";
|
|
111
|
+
}
|
|
112
|
+
export function tailCodexSessions(options) {
|
|
113
|
+
const now = options.now ?? new Date();
|
|
114
|
+
const windowStart = now.getTime() - BURN_WINDOW_MS;
|
|
115
|
+
const recentDays = options.recentDays;
|
|
116
|
+
const previous = options.state?.files ?? {};
|
|
117
|
+
const cursorExists = Object.keys(previous).length > 0;
|
|
118
|
+
const candidates = listSessionFiles(options.sessionsDir, recentDays)
|
|
119
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs || a.file.localeCompare(b.file));
|
|
120
|
+
const next = {};
|
|
121
|
+
const readings = [];
|
|
122
|
+
const failures = [];
|
|
123
|
+
const latest = new Map();
|
|
124
|
+
const emittedAt = new Map();
|
|
125
|
+
let bestEventMs = Number.NEGATIVE_INFINITY;
|
|
126
|
+
const accountId = hashAccountId(options.accountId ?? (options.accountHome ? `home:${path.resolve(options.accountHome)}` : LOCAL_ACCOUNT));
|
|
127
|
+
const consider = (parsed, emit) => {
|
|
128
|
+
if (emit) {
|
|
129
|
+
readings.push(...parsed.readings);
|
|
130
|
+
failures.push(...parsed.failures);
|
|
131
|
+
}
|
|
132
|
+
for (const reading of parsed.readings) {
|
|
133
|
+
const at = Date.parse(reading.observedAt);
|
|
134
|
+
if (Number.isNaN(at))
|
|
135
|
+
continue;
|
|
136
|
+
if (at > bestEventMs)
|
|
137
|
+
bestEventMs = at;
|
|
138
|
+
const prev = latest.get(reading.limit.limitId);
|
|
139
|
+
if (!prev || at >= prev.at)
|
|
140
|
+
latest.set(reading.limit.limitId, { reading, at });
|
|
141
|
+
if (emit) {
|
|
142
|
+
const seen = emittedAt.get(reading.limit.limitId) ?? Number.NEGATIVE_INFINITY;
|
|
143
|
+
if (at >= seen)
|
|
144
|
+
emittedAt.set(reading.limit.limitId, at);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
for (const cand of candidates) {
|
|
149
|
+
const prior = previous[cand.file];
|
|
150
|
+
const sameInode = Boolean(prior && prior.ino === cand.ino && prior.dev === cand.dev && cand.size >= prior.offset);
|
|
151
|
+
const olderThanWindow = cand.mtimeMs < windowStart;
|
|
152
|
+
const latestSettled = bestEventMs > Number.NEGATIVE_INFINITY && bestEventMs >= cand.mtimeMs;
|
|
153
|
+
if (olderThanWindow && (cursorExists || latestSettled)) {
|
|
154
|
+
// Not read this pass, so the cursor is carried forward unverified.
|
|
155
|
+
if (sameInode && prior) {
|
|
156
|
+
next[cand.file] = { ...prior, ino: cand.ino, dev: cand.dev };
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const hasCursor = Boolean(prior && sameInode && cursorMarkMatches(cand.file, prior));
|
|
161
|
+
if (hasCursor && prior && cand.size === prior.offset) {
|
|
162
|
+
next[cand.file] = { ...prior, ino: cand.ino, dev: cand.dev };
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const bootstrap = !hasCursor;
|
|
166
|
+
const chunk = bootstrap
|
|
167
|
+
? readBootstrapSlice(cand.file, cand.size, {
|
|
168
|
+
stopBeforeMs: olderThanWindow ? null : windowStart,
|
|
169
|
+
newestTokenOnly: olderThanWindow,
|
|
170
|
+
})
|
|
171
|
+
: readCompleteLines(cand.file, prior?.offset ?? 0, cand.size);
|
|
172
|
+
next[cand.file] = {
|
|
173
|
+
offset: chunk.nextOffset,
|
|
174
|
+
ino: cand.ino,
|
|
175
|
+
dev: cand.dev,
|
|
176
|
+
mark: cursorMark(cand.file, chunk.nextOffset),
|
|
177
|
+
};
|
|
178
|
+
const mtimeIso = new Date(cand.mtimeMs).toISOString();
|
|
179
|
+
for (const line of chunk.lines) {
|
|
180
|
+
const parsed = parseSessionLine(line.text, {
|
|
181
|
+
source: "codex-jsonl",
|
|
182
|
+
cliVersion: options.cliVersion,
|
|
183
|
+
observedAt: mtimeIso,
|
|
184
|
+
accountId,
|
|
185
|
+
readingSalt: `${cand.file}\0${line.byteOffset}`,
|
|
186
|
+
accountLabel: options.accountLabel,
|
|
187
|
+
accountLabels: options.accountLabels,
|
|
188
|
+
accountHome: options.accountHome,
|
|
189
|
+
});
|
|
190
|
+
if (!parsed)
|
|
191
|
+
continue;
|
|
192
|
+
if (!bootstrap) {
|
|
193
|
+
consider(parsed, true);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const at = resultAt(parsed);
|
|
197
|
+
consider(parsed, at !== null && at >= windowStart);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const [limitId, item] of latest) {
|
|
201
|
+
const emitted = emittedAt.get(limitId);
|
|
202
|
+
if (emitted === undefined || item.at > emitted)
|
|
203
|
+
readings.push(item.reading);
|
|
204
|
+
}
|
|
205
|
+
return { readings, failures, providerMeta: null, state: { files: next } };
|
|
206
|
+
}
|
|
207
|
+
/** Map an account/rateLimits/read result. Does not spawn. */
|
|
208
|
+
export function mapCodexAppServerBody(body, options) {
|
|
209
|
+
const now = options.now ?? new Date();
|
|
210
|
+
const observedAt = now.toISOString();
|
|
211
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
212
|
+
return {
|
|
213
|
+
readings: [],
|
|
214
|
+
failures: [failureEvent({
|
|
215
|
+
observedAt,
|
|
216
|
+
source: "codex-app-server",
|
|
217
|
+
cliVersion: options.cliVersion,
|
|
218
|
+
accountId: hashAccountId(options.accountId ?? LOCAL_ACCOUNT),
|
|
219
|
+
error: "codex app-server rate limit payload drifted",
|
|
220
|
+
})],
|
|
221
|
+
providerMeta: null,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const record = body;
|
|
225
|
+
const rawAccount = typeof record.accountId === "string" && record.accountId.trim()
|
|
226
|
+
? record.accountId
|
|
227
|
+
: options.accountId ?? (options.accountHome ? `home:${path.resolve(options.accountHome)}` : LOCAL_ACCOUNT);
|
|
228
|
+
const accountId = hashAccountId(rawAccount);
|
|
229
|
+
const mapped = mapRateLimitCarrier(record, {
|
|
230
|
+
source: "codex-app-server",
|
|
231
|
+
cliVersion: options.cliVersion,
|
|
232
|
+
observedAt,
|
|
233
|
+
accountId,
|
|
234
|
+
readingSalt: `app-server\0${observedAt}`,
|
|
235
|
+
accountLabel: options.accountLabel,
|
|
236
|
+
accountLabels: options.accountLabels,
|
|
237
|
+
accountHome: options.accountHome,
|
|
238
|
+
}, { missingLimitsAreDrift: true });
|
|
239
|
+
const ordinary = pickBool(record, ["ordinaryUsageAllowed"]);
|
|
240
|
+
const reset = mapResetCredits(record.rateLimitResetCredits ?? record.rate_limit_reset_credits);
|
|
241
|
+
if (ordinary.drifted) {
|
|
242
|
+
mapped.failures.push(failureEvent({
|
|
243
|
+
observedAt,
|
|
244
|
+
source: "codex-app-server",
|
|
245
|
+
cliVersion: options.cliVersion,
|
|
246
|
+
accountId,
|
|
247
|
+
error: "codex field drift: ordinaryUsageAllowed",
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
if (reset.drifted) {
|
|
251
|
+
mapped.failures.push(failureEvent({
|
|
252
|
+
observedAt,
|
|
253
|
+
source: "codex-app-server",
|
|
254
|
+
cliVersion: options.cliVersion,
|
|
255
|
+
accountId,
|
|
256
|
+
error: "codex field drift: rateLimitResetCredits",
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
readings: mapped.readings,
|
|
261
|
+
failures: mapped.failures,
|
|
262
|
+
providerMeta: {
|
|
263
|
+
observedAt,
|
|
264
|
+
source: "codex-app-server",
|
|
265
|
+
cliVersion: options.cliVersion,
|
|
266
|
+
accountId,
|
|
267
|
+
ordinaryUsageAllowed: ordinary.drifted ? null : ordinary.value,
|
|
268
|
+
rateLimitResetCredits: reset.drifted ? null : reset.value,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export async function readCodexAppServer(options) {
|
|
273
|
+
const timeoutMs = options.timeoutMs ?? CODEX_APP_SERVER_TIMEOUT_MS;
|
|
274
|
+
const accountId = options.accountId ?? (options.home ? `home:${path.resolve(options.home)}` : undefined);
|
|
275
|
+
const spawnImpl = options.spawn ?? defaultCodexSpawn;
|
|
276
|
+
const command = options.command ?? "codex";
|
|
277
|
+
let child = null;
|
|
278
|
+
try {
|
|
279
|
+
try {
|
|
280
|
+
const env = options.home ? { ...process.env, CODEX_HOME: options.home } : undefined;
|
|
281
|
+
child = spawnImpl(command, ["app-server", "--stdio"], env);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return transportFailure("spawn-failed", options);
|
|
285
|
+
}
|
|
286
|
+
const rpc = await exchangeRateLimits(child, timeoutMs, options.signal);
|
|
287
|
+
if (!rpc.ok)
|
|
288
|
+
return transportFailure(rpc.reason, options);
|
|
289
|
+
const mapped = mapCodexAppServerBody(rpc.result, {
|
|
290
|
+
cliVersion: options.cliVersion,
|
|
291
|
+
now: options.now,
|
|
292
|
+
accountId,
|
|
293
|
+
accountLabel: options.accountLabel,
|
|
294
|
+
accountLabels: options.accountLabels,
|
|
295
|
+
accountHome: options.accountHome ?? options.home,
|
|
296
|
+
});
|
|
297
|
+
return { ok: true, ...mapped };
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
if (child)
|
|
301
|
+
shutdownChild(child);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
export function ingestCodexResult(result, options) {
|
|
305
|
+
if (result.readings.length === 0 && result.failures.length === 0 && !result.providerMeta)
|
|
306
|
+
return null;
|
|
307
|
+
const meta = result.providerMeta
|
|
308
|
+
? {
|
|
309
|
+
event: "provider_meta",
|
|
310
|
+
observedAt: result.providerMeta.observedAt,
|
|
311
|
+
provider: CODEX_PROVIDER,
|
|
312
|
+
source: result.providerMeta.source,
|
|
313
|
+
ordinaryUsageAllowed: result.providerMeta.ordinaryUsageAllowed,
|
|
314
|
+
rateLimitResetCredits: result.providerMeta.rateLimitResetCredits,
|
|
315
|
+
accountId: result.providerMeta.accountId,
|
|
316
|
+
cliVersion: result.providerMeta.cliVersion,
|
|
317
|
+
parserVersion: CODEX_PARSER_VERSION,
|
|
318
|
+
}
|
|
319
|
+
: null;
|
|
320
|
+
return appendReadings(result.readings, result.failures, meta, options);
|
|
321
|
+
}
|
|
322
|
+
function transportFailure(reason, options) {
|
|
323
|
+
const raw = options?.accountId ?? (options?.home ? `home:${path.resolve(options.home)}` : undefined);
|
|
324
|
+
const failures = [];
|
|
325
|
+
if (raw && options?.cliVersion) {
|
|
326
|
+
failures.push(failureEvent({
|
|
327
|
+
observedAt: (options.now ?? new Date()).toISOString(),
|
|
328
|
+
source: "codex-app-server",
|
|
329
|
+
cliVersion: options.cliVersion,
|
|
330
|
+
accountId: hashAccountId(raw),
|
|
331
|
+
error: `unavailable: ${reason}`,
|
|
332
|
+
accountLabel: options.accountLabel,
|
|
333
|
+
accountLabels: options.accountLabels,
|
|
334
|
+
accountHome: options.accountHome ?? options.home,
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
reason,
|
|
340
|
+
backoffMs: CODEX_APP_SERVER_BACKOFF_MS,
|
|
341
|
+
readings: [],
|
|
342
|
+
failures,
|
|
343
|
+
providerMeta: null,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function exchangeRateLimits(child, timeoutMs, signal) {
|
|
347
|
+
return new Promise((resolve) => {
|
|
348
|
+
let settled = false;
|
|
349
|
+
let buffer = "";
|
|
350
|
+
let pumping = false;
|
|
351
|
+
const finish = (value) => {
|
|
352
|
+
if (settled)
|
|
353
|
+
return;
|
|
354
|
+
settled = true;
|
|
355
|
+
clearTimeout(timer);
|
|
356
|
+
resolve(value);
|
|
357
|
+
};
|
|
358
|
+
const timer = setTimeout(() => finish({ ok: false, reason: "timeout" }), timeoutMs);
|
|
359
|
+
if (signal) {
|
|
360
|
+
if (signal.aborted)
|
|
361
|
+
finish({ ok: false, reason: "timeout" });
|
|
362
|
+
else
|
|
363
|
+
signal.addEventListener("abort", () => finish({ ok: false, reason: "timeout" }), { once: true });
|
|
364
|
+
}
|
|
365
|
+
const handle = (line) => {
|
|
366
|
+
if (settled)
|
|
367
|
+
return;
|
|
368
|
+
let message;
|
|
369
|
+
try {
|
|
370
|
+
const parsed = JSON.parse(line);
|
|
371
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
372
|
+
return;
|
|
373
|
+
message = parsed;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (message.id === 1) {
|
|
379
|
+
if (message.error)
|
|
380
|
+
finish({ ok: false, reason: "rpc-error" });
|
|
381
|
+
else {
|
|
382
|
+
writeRpc(child, { jsonrpc: "2.0", method: "initialized", params: {} });
|
|
383
|
+
writeRpc(child, {
|
|
384
|
+
jsonrpc: "2.0",
|
|
385
|
+
id: 2,
|
|
386
|
+
method: "account/rateLimits/read",
|
|
387
|
+
params: {},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (message.id === 2) {
|
|
393
|
+
if (message.error)
|
|
394
|
+
finish({ ok: false, reason: "rpc-error" });
|
|
395
|
+
else
|
|
396
|
+
finish({ ok: true, result: message.result });
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const pump = () => {
|
|
400
|
+
if (pumping)
|
|
401
|
+
return;
|
|
402
|
+
pumping = true;
|
|
403
|
+
try {
|
|
404
|
+
while (!settled) {
|
|
405
|
+
const nl = buffer.indexOf("\n");
|
|
406
|
+
if (nl < 0)
|
|
407
|
+
break;
|
|
408
|
+
const line = buffer.slice(0, nl).trim();
|
|
409
|
+
buffer = buffer.slice(nl + 1);
|
|
410
|
+
if (line)
|
|
411
|
+
handle(line);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
pumping = false;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
child.stdout.on("data", (chunk) => {
|
|
419
|
+
buffer += String(chunk);
|
|
420
|
+
pump();
|
|
421
|
+
});
|
|
422
|
+
child.once?.("error", () => finish({ ok: false, reason: "spawn-failed" }));
|
|
423
|
+
child.once?.("exit", () => finish({ ok: false, reason: "exited" }));
|
|
424
|
+
try {
|
|
425
|
+
writeRpc(child, {
|
|
426
|
+
jsonrpc: "2.0",
|
|
427
|
+
id: 1,
|
|
428
|
+
method: "initialize",
|
|
429
|
+
params: {
|
|
430
|
+
clientInfo: { name: "hq-capacity", title: "HQ capacity", version: "1" },
|
|
431
|
+
capabilities: { experimentalApi: true },
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
finish({ ok: false, reason: "spawn-failed" });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function writeRpc(child, message) {
|
|
441
|
+
const method = message.method;
|
|
442
|
+
if (typeof method !== "string" || !ALLOWED_METHODS.has(method)) {
|
|
443
|
+
throw new Error("codex app-server method is not a rate-limit read");
|
|
444
|
+
}
|
|
445
|
+
child.stdin.on?.("error", () => undefined);
|
|
446
|
+
try {
|
|
447
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
// EPIPE is reported on the error event. A closed pipe is not a daemon crash.
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function shutdownChild(child) {
|
|
454
|
+
try {
|
|
455
|
+
child.stdin.end();
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// The child may already have closed stdin.
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
child.kill("SIGTERM");
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// The child may already have exited.
|
|
465
|
+
}
|
|
466
|
+
if (typeof child.pid !== "number")
|
|
467
|
+
return;
|
|
468
|
+
const timer = setTimeout(() => {
|
|
469
|
+
try {
|
|
470
|
+
child.kill("SIGKILL");
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// Already gone.
|
|
474
|
+
}
|
|
475
|
+
}, 500);
|
|
476
|
+
timer.unref();
|
|
477
|
+
}
|
|
478
|
+
const TAIL_CHUNK_BYTES = 64 * 1024;
|
|
479
|
+
const TIMESTAMP_PREFIX = /^\{"timestamp":"(\d{4}-\d{2}-\d{2}T[^"]+)"/;
|
|
480
|
+
function listSessionFiles(dir, recentDays) {
|
|
481
|
+
if (!dir || !fs.existsSync(dir))
|
|
482
|
+
return [];
|
|
483
|
+
const out = [];
|
|
484
|
+
const stack = [dir];
|
|
485
|
+
while (stack.length > 0) {
|
|
486
|
+
const current = stack.pop();
|
|
487
|
+
if (!current)
|
|
488
|
+
continue;
|
|
489
|
+
let entries;
|
|
490
|
+
try {
|
|
491
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
for (const entry of entries) {
|
|
497
|
+
if (entry.isSymbolicLink())
|
|
498
|
+
continue;
|
|
499
|
+
const full = path.join(current, entry.name);
|
|
500
|
+
if (entry.isDirectory()) {
|
|
501
|
+
if (recentDays !== undefined && skipOldSessionDir(entry.name, full, recentDays))
|
|
502
|
+
continue;
|
|
503
|
+
stack.push(full);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
|
|
507
|
+
continue;
|
|
508
|
+
try {
|
|
509
|
+
const stat = fs.statSync(full);
|
|
510
|
+
out.push({ file: full, mtimeMs: stat.mtimeMs, size: stat.size, ino: stat.ino, dev: stat.dev });
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
// The file disappeared between readdir and stat.
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
function skipOldSessionDir(name, full, recentDays) {
|
|
520
|
+
if (/^\d{4}$/.test(name) && Number(name) < new Date().getUTCFullYear() - 1)
|
|
521
|
+
return true;
|
|
522
|
+
const match = /[/\\](\d{4})[/\\](\d{2})[/\\](\d{2})$/.exec(full);
|
|
523
|
+
if (!match)
|
|
524
|
+
return false;
|
|
525
|
+
const day = Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
526
|
+
const today = new Date();
|
|
527
|
+
const start = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
|
|
528
|
+
return day < start - (recentDays - 1) * 24 * 60 * 60 * 1000;
|
|
529
|
+
}
|
|
530
|
+
function resultAt(parsed) {
|
|
531
|
+
const stamp = parsed.readings[0]?.observedAt ?? parsed.failures[0]?.observedAt;
|
|
532
|
+
if (!stamp)
|
|
533
|
+
return null;
|
|
534
|
+
const at = Date.parse(stamp);
|
|
535
|
+
return Number.isNaN(at) ? null : at;
|
|
536
|
+
}
|
|
537
|
+
function lineFacts(text) {
|
|
538
|
+
const token = text.includes('"type":"token_count"');
|
|
539
|
+
const match = TIMESTAMP_PREFIX.exec(text);
|
|
540
|
+
if (!match?.[1])
|
|
541
|
+
return { ts: null, token };
|
|
542
|
+
const ts = Date.parse(match[1]);
|
|
543
|
+
return { ts: Number.isNaN(ts) ? null : ts, token };
|
|
544
|
+
}
|
|
545
|
+
/** Byte offset just past the last complete line, or 0 when the file has no newline. */
|
|
546
|
+
function completeEndOffset(fd, size) {
|
|
547
|
+
if (size <= 0)
|
|
548
|
+
return 0;
|
|
549
|
+
const last = Buffer.alloc(1);
|
|
550
|
+
if (fs.readSync(fd, last, 0, 1, size - 1) !== 1)
|
|
551
|
+
return 0;
|
|
552
|
+
if (last[0] === 0x0a)
|
|
553
|
+
return size;
|
|
554
|
+
let pos = size - 1;
|
|
555
|
+
while (pos > 0) {
|
|
556
|
+
const start = Math.max(0, pos - TAIL_CHUNK_BYTES);
|
|
557
|
+
const buf = Buffer.alloc(pos - start);
|
|
558
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
559
|
+
const nl = buf.lastIndexOf(0x0a);
|
|
560
|
+
if (nl >= 0)
|
|
561
|
+
return start + nl + 1;
|
|
562
|
+
if (start === 0)
|
|
563
|
+
return 0;
|
|
564
|
+
pos = start;
|
|
565
|
+
}
|
|
566
|
+
return 0;
|
|
567
|
+
}
|
|
568
|
+
function splitCompleteLines(region, regionOffset) {
|
|
569
|
+
const lines = [];
|
|
570
|
+
let rel = 0;
|
|
571
|
+
for (let i = 0; i < region.length; i += 1) {
|
|
572
|
+
if (region[i] !== 0x0a)
|
|
573
|
+
continue;
|
|
574
|
+
lines.push({
|
|
575
|
+
text: region.subarray(rel, i).toString("utf8"),
|
|
576
|
+
byteOffset: regionOffset + rel,
|
|
577
|
+
});
|
|
578
|
+
rel = i + 1;
|
|
579
|
+
}
|
|
580
|
+
return lines;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Start offset of the oldest line a first-run read must see: events inside
|
|
584
|
+
* the burn-rate window, or the newest token_count when the file is older.
|
|
585
|
+
* The cursor still advances to EOF; this only limits how much history is parsed.
|
|
586
|
+
*/
|
|
587
|
+
function oldestKeptOffset(fd, end, mode) {
|
|
588
|
+
let pos = end;
|
|
589
|
+
let span = TAIL_CHUNK_BYTES;
|
|
590
|
+
let kept = end;
|
|
591
|
+
let sawToken = false;
|
|
592
|
+
while (pos > 0) {
|
|
593
|
+
const readStart = Math.max(0, pos - span);
|
|
594
|
+
const buf = Buffer.alloc(pos - readStart);
|
|
595
|
+
fs.readSync(fd, buf, 0, buf.length, readStart);
|
|
596
|
+
const nl = readStart === 0 ? -1 : buf.indexOf(0x0a);
|
|
597
|
+
if (readStart > 0 && nl < 0) {
|
|
598
|
+
const grown = Math.min(span * 2, pos);
|
|
599
|
+
if (grown === span)
|
|
600
|
+
break;
|
|
601
|
+
span = grown;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
span = TAIL_CHUNK_BYTES;
|
|
605
|
+
const lineStart = readStart === 0 ? 0 : nl + 1;
|
|
606
|
+
const regionOffset = readStart + lineStart;
|
|
607
|
+
const lines = splitCompleteLines(buf.subarray(lineStart), regionOffset);
|
|
608
|
+
let stop = false;
|
|
609
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
610
|
+
const line = lines[i];
|
|
611
|
+
if (!line || !line.text.trim())
|
|
612
|
+
continue;
|
|
613
|
+
const facts = lineFacts(line.text);
|
|
614
|
+
if (mode.newestTokenOnly) {
|
|
615
|
+
if (!facts.token)
|
|
616
|
+
continue;
|
|
617
|
+
kept = line.byteOffset;
|
|
618
|
+
stop = true;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
const older = mode.stopBeforeMs !== null && facts.ts !== null && facts.ts < mode.stopBeforeMs;
|
|
622
|
+
if (older && (sawToken || facts.token)) {
|
|
623
|
+
if (facts.token)
|
|
624
|
+
kept = line.byteOffset;
|
|
625
|
+
stop = true;
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
if (older)
|
|
629
|
+
continue;
|
|
630
|
+
kept = line.byteOffset;
|
|
631
|
+
if (facts.token)
|
|
632
|
+
sawToken = true;
|
|
633
|
+
}
|
|
634
|
+
if (stop || readStart === 0)
|
|
635
|
+
break;
|
|
636
|
+
if (regionOffset >= pos)
|
|
637
|
+
break;
|
|
638
|
+
pos = regionOffset;
|
|
639
|
+
}
|
|
640
|
+
return kept;
|
|
641
|
+
}
|
|
642
|
+
function locateBootstrapSlice(file, size, mode) {
|
|
643
|
+
const fd = fs.openSync(file, "r");
|
|
644
|
+
try {
|
|
645
|
+
const nextOffset = completeEndOffset(fd, size);
|
|
646
|
+
if (nextOffset <= 0)
|
|
647
|
+
return null;
|
|
648
|
+
return { start: oldestKeptOffset(fd, nextOffset, mode), nextOffset };
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
fs.closeSync(fd);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function readBootstrapSlice(file, size, mode) {
|
|
655
|
+
if (size <= 0)
|
|
656
|
+
return { lines: [], nextOffset: 0 };
|
|
657
|
+
const located = locateBootstrapSlice(file, size, mode);
|
|
658
|
+
if (!located || located.start >= located.nextOffset) {
|
|
659
|
+
return { lines: [], nextOffset: located?.nextOffset ?? 0 };
|
|
660
|
+
}
|
|
661
|
+
const chunk = readCompleteLines(file, located.start, size);
|
|
662
|
+
return { lines: chunk.lines, nextOffset: chunk.nextOffset };
|
|
663
|
+
}
|
|
664
|
+
/** Hash of the bytes before `offset`, or undefined when they cannot be read. */
|
|
665
|
+
function cursorMark(file, offset) {
|
|
666
|
+
const start = Math.max(0, offset - CURSOR_MARK_BYTES);
|
|
667
|
+
const buf = Buffer.alloc(offset - start);
|
|
668
|
+
try {
|
|
669
|
+
const fd = fs.openSync(file, "r");
|
|
670
|
+
try {
|
|
671
|
+
if (fs.readSync(fd, buf, 0, buf.length, start) !== buf.length)
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
fs.closeSync(fd);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
return crypto.createHash("sha256").update(buf).digest("hex").slice(0, 16);
|
|
682
|
+
}
|
|
683
|
+
function cursorMarkMatches(file, cursor) {
|
|
684
|
+
if (cursor.mark === undefined)
|
|
685
|
+
return true;
|
|
686
|
+
return cursorMark(file, cursor.offset) === cursor.mark;
|
|
687
|
+
}
|
|
688
|
+
function readCompleteLines(file, offset, size) {
|
|
689
|
+
if (size < offset)
|
|
690
|
+
offset = 0;
|
|
691
|
+
if (size === offset)
|
|
692
|
+
return { lines: [], nextOffset: offset };
|
|
693
|
+
const length = size - offset;
|
|
694
|
+
const buf = Buffer.alloc(length);
|
|
695
|
+
const fd = fs.openSync(file, "r");
|
|
696
|
+
try {
|
|
697
|
+
fs.readSync(fd, buf, 0, length, offset);
|
|
698
|
+
}
|
|
699
|
+
finally {
|
|
700
|
+
fs.closeSync(fd);
|
|
701
|
+
}
|
|
702
|
+
let end = buf.length;
|
|
703
|
+
if (buf[buf.length - 1] !== 0x0a) {
|
|
704
|
+
const nl = buf.lastIndexOf(0x0a);
|
|
705
|
+
if (nl < 0)
|
|
706
|
+
return { lines: [], nextOffset: offset };
|
|
707
|
+
end = nl + 1;
|
|
708
|
+
}
|
|
709
|
+
const lines = [];
|
|
710
|
+
let start = 0;
|
|
711
|
+
for (let i = 0; i < end; i += 1) {
|
|
712
|
+
if (buf[i] !== 0x0a)
|
|
713
|
+
continue;
|
|
714
|
+
lines.push({
|
|
715
|
+
text: buf.subarray(start, i).toString("utf8"),
|
|
716
|
+
byteOffset: offset + start,
|
|
717
|
+
});
|
|
718
|
+
start = i + 1;
|
|
719
|
+
}
|
|
720
|
+
return { lines, nextOffset: offset + end };
|
|
721
|
+
}
|
|
722
|
+
function parseSessionLine(text, ctx) {
|
|
723
|
+
const trimmed = text.trim();
|
|
724
|
+
if (!trimmed || !trimmed.includes('"type":"token_count"'))
|
|
725
|
+
return null;
|
|
726
|
+
let value;
|
|
727
|
+
try {
|
|
728
|
+
value = JSON.parse(trimmed);
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
734
|
+
return null;
|
|
735
|
+
const record = value;
|
|
736
|
+
const payload = record.payload && typeof record.payload === "object" && !Array.isArray(record.payload)
|
|
737
|
+
? record.payload
|
|
738
|
+
: record;
|
|
739
|
+
const type = payload.type ?? record.type;
|
|
740
|
+
if (type !== "token_count")
|
|
741
|
+
return null;
|
|
742
|
+
const timed = eventTimeIso(record);
|
|
743
|
+
const observedAt = timed ?? ctx.observedAt;
|
|
744
|
+
const next = { ...ctx, observedAt };
|
|
745
|
+
if (!timed)
|
|
746
|
+
next.confidence = "inferred";
|
|
747
|
+
return mapRateLimitCarrier(payload, next, { missingLimitsAreDrift: false });
|
|
748
|
+
}
|
|
749
|
+
function mapRateLimitCarrier(payload, ctx, mode) {
|
|
750
|
+
const readings = [];
|
|
751
|
+
const failures = [];
|
|
752
|
+
const byId = payload.rateLimitsByLimitId ?? payload.rate_limits_by_limit_id;
|
|
753
|
+
const single = payload.rateLimits ?? payload.rate_limits;
|
|
754
|
+
if (byId == null && single == null) {
|
|
755
|
+
if (mode.missingLimitsAreDrift) {
|
|
756
|
+
failures.push(failureEvent({
|
|
757
|
+
...ctx,
|
|
758
|
+
error: "codex field drift: rateLimits missing",
|
|
759
|
+
}));
|
|
760
|
+
}
|
|
761
|
+
return { readings, failures, providerMeta: null };
|
|
762
|
+
}
|
|
763
|
+
const snapshots = new Map();
|
|
764
|
+
if (byId != null) {
|
|
765
|
+
if (typeof byId !== "object" || Array.isArray(byId)) {
|
|
766
|
+
failures.push(failureEvent({ ...ctx, error: "codex field drift: rateLimitsByLimitId" }));
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
for (const [key, value] of Object.entries(byId)) {
|
|
770
|
+
const snap = asSnapshot(value, key);
|
|
771
|
+
if (!snap) {
|
|
772
|
+
failures.push(failureEvent({
|
|
773
|
+
...ctx,
|
|
774
|
+
limitId: key,
|
|
775
|
+
error: `codex field drift: rate limit ${key}`,
|
|
776
|
+
}));
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
snapshots.set(snap.limitId, snap);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (single != null && (typeof single !== "object" || Array.isArray(single))) {
|
|
784
|
+
failures.push(failureEvent({ ...ctx, error: "codex field drift: rate_limits" }));
|
|
785
|
+
}
|
|
786
|
+
else if (single != null) {
|
|
787
|
+
const snap = asSnapshot(single, "codex");
|
|
788
|
+
if (!snap) {
|
|
789
|
+
failures.push(failureEvent({ ...ctx, error: "codex field drift: rate_limits" }));
|
|
790
|
+
}
|
|
791
|
+
else if (!snapshots.has(snap.limitId)) {
|
|
792
|
+
snapshots.set(snap.limitId, snap);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
for (const snap of snapshots.values()) {
|
|
796
|
+
emitSnapshot(snap, ctx, readings, failures);
|
|
797
|
+
}
|
|
798
|
+
return { readings, failures, providerMeta: null };
|
|
799
|
+
}
|
|
800
|
+
function emitSnapshot(snap, ctx, readings, failures) {
|
|
801
|
+
if (snap.reached.drifted) {
|
|
802
|
+
failures.push(failureEvent({
|
|
803
|
+
...ctx,
|
|
804
|
+
limitId: snap.limitId,
|
|
805
|
+
error: `codex field drift: rate_limit_reached_type on ${snap.limitId}`,
|
|
806
|
+
}));
|
|
807
|
+
}
|
|
808
|
+
if (snap.spend.drifted) {
|
|
809
|
+
failures.push(failureEvent({
|
|
810
|
+
...ctx,
|
|
811
|
+
limitId: snap.limitId,
|
|
812
|
+
error: `codex field drift: spend_control_reached on ${snap.limitId}`,
|
|
813
|
+
}));
|
|
814
|
+
}
|
|
815
|
+
for (const which of ["primary", "secondary"]) {
|
|
816
|
+
const window = which === "primary" ? snap.primary : snap.secondary;
|
|
817
|
+
const emitted = emitWindow(snap, which, window, ctx);
|
|
818
|
+
if (!emitted)
|
|
819
|
+
continue;
|
|
820
|
+
if ("event" in emitted)
|
|
821
|
+
failures.push(emitted);
|
|
822
|
+
else
|
|
823
|
+
readings.push(emitted);
|
|
824
|
+
}
|
|
825
|
+
const credits = emitCredits(snap, ctx);
|
|
826
|
+
if (credits) {
|
|
827
|
+
if ("event" in credits)
|
|
828
|
+
failures.push(credits);
|
|
829
|
+
else
|
|
830
|
+
readings.push(credits);
|
|
831
|
+
}
|
|
832
|
+
const individual = emitIndividual(snap, ctx);
|
|
833
|
+
if (individual) {
|
|
834
|
+
if ("event" in individual)
|
|
835
|
+
failures.push(individual);
|
|
836
|
+
else
|
|
837
|
+
readings.push(individual);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function emitWindow(snap, which, window, ctx) {
|
|
841
|
+
if (window == null)
|
|
842
|
+
return null;
|
|
843
|
+
const limitId = `${snap.limitId}:${which}`;
|
|
844
|
+
if (typeof window !== "object" || Array.isArray(window)) {
|
|
845
|
+
return failureEvent({
|
|
846
|
+
...ctx,
|
|
847
|
+
limitId,
|
|
848
|
+
error: `codex field drift: ${limitId} shape`,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
const record = window;
|
|
852
|
+
const used = pickNumber(record, ["usedPercent", "used_percent"]);
|
|
853
|
+
const resets = pickNumber(record, ["resetsAt", "resets_at"]);
|
|
854
|
+
const minutes = pickNumber(record, ["windowDurationMins", "window_minutes", "windowMinutes"]);
|
|
855
|
+
const missing = [];
|
|
856
|
+
if (used.value === undefined || used.presentNull)
|
|
857
|
+
missing.push("usedPercent");
|
|
858
|
+
if (resets.value === undefined || resets.presentNull)
|
|
859
|
+
missing.push("resetsAt");
|
|
860
|
+
if (missing.length > 0) {
|
|
861
|
+
return failureEvent({
|
|
862
|
+
...ctx,
|
|
863
|
+
limitId,
|
|
864
|
+
error: `codex field drift: missing ${missing.join(" and ")} on ${limitId}`,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
return makeReading(ctx, snap, {
|
|
868
|
+
limitId,
|
|
869
|
+
kind: kindForMinutes(minutes.value ?? null),
|
|
870
|
+
scope: scopeFor(snap),
|
|
871
|
+
usedPercent: used.value ?? null,
|
|
872
|
+
windowMinutes: minutes.value ?? null,
|
|
873
|
+
resetsAt: resets.value ?? null,
|
|
874
|
+
balance: null,
|
|
875
|
+
planType: snap.planType,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
function emitCredits(snap, ctx) {
|
|
879
|
+
if (snap.credits == null)
|
|
880
|
+
return null;
|
|
881
|
+
const limitId = `${snap.limitId}:credits`;
|
|
882
|
+
if (typeof snap.credits !== "object" || Array.isArray(snap.credits)) {
|
|
883
|
+
return failureEvent({ ...ctx, limitId, error: `codex field drift: ${limitId} shape` });
|
|
884
|
+
}
|
|
885
|
+
const record = snap.credits;
|
|
886
|
+
const hasCredits = pickBool(record, ["hasCredits", "has_credits"]);
|
|
887
|
+
const unlimited = pickBool(record, ["unlimited"]);
|
|
888
|
+
const balance = pickBalance(record);
|
|
889
|
+
if (!hasCredits.set && !unlimited.set && !balance.set) {
|
|
890
|
+
return failureEvent({ ...ctx, limitId, error: `codex field drift: ${limitId}` });
|
|
891
|
+
}
|
|
892
|
+
return makeReading(ctx, snap, {
|
|
893
|
+
limitId,
|
|
894
|
+
kind: "credits",
|
|
895
|
+
scope: scopeFor(snap),
|
|
896
|
+
usedPercent: null,
|
|
897
|
+
windowMinutes: null,
|
|
898
|
+
resetsAt: null,
|
|
899
|
+
balance: balance.value,
|
|
900
|
+
planType: snap.planType,
|
|
901
|
+
hasCredits: hasCredits.value,
|
|
902
|
+
unlimited: unlimited.value,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
function emitIndividual(snap, ctx) {
|
|
906
|
+
if (snap.individual == null)
|
|
907
|
+
return null;
|
|
908
|
+
const limitId = `${snap.limitId}:individual`;
|
|
909
|
+
if (typeof snap.individual !== "object" || Array.isArray(snap.individual)) {
|
|
910
|
+
return failureEvent({ ...ctx, limitId, error: `codex field drift: ${limitId} shape` });
|
|
911
|
+
}
|
|
912
|
+
const record = snap.individual;
|
|
913
|
+
const used = pickNumber(record, ["used", "usedPercent", "used_percent"]);
|
|
914
|
+
const limit = pickNumber(record, ["limit"]);
|
|
915
|
+
const resets = pickNumber(record, ["resetsAt", "resets_at"]);
|
|
916
|
+
if (used.value === undefined && limit.value === undefined && resets.value === undefined) {
|
|
917
|
+
return failureEvent({ ...ctx, limitId, error: `codex field drift: ${limitId}` });
|
|
918
|
+
}
|
|
919
|
+
let usedPercent = null;
|
|
920
|
+
if (typeof used.value === "number" && typeof limit.value === "number" && limit.value > 0) {
|
|
921
|
+
usedPercent = (used.value / limit.value) * 100;
|
|
922
|
+
}
|
|
923
|
+
else if (typeof used.value === "number") {
|
|
924
|
+
usedPercent = used.value;
|
|
925
|
+
}
|
|
926
|
+
return makeReading(ctx, snap, {
|
|
927
|
+
limitId,
|
|
928
|
+
kind: "unknown",
|
|
929
|
+
scope: scopeFor(snap),
|
|
930
|
+
usedPercent,
|
|
931
|
+
windowMinutes: null,
|
|
932
|
+
resetsAt: resets.value ?? null,
|
|
933
|
+
balance: limit.value ?? null,
|
|
934
|
+
planType: snap.planType,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
function makeReading(ctx, snap, limit) {
|
|
938
|
+
const reading = {
|
|
939
|
+
readingId: makeReadingId([ctx.source, ctx.readingSalt, ctx.accountId, limit.limitId, ctx.observedAt]),
|
|
940
|
+
observedAt: ctx.observedAt,
|
|
941
|
+
provider: CODEX_PROVIDER,
|
|
942
|
+
accountId: ctx.accountId,
|
|
943
|
+
source: ctx.source,
|
|
944
|
+
cliVersion: ctx.cliVersion,
|
|
945
|
+
confidence: ctx.confidence ?? confidenceFor(ctx.source),
|
|
946
|
+
parserVersion: CODEX_PARSER_VERSION,
|
|
947
|
+
limit,
|
|
948
|
+
};
|
|
949
|
+
if (snap.reached.set && !snap.reached.drifted)
|
|
950
|
+
reading.rateLimitReachedType = snap.reached.value;
|
|
951
|
+
if (snap.spend.set && !snap.spend.drifted)
|
|
952
|
+
reading.spendControlReached = snap.spend.value;
|
|
953
|
+
if (ctx.accountLabel) {
|
|
954
|
+
reading.accountLabel = ctx.accountLabel;
|
|
955
|
+
reading.accountLabels = ctx.accountLabels ? [...ctx.accountLabels] : [ctx.accountLabel];
|
|
956
|
+
if (ctx.accountHome)
|
|
957
|
+
reading.accountHome = ctx.accountHome;
|
|
958
|
+
}
|
|
959
|
+
return reading;
|
|
960
|
+
}
|
|
961
|
+
function asSnapshot(value, fallbackId) {
|
|
962
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
963
|
+
return null;
|
|
964
|
+
const record = value;
|
|
965
|
+
const limitId = stringField(record, ["limitId", "limit_id"]) ?? fallbackId;
|
|
966
|
+
if (!limitId.trim())
|
|
967
|
+
return null;
|
|
968
|
+
return {
|
|
969
|
+
limitId,
|
|
970
|
+
limitName: stringField(record, ["limitName", "limit_name"]),
|
|
971
|
+
planType: stringField(record, ["planType", "plan_type"]),
|
|
972
|
+
reached: pickString(record, ["rateLimitReachedType", "rate_limit_reached_type"]),
|
|
973
|
+
spend: pickBool(record, ["spendControlReached", "spend_control_reached"]),
|
|
974
|
+
primary: record.primary,
|
|
975
|
+
secondary: record.secondary,
|
|
976
|
+
credits: record.credits,
|
|
977
|
+
individual: record.individualLimit ?? record.individual_limit,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function mapResetCredits(value) {
|
|
981
|
+
if (value == null)
|
|
982
|
+
return { value: null, drifted: false };
|
|
983
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
984
|
+
return { value: null, drifted: true };
|
|
985
|
+
const record = value;
|
|
986
|
+
const available = pickNumber(record, ["availableCount"]);
|
|
987
|
+
if (available.presentNull)
|
|
988
|
+
return { value: null, drifted: true };
|
|
989
|
+
const rows = record.credits;
|
|
990
|
+
const credits = [];
|
|
991
|
+
if (rows != null && !Array.isArray(rows))
|
|
992
|
+
return { value: null, drifted: true };
|
|
993
|
+
if (Array.isArray(rows)) {
|
|
994
|
+
for (const row of rows) {
|
|
995
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
996
|
+
continue;
|
|
997
|
+
const item = row;
|
|
998
|
+
const rawId = typeof item.id === "string" && item.id.trim() ? item.id : "missing";
|
|
999
|
+
credits.push({
|
|
1000
|
+
id: hashCreditId(rawId),
|
|
1001
|
+
resetType: stringField(item, ["resetType"]),
|
|
1002
|
+
status: stringField(item, ["status"]),
|
|
1003
|
+
grantedAt: pickNumber(item, ["grantedAt"]).value ?? null,
|
|
1004
|
+
expiresAt: pickNumber(item, ["expiresAt"]).value ?? null,
|
|
1005
|
+
title: redactText(stringField(item, ["title"])),
|
|
1006
|
+
description: redactText(stringField(item, ["description"])),
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
value: { availableCount: available.value ?? null, credits },
|
|
1012
|
+
drifted: false,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function failureEvent(input) {
|
|
1016
|
+
const event = {
|
|
1017
|
+
event: "parser_failure",
|
|
1018
|
+
observedAt: input.observedAt,
|
|
1019
|
+
provider: CODEX_PROVIDER,
|
|
1020
|
+
source: input.source,
|
|
1021
|
+
error: input.error,
|
|
1022
|
+
accountId: input.accountId,
|
|
1023
|
+
cliVersion: input.cliVersion,
|
|
1024
|
+
parserVersion: CODEX_PARSER_VERSION,
|
|
1025
|
+
};
|
|
1026
|
+
if (input.limitId)
|
|
1027
|
+
event.limitId = input.limitId;
|
|
1028
|
+
if (input.accountLabel) {
|
|
1029
|
+
event.accountLabel = input.accountLabel;
|
|
1030
|
+
event.accountLabels = input.accountLabels ? [...input.accountLabels] : [input.accountLabel];
|
|
1031
|
+
if (input.accountHome)
|
|
1032
|
+
event.accountHome = input.accountHome;
|
|
1033
|
+
}
|
|
1034
|
+
return event;
|
|
1035
|
+
}
|
|
1036
|
+
function eventTimeIso(record) {
|
|
1037
|
+
const ts = record.timestamp;
|
|
1038
|
+
let ms = null;
|
|
1039
|
+
if (typeof ts === "string") {
|
|
1040
|
+
const parsed = Date.parse(ts);
|
|
1041
|
+
ms = Number.isNaN(parsed) ? null : parsed;
|
|
1042
|
+
}
|
|
1043
|
+
else if (typeof ts === "number" && Number.isFinite(ts)) {
|
|
1044
|
+
ms = Math.abs(ts) < 1e12 ? ts * 1000 : ts;
|
|
1045
|
+
}
|
|
1046
|
+
if (ms === null)
|
|
1047
|
+
return null;
|
|
1048
|
+
const date = new Date(ms);
|
|
1049
|
+
if (Number.isNaN(date.getTime()))
|
|
1050
|
+
return null;
|
|
1051
|
+
return date.toISOString();
|
|
1052
|
+
}
|
|
1053
|
+
function kindForMinutes(mins) {
|
|
1054
|
+
if (mins === null)
|
|
1055
|
+
return "unknown";
|
|
1056
|
+
if (mins <= 24 * 60)
|
|
1057
|
+
return "session";
|
|
1058
|
+
if (mins <= 10 * 24 * 60)
|
|
1059
|
+
return "weekly_all";
|
|
1060
|
+
return "unknown";
|
|
1061
|
+
}
|
|
1062
|
+
function scopeFor(snap) {
|
|
1063
|
+
return snap.limitName;
|
|
1064
|
+
}
|
|
1065
|
+
function confidenceFor(source) {
|
|
1066
|
+
return source === "codex-jsonl" ? "observed" : "fetched";
|
|
1067
|
+
}
|
|
1068
|
+
function makeReadingId(parts) {
|
|
1069
|
+
const digest = crypto.createHash("sha256").update(parts.join("\0"), "utf8").digest("hex").slice(0, 20);
|
|
1070
|
+
return `rdg_${digest}`;
|
|
1071
|
+
}
|
|
1072
|
+
function stringField(record, keys) {
|
|
1073
|
+
for (const key of keys) {
|
|
1074
|
+
if (!(key in record))
|
|
1075
|
+
continue;
|
|
1076
|
+
const value = record[key];
|
|
1077
|
+
if (typeof value === "string" && value.trim())
|
|
1078
|
+
return value;
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
function pickNumber(record, keys) {
|
|
1084
|
+
for (const key of keys) {
|
|
1085
|
+
if (!(key in record))
|
|
1086
|
+
continue;
|
|
1087
|
+
const value = record[key];
|
|
1088
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1089
|
+
return { value, presentNull: false };
|
|
1090
|
+
return { value: undefined, presentNull: true };
|
|
1091
|
+
}
|
|
1092
|
+
return { value: undefined, presentNull: false };
|
|
1093
|
+
}
|
|
1094
|
+
function pickBool(record, keys) {
|
|
1095
|
+
for (const key of keys) {
|
|
1096
|
+
if (!(key in record))
|
|
1097
|
+
continue;
|
|
1098
|
+
const value = record[key];
|
|
1099
|
+
if (typeof value === "boolean")
|
|
1100
|
+
return { set: true, value, drifted: false };
|
|
1101
|
+
if (value === null)
|
|
1102
|
+
return { set: true, value: null, drifted: false };
|
|
1103
|
+
return { set: true, value: null, drifted: true };
|
|
1104
|
+
}
|
|
1105
|
+
return { set: false, value: null, drifted: false };
|
|
1106
|
+
}
|
|
1107
|
+
function pickString(record, keys) {
|
|
1108
|
+
for (const key of keys) {
|
|
1109
|
+
if (!(key in record))
|
|
1110
|
+
continue;
|
|
1111
|
+
const value = record[key];
|
|
1112
|
+
if (typeof value === "string")
|
|
1113
|
+
return { set: true, value, drifted: false };
|
|
1114
|
+
if (value === null)
|
|
1115
|
+
return { set: true, value: null, drifted: false };
|
|
1116
|
+
return { set: true, value: null, drifted: true };
|
|
1117
|
+
}
|
|
1118
|
+
return { set: false, value: null, drifted: false };
|
|
1119
|
+
}
|
|
1120
|
+
function pickBalance(record) {
|
|
1121
|
+
if (!("balance" in record))
|
|
1122
|
+
return { set: false, value: null };
|
|
1123
|
+
const value = record.balance;
|
|
1124
|
+
if (typeof value === "string" || (typeof value === "number" && Number.isFinite(value))) {
|
|
1125
|
+
return { set: true, value };
|
|
1126
|
+
}
|
|
1127
|
+
return { set: true, value: null };
|
|
1128
|
+
}
|
|
1129
|
+
function redactText(value) {
|
|
1130
|
+
if (value === null)
|
|
1131
|
+
return null;
|
|
1132
|
+
return redactProviderTokens(value);
|
|
1133
|
+
}
|
|
1134
|
+
//# sourceMappingURL=codex.js.map
|