agent-dag 1.30.6 → 1.30.7
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/dist/web/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>agents-deck</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-LYGlqJEO.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-BHfNMFBX.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.30.
|
|
3
|
+
"version": "1.30.7",
|
|
4
4
|
"description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,9 +42,31 @@ const CACHE_MS = 5_000;
|
|
|
42
42
|
// without a marker would misrepresent them (its own trust ceiling is 3600s).
|
|
43
43
|
const STALE_AFTER_MS = 15 * 60_000;
|
|
44
44
|
|
|
45
|
-
// Nudging the collector
|
|
46
|
-
|
|
45
|
+
// Nudging the collector.
|
|
46
|
+
//
|
|
47
|
+
// Two throttles, because the cost of asking is not the cost of fetching. When
|
|
48
|
+
// claude-swap's plan says nothing is due, `cswap list` fetches nothing — the
|
|
49
|
+
// spawn rate is bounded by the plan (one per 180-600s), not by how often we
|
|
50
|
+
// ask. So asking often is cheap in requests and only costs a subprocess, and
|
|
51
|
+
// asking often is exactly how `cswap watch` stays current: it re-asks every
|
|
52
|
+
// three seconds and therefore collects the moment a plan comes due.
|
|
53
|
+
//
|
|
54
|
+
// The exception is an ask that changes nothing — a claim held by another
|
|
55
|
+
// collector, a backoff, a plan that stays overdue. Repeating that every few
|
|
56
|
+
// seconds is pure spawn churn, so a second, slower throttle applies until the
|
|
57
|
+
// store actually moves.
|
|
58
|
+
const NUDGE_EVERY_MS = 15_000; // when the last ask produced new data
|
|
59
|
+
const NUDGE_QUIET_MS = 60_000; // when it did not
|
|
47
60
|
let _lastNudge = 0;
|
|
61
|
+
let _lastSeenFetch = 0; // newest fetchedAt observed, in seconds
|
|
62
|
+
|
|
63
|
+
// claude-swap's SERVE_TTL_S (180s) plus slack: below this age it serves from
|
|
64
|
+
// the store and fetches nothing, so asking earlier only costs a subprocess.
|
|
65
|
+
const FRESH_MIN_AGE_MS = 190_000;
|
|
66
|
+
|
|
67
|
+
// claude-swap's RECENT_429_WINDOW_S. While a 429 is this recent, its own
|
|
68
|
+
// congestion control is deliberately holding back, and so do we.
|
|
69
|
+
const RECENT_429_MS = 3_600_000;
|
|
48
70
|
|
|
49
71
|
/**
|
|
50
72
|
* Ask claude-swap to collect, if its own schedule says anything is due.
|
|
@@ -91,14 +113,107 @@ export function collectionDue(rows, slots, now) {
|
|
|
91
113
|
return false;
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Whether the engine path may be used to refresh this row now.
|
|
118
|
+
*
|
|
119
|
+
* claude-swap gates fetches two different ways (usage_store._row_eligible).
|
|
120
|
+
* On-demand surfaces — `cswap list`, status, switch — need the row to be BOTH
|
|
121
|
+
* stale and past its planned poll time. The auto engine needs it to be stale
|
|
122
|
+
* OR due, and stale means older than SERVE_TTL_S, which is 180 seconds. That
|
|
123
|
+
* OR is the whole difference: it is why a plan stretched out to 30 minutes
|
|
124
|
+
* still yields three-minute-old numbers to the engine, and why `cswap list`
|
|
125
|
+
* cannot do the same however often it is called.
|
|
126
|
+
*
|
|
127
|
+
* The plan is stretched for a reason, though, and one of those reasons must be
|
|
128
|
+
* respected rather than routed around: claude-swap runs AIMD congestion
|
|
129
|
+
* control on a budget it shares with every other machine holding the same
|
|
130
|
+
* account (POST_429_BACKOFF_MULT, RECENT_429_WINDOW_S). While an account is
|
|
131
|
+
* recovering from a 429, backing off IS the correct behaviour, and polling
|
|
132
|
+
* every 180 seconds through it would re-saturate exactly the window that needs
|
|
133
|
+
* to drain. So the engine path is used only for a healthy row: no live
|
|
134
|
+
* backoff, no failures, and no 429 seen within claude-swap's own recovery
|
|
135
|
+
* window.
|
|
136
|
+
*
|
|
137
|
+
* Exported for tests.
|
|
138
|
+
*/
|
|
139
|
+
export function freshenAllowed(row, now, recent429Ms = RECENT_429_MS) {
|
|
140
|
+
if (!row || typeof row.fetchedAt !== "number") return false; // the list path owns this case
|
|
141
|
+
if (process.env.AGENTS_DECK_NO_FRESHEN === "1") return false;
|
|
142
|
+
if (typeof row.backoffUntil === "number" && row.backoffUntil * 1000 > now) return false;
|
|
143
|
+
if ((row.consecutiveFailures ?? 0) > 0) return false;
|
|
144
|
+
if (typeof row.last429At === "number" && now - row.last429At * 1000 < recent429Ms) return false;
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** freshenAllowed, plus old enough that asking would actually fetch. */
|
|
149
|
+
export function freshenDue(row, now, { minAgeMs = FRESH_MIN_AGE_MS, recent429Ms = RECENT_429_MS } = {}) {
|
|
150
|
+
if (!freshenAllowed(row, now, recent429Ms)) return false;
|
|
151
|
+
// Younger than the serve TTL: claude-swap would answer from the store
|
|
152
|
+
// without fetching, so asking achieves nothing but a subprocess.
|
|
153
|
+
return now - row.fetchedAt * 1000 >= minAgeMs;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* When this account's numbers will next be refreshed, in epoch ms.
|
|
158
|
+
*
|
|
159
|
+
* claude-swap's plan, except for a healthy active account, where the deck's
|
|
160
|
+
* own freshen tick gets there first. Exported for tests.
|
|
161
|
+
*/
|
|
162
|
+
export function nextReadAt(row, matches, fetchedAtMs, isActive, now) {
|
|
163
|
+
const planned = matches && typeof row?.nextPollAt === "number"
|
|
164
|
+
? Math.round(row.nextPollAt * 1000)
|
|
165
|
+
: null;
|
|
166
|
+
const freshenAt = (isActive && fetchedAtMs != null && freshenAllowed(row, now))
|
|
167
|
+
? fetchedAtMs + FRESH_MIN_AGE_MS
|
|
168
|
+
: null;
|
|
169
|
+
if (planned == null) return freshenAt;
|
|
170
|
+
if (freshenAt == null) return planned;
|
|
171
|
+
return Math.min(planned, freshenAt);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Keep the store moving while someone is looking at it.
|
|
176
|
+
*
|
|
177
|
+
* Two ways to ask, and the cheaper one is preferred:
|
|
178
|
+
* - something is due by claude-swap's own plan → `cswap list`, the ordinary
|
|
179
|
+
* on-demand pass every surface uses;
|
|
180
|
+
* - nothing is due but the active account's numbers have aged past the serve
|
|
181
|
+
* TTL → one `cswap auto --once --dry-run`, which is the engine path and so
|
|
182
|
+
* is judged on staleness rather than on the plan.
|
|
183
|
+
*
|
|
184
|
+
* What dry run guarantees is narrower than the name suggests, and worth stating
|
|
185
|
+
* exactly: it never switches accounts and never writes autoswitch state — the
|
|
186
|
+
* switch call is unreachable behind its dry-run return. Its collect pass, on
|
|
187
|
+
* the other hand, runs unconditionally, which is the point: it fetches, writes
|
|
188
|
+
* usage rows, and can rotate and persist an OAuth token exactly as `cswap list`
|
|
189
|
+
* does. So this is not a read-only call; it is the same collection every other
|
|
190
|
+
* surface performs, minus the switch.
|
|
191
|
+
*
|
|
192
|
+
* Either way claude-swap decides whether a network call actually happens, and
|
|
193
|
+
* this is throttled on top of that.
|
|
194
|
+
*/
|
|
195
|
+
function nudgeCollector(rows, slots, now, activeNum) {
|
|
196
|
+
// Did the last ask accomplish anything? Cheap proxy: the newest collection
|
|
197
|
+
// timestamp in the store.
|
|
198
|
+
let newest = 0;
|
|
199
|
+
for (const slot of slots) {
|
|
200
|
+
const f = rows[slot]?.fetchedAt;
|
|
201
|
+
if (typeof f === "number" && f > newest) newest = f;
|
|
202
|
+
}
|
|
203
|
+
const moved = newest > _lastSeenFetch;
|
|
204
|
+
_lastSeenFetch = Math.max(_lastSeenFetch, newest);
|
|
205
|
+
|
|
206
|
+
if (now - _lastNudge < (moved ? NUDGE_EVERY_MS : NUDGE_QUIET_MS)) return;
|
|
207
|
+
|
|
208
|
+
const due = collectionDue(rows, slots, now);
|
|
209
|
+
const freshen = !due && freshenDue(rows[String(activeNum)], now);
|
|
210
|
+
if (!due && !freshen) return;
|
|
97
211
|
|
|
98
212
|
_lastNudge = now;
|
|
213
|
+
const args = due ? ["list"] : ["auto", "--once", "--dry-run", "--json"];
|
|
99
214
|
// Fire-and-forget: this function is deliberately synchronous so callers never
|
|
100
215
|
// wait on it, and resolving the binary is the only async part.
|
|
101
|
-
cswapBin().then(bin => runDetached(bin,
|
|
216
|
+
cswapBin().then(bin => runDetached(bin, args)).catch(() => {});
|
|
102
217
|
}
|
|
103
218
|
|
|
104
219
|
async function readJson(path) {
|
|
@@ -161,7 +276,7 @@ export async function fetchClaudeAccounts({ force = false } = {}) {
|
|
|
161
276
|
// Kick a collection for the NEXT poll if anything is due — either because
|
|
162
277
|
// claude-swap's schedule says so, or because an account has never been
|
|
163
278
|
// fetched at all.
|
|
164
|
-
nudgeCollector(rows, Object.keys(seq.accounts), now);
|
|
279
|
+
nudgeCollector(rows, Object.keys(seq.accounts), now, seq.activeAccountNumber);
|
|
165
280
|
|
|
166
281
|
const order = Array.isArray(seq.sequence) && seq.sequence.length
|
|
167
282
|
? seq.sequence.map(String)
|
|
@@ -203,12 +318,11 @@ export async function fetchClaudeAccounts({ force = false } = {}) {
|
|
|
203
318
|
// this account is worth switching to.
|
|
204
319
|
headroom: lanes.length ? Math.max(0, 100 - Math.max(...lanes.map(l => l.pct))) : null,
|
|
205
320
|
fetchedAt: fetchedAtMs,
|
|
206
|
-
// When
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
nextAt: matches && typeof row.nextPollAt === "number" ? Math.round(row.nextPollAt * 1000) : null,
|
|
321
|
+
// When this account will next be read — the earlier of claude-swap's own
|
|
322
|
+
// plan and, for a healthy active account, the deck's freshen tick. The
|
|
323
|
+
// plan alone would promise "next in 15m" while the panel actually
|
|
324
|
+
// updates in three.
|
|
325
|
+
nextAt: nextReadAt(row, matches, fetchedAtMs, String(seq.activeAccountNumber) === num, now),
|
|
212
326
|
stale: fetchedAtMs == null || now - fetchedAtMs > STALE_AFTER_MS,
|
|
213
327
|
// Surfaced rather than hidden: a rate-limited or re-login-needed account
|
|
214
328
|
// is exactly the one the user is about to try switching to.
|
|
@@ -279,7 +393,7 @@ export async function requestCollection() {
|
|
|
279
393
|
const usage = await readJson(join(root, "cache", "usage.json"));
|
|
280
394
|
const rows = usage?.schemaVersion === 2 ? (usage.accounts ?? {}) : {};
|
|
281
395
|
const before = _lastNudge;
|
|
282
|
-
nudgeCollector(rows, Object.keys(seq.accounts), Date.now());
|
|
396
|
+
nudgeCollector(rows, Object.keys(seq.accounts), Date.now(), seq.activeAccountNumber);
|
|
283
397
|
return _lastNudge !== before;
|
|
284
398
|
}
|
|
285
399
|
|