agent-dag 1.47.0 → 3.0.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/README.md +6 -2
- package/dist/web/assets/index-6cAJDiBs.js +125 -0
- package/dist/web/assets/index-Bnn7d7u8.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/src/server/agent-activity.mjs +506 -0
- package/src/server/browser-history.mjs +467 -0
- package/src/server/browser-presence.mjs +157 -0
- package/src/server/browser-profiles.mjs +267 -0
- package/src/server/browser-react.mjs +204 -0
- package/src/server/browser-watch-store.mjs +288 -0
- package/src/server/browser-watch.mjs +717 -0
- package/src/server/claude-accounts.mjs +77 -7
- package/src/server/codex-usage.mjs +79 -1
- package/src/server/cswap-admin.mjs +485 -36
- package/src/server/index.mjs +172 -4
- package/src/server/installer.mjs +13 -0
- package/src/server/relay-guard.mjs +502 -0
- package/src/server/system-metrics.mjs +715 -32
- package/dist/web/assets/index-BgcITzCV.css +0 -1
- package/dist/web/assets/index-ChVeFKhn.js +0 -101
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
17
17
|
import { cswapBin, cswapVersion, installHint } from "./cswap-install.mjs";
|
|
18
18
|
import { run, runDetached } from "./exec.mjs";
|
|
19
|
+
// The CLI identity oracle, already written and already trusted by the account
|
|
20
|
+
// admin routes. #721 needs the same answer, so it reuses the same function
|
|
21
|
+
// rather than shelling out a second way to ask one question.
|
|
22
|
+
import { currentIdentity } from "./cswap-admin.mjs";
|
|
19
23
|
import { dirname, join } from "node:path";
|
|
20
24
|
import { existsSync } from "node:fs";
|
|
21
25
|
import { homedir, platform } from "node:os";
|
|
@@ -398,6 +402,17 @@ async function readRoster(now, gen) {
|
|
|
398
402
|
// fetched at all.
|
|
399
403
|
nudgeCollector(rows, Object.keys(seq.accounts), now, seq.activeAccountNumber);
|
|
400
404
|
|
|
405
|
+
// Who the CLI says is signed in, asked ONLY when the store claims the active
|
|
406
|
+
// account is in trouble — see authTrouble. That is the one case where the
|
|
407
|
+
// stored verdict and the live truth can disagree, and it is rare: a healthy
|
|
408
|
+
// machine never spends this subprocess. Never fatal, because a CLI that
|
|
409
|
+
// cannot be reached is not evidence either way.
|
|
410
|
+
const activeNum = seq.activeAccountNumber != null ? String(seq.activeAccountNumber) : null;
|
|
411
|
+
const activeRow = activeNum ? rows[activeNum] : null;
|
|
412
|
+
const identity = (activeRow?.consecutiveFailures ?? 0) > 0
|
|
413
|
+
? await currentIdentity().catch(() => null)
|
|
414
|
+
: null;
|
|
415
|
+
|
|
401
416
|
const order = Array.isArray(seq.sequence) && seq.sequence.length
|
|
402
417
|
? seq.sequence.map(String)
|
|
403
418
|
: Object.keys(seq.accounts).sort((a, b) => Number(a) - Number(b));
|
|
@@ -417,6 +432,8 @@ async function readRoster(now, gen) {
|
|
|
417
432
|
const good = matches ? row.lastGood : null;
|
|
418
433
|
|
|
419
434
|
const fetchedAtMs = matches && typeof row.fetchedAt === "number" ? row.fetchedAt * 1000 : null;
|
|
435
|
+
const isActive = String(seq.activeAccountNumber) === num;
|
|
436
|
+
const trouble = authTrouble(row, { matches, isActive, identity, email: acct.email });
|
|
420
437
|
|
|
421
438
|
const lanes = [
|
|
422
439
|
lane("five_hour", "5h", good?.five_hour),
|
|
@@ -442,23 +459,76 @@ async function readRoster(now, gen) {
|
|
|
442
459
|
// plan and, for a healthy active account, the deck's freshen tick. The
|
|
443
460
|
// plan alone would promise "next in 15m" while the panel actually
|
|
444
461
|
// updates in three.
|
|
445
|
-
nextAt: nextReadAt(row, matches, fetchedAtMs,
|
|
462
|
+
nextAt: nextReadAt(row, matches, fetchedAtMs, isActive, now),
|
|
446
463
|
stale: fetchedAtMs == null || now - fetchedAtMs > STALE_AFTER_MS,
|
|
447
464
|
// Surfaced rather than hidden: a rate-limited or re-login-needed account
|
|
448
465
|
// is exactly the one the user is about to try switching to.
|
|
449
466
|
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
// account
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
467
|
+
// Through authTrouble rather than read straight off the row: see #721.
|
|
468
|
+
// consecutiveFailures says the COLLECTOR is failing, which for the active
|
|
469
|
+
// account is not the same claim as the user being signed out — and the
|
|
470
|
+
// CLI can settle that.
|
|
471
|
+
error: trouble?.error ?? null,
|
|
472
|
+
// True when the collector cannot read this account but the user is signed
|
|
473
|
+
// in as it anyway. The panel says so quietly instead of offering to log
|
|
474
|
+
// them in again.
|
|
475
|
+
staleCopy: trouble?.kind === "stale-copy",
|
|
456
476
|
});
|
|
457
477
|
}
|
|
458
478
|
|
|
459
479
|
return finish({ ok: true, accounts, activeNum: seq.activeAccountNumber ?? null, fetchedAt: now });
|
|
460
480
|
}
|
|
461
481
|
|
|
482
|
+
/**
|
|
483
|
+
* What to say about an account whose collector is failing — which is not the
|
|
484
|
+
* same question as whether the user is signed out.
|
|
485
|
+
*
|
|
486
|
+
* TWO FACTS, AND #721 SHIPPED THEM AS ONE. claude-swap keeps its own copy of
|
|
487
|
+
* each account's credentials, taken when `cswap add` captured the slot. When
|
|
488
|
+
* that copy's refresh token dies, claude-swap can no longer collect usage for
|
|
489
|
+
* the row and says `relogin_required`. That is true, and it is about the COPY.
|
|
490
|
+
*
|
|
491
|
+
* It says nothing about whether the user is signed in. Measured on the machine
|
|
492
|
+
* that reported this, at the same instant:
|
|
493
|
+
*
|
|
494
|
+
* claude auth status --json -> loggedIn: true, claude3@sapec.md
|
|
495
|
+
* cswap list --json -> claude3@sapec.md: relogin_required
|
|
496
|
+
* GET /api/quota -> source: cli, 5h 33%, 7d 37%
|
|
497
|
+
*
|
|
498
|
+
* The user had signed in again in a terminal, which refreshes the LIVE
|
|
499
|
+
* credentials and leaves claude-swap's stored copy exactly as dead as it was.
|
|
500
|
+
* The deck held live quota numbers for that account and printed "login expired"
|
|
501
|
+
* beside them, and the button it offered ran `claude auth login` — a full
|
|
502
|
+
* re-login of the account the user was mid-session in, to fix a problem they
|
|
503
|
+
* did not have.
|
|
504
|
+
*
|
|
505
|
+
* So: for the ACTIVE account the CLI is the authority, because it is the one
|
|
506
|
+
* thing that can answer about the live credentials rather than about a copy.
|
|
507
|
+
* When it says the user is signed in as this account, there is no login
|
|
508
|
+
* failure to report — only a collector that cannot see it, which is quieter,
|
|
509
|
+
* true, and fixed by re-capturing the slot rather than by signing in again.
|
|
510
|
+
*
|
|
511
|
+
* `identity` is null when the CLI could not be asked at all. That is not
|
|
512
|
+
* evidence of anything, so the stored verdict stands: refusing to show a real
|
|
513
|
+
* expiry because a subprocess failed is the opposite mistake.
|
|
514
|
+
*/
|
|
515
|
+
export function authTrouble(row, { matches, isActive, identity, email } = {}) {
|
|
516
|
+
if (!matches || !((row?.consecutiveFailures ?? 0) > 0)) return null;
|
|
517
|
+
|
|
518
|
+
const signedInHere = isActive
|
|
519
|
+
&& identity
|
|
520
|
+
&& typeof identity.email === "string"
|
|
521
|
+
&& email
|
|
522
|
+
&& identity.email.toLowerCase() === String(email).toLowerCase();
|
|
523
|
+
|
|
524
|
+
if (signedInHere) {
|
|
525
|
+
// Deliberately not the `error` field: this is not the user's problem to
|
|
526
|
+
// fix under a red badge, and it must not offer to sign them in again.
|
|
527
|
+
return { kind: "stale-copy", error: null };
|
|
528
|
+
}
|
|
529
|
+
return { kind: "auth", error: row.lastError ?? "error" };
|
|
530
|
+
}
|
|
531
|
+
|
|
462
532
|
/**
|
|
463
533
|
* claude-swap's last good usage for whichever account is active right now.
|
|
464
534
|
*
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
import { open, stat } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { StringDecoder } from "node:string_decoder";
|
|
7
|
+
import { createReadStream } from "node:fs";
|
|
8
|
+
// The NAMESPACE, never a named import. `import { createZstdDecompress }` is
|
|
9
|
+
// resolved at link time, and node:zlib has no such export before Node 22.15 —
|
|
10
|
+
// so the named form does not degrade on an older runtime, it throws before a
|
|
11
|
+
// single line of this module runs and takes the whole deck down with it. Caught
|
|
12
|
+
// by the timezone probe, which spawns a child that imports this file.
|
|
13
|
+
import zlib from "node:zlib";
|
|
7
14
|
import { STOP, walkRolloutDays } from "./codex-dir.mjs";
|
|
8
15
|
import { PRODUCT } from "./brand.mjs";
|
|
9
16
|
|
|
@@ -154,7 +161,75 @@ function foldTokenLine(series, raw) {
|
|
|
154
161
|
// Returns an ascending-by-time array of { ts, inp, out, cacheR, total } where
|
|
155
162
|
// `inp` includes the cached portion (Codex reports input_tokens incl. cache),
|
|
156
163
|
// or null if the file has no usable token_count events.
|
|
164
|
+
/**
|
|
165
|
+
* Codex compresses cold rollouts, and this is how the deck keeps reading them.
|
|
166
|
+
*
|
|
167
|
+
* openai/codex 0.153.0 added a background worker that rewrites any rollout older
|
|
168
|
+
* than seven days as `rollout-….jsonl.zst`. Its own source says the quiet part
|
|
169
|
+
* out loud — "Requires every reader of the Codex home to support compressed
|
|
170
|
+
* shared histories" — and this deck is one of those readers. The flag
|
|
171
|
+
* (`local_thread_store_compression`) is still `default_enabled: false`, so
|
|
172
|
+
* nothing on disk has changed yet; the failure it would cause is why this is
|
|
173
|
+
* here before it does. A reader that matched only `.jsonl` would have skipped
|
|
174
|
+
* every day past the seventh IN SILENCE, and the 30-day Codex window would have
|
|
175
|
+
* quietly collapsed to the last seven with figures that still looked right.
|
|
176
|
+
*
|
|
177
|
+
* Streamed, not decompressed whole. The plain path reads a chunk at a time
|
|
178
|
+
* precisely so a megabyte of prompt text is never buffered, and handing that
|
|
179
|
+
* property back for a one-line `zstdDecompressSync` would trade a silent
|
|
180
|
+
* undercount for a memory spike.
|
|
181
|
+
*
|
|
182
|
+
* `createZstdDecompress` arrived in Node 22.15 and this package declares
|
|
183
|
+
* `>=18`, so a deck on an older runtime cannot read them at all. It says so
|
|
184
|
+
* once, on the terminal it was started from, rather than counting zero and
|
|
185
|
+
* looking healthy.
|
|
186
|
+
*/
|
|
187
|
+
const COMPRESSED = ".jsonl.zst";
|
|
188
|
+
const createZstdDecompress = typeof zlib.createZstdDecompress === "function"
|
|
189
|
+
? zlib.createZstdDecompress
|
|
190
|
+
: null;
|
|
191
|
+
let warnedNoZstd = false;
|
|
192
|
+
|
|
193
|
+
async function readCompressedTokenSeries(filePath) {
|
|
194
|
+
if (!createZstdDecompress) {
|
|
195
|
+
if (!warnedNoZstd) {
|
|
196
|
+
warnedNoZstd = true;
|
|
197
|
+
console.error(
|
|
198
|
+
`${PRODUCT} codex-usage: this Node (${process.version}) cannot read Codex's compressed `
|
|
199
|
+
+ "rollouts; sessions older than about a week are being left out. Node 22.15 or newer reads them.",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const series = [];
|
|
206
|
+
const decoder = new StringDecoder("utf8");
|
|
207
|
+
let pending = "";
|
|
208
|
+
const stream = createReadStream(filePath).pipe(createZstdDecompress());
|
|
209
|
+
for await (const chunk of stream) {
|
|
210
|
+
pending += decoder.write(chunk);
|
|
211
|
+
let from = 0;
|
|
212
|
+
let nl;
|
|
213
|
+
while ((nl = pending.indexOf("\n", from)) >= 0) {
|
|
214
|
+
foldTokenLine(series, pending.slice(from, nl));
|
|
215
|
+
from = nl + 1;
|
|
216
|
+
}
|
|
217
|
+
if (from > 0) pending = pending.slice(from);
|
|
218
|
+
}
|
|
219
|
+
pending += decoder.end();
|
|
220
|
+
if (pending) foldTokenLine(series, pending);
|
|
221
|
+
return series.length ? series : null;
|
|
222
|
+
} catch { return null; }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The reader, exported under a test-only name. The compressed path cannot be
|
|
226
|
+
* reached through `fetchCodexUsage` without a Codex home full of week-old
|
|
227
|
+
* sessions, and the thing worth checking is that both spellings produce the
|
|
228
|
+
* same series. */
|
|
229
|
+
export const readTokenSeriesForTest = filePath => readTokenSeries(filePath);
|
|
230
|
+
|
|
157
231
|
async function readTokenSeries(filePath) {
|
|
232
|
+
if (filePath.endsWith(COMPRESSED)) return readCompressedTokenSeries(filePath);
|
|
158
233
|
let fd;
|
|
159
234
|
try {
|
|
160
235
|
fd = await open(filePath, "r");
|
|
@@ -330,7 +405,10 @@ async function listRolloutFiles(sinceMs) {
|
|
|
330
405
|
await walkRolloutDays(
|
|
331
406
|
(dir, files) => {
|
|
332
407
|
for (const f of files) {
|
|
333
|
-
|
|
408
|
+
// Both spellings. A cold rollout is `rollout-….jsonl.zst` and its name
|
|
409
|
+
// still carries the timestamp parseRolloutTime reads, so nothing else
|
|
410
|
+
// in this function has to know.
|
|
411
|
+
if (!f.endsWith(".jsonl") && !f.endsWith(COMPRESSED)) continue;
|
|
334
412
|
const t = parseRolloutTime(f);
|
|
335
413
|
if (t != null && nowMs - t <= sinceMs) {
|
|
336
414
|
out.push({ path: join(dir, f), startMs: t });
|