agent-dag 1.33.63 → 1.33.65
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-A5CpPEUh.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-CF2yG9oG.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.33.
|
|
3
|
+
"version": "1.33.65",
|
|
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": {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readdir, open, stat } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
+
import { StringDecoder } from "node:string_decoder";
|
|
7
8
|
|
|
8
9
|
const CODEX_HOME = process.env.CODEX_HOME
|
|
9
10
|
? process.env.CODEX_HOME
|
|
@@ -18,6 +19,47 @@ const CACHE_MS = 60_000;
|
|
|
18
19
|
const WINDOW_5H_MS = 5 * 60 * 60 * 1000;
|
|
19
20
|
const WINDOW_7D_MS = 7 * 24 * 60 * 60 * 1000;
|
|
20
21
|
|
|
22
|
+
// A rollout file grows for as long as its session runs, and a week's worth of
|
|
23
|
+
// them is however much the user happened to type. Reading them all at once made
|
|
24
|
+
// the peak working set a function of that total; these two caps make it a
|
|
25
|
+
// function of the pool instead — at most MAX_PARALLEL_READS files in flight,
|
|
26
|
+
// each holding one chunk plus the line it is still assembling.
|
|
27
|
+
const READ_CHUNK_BYTES = 256 * 1024;
|
|
28
|
+
const MAX_PARALLEL_READS = 4;
|
|
29
|
+
|
|
30
|
+
// Run `worker` over `items` with at most `limit` of them in flight. Workers
|
|
31
|
+
// pull from a shared cursor, so a slow file delays only its own worker instead
|
|
32
|
+
// of stalling a fixed-size batch.
|
|
33
|
+
async function forEachLimited(items, limit, worker) {
|
|
34
|
+
let next = 0;
|
|
35
|
+
const workers = [];
|
|
36
|
+
for (let i = 0; i < Math.min(limit, items.length); i++) {
|
|
37
|
+
workers.push((async () => {
|
|
38
|
+
while (next < items.length) await worker(items[next++]);
|
|
39
|
+
})());
|
|
40
|
+
}
|
|
41
|
+
await Promise.all(workers);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Append one rollout line's token_count event to the series, if it has one.
|
|
45
|
+
function foldTokenLine(series, raw) {
|
|
46
|
+
// Cheap pre-filter before the (relatively) expensive JSON.parse.
|
|
47
|
+
if (!raw.includes("total_token_usage")) return;
|
|
48
|
+
let obj;
|
|
49
|
+
try { obj = JSON.parse(raw); } catch { return; }
|
|
50
|
+
if (obj.type !== "event_msg" || obj.payload?.type !== "token_count") return;
|
|
51
|
+
const u = obj.payload.info?.total_token_usage;
|
|
52
|
+
if (!u) return;
|
|
53
|
+
const ts = obj.timestamp ? Date.parse(obj.timestamp) : NaN;
|
|
54
|
+
series.push({
|
|
55
|
+
ts: isNaN(ts) ? null : ts,
|
|
56
|
+
inp: u.input_tokens ?? 0,
|
|
57
|
+
out: u.output_tokens ?? 0,
|
|
58
|
+
cacheR: u.cached_input_tokens ?? 0,
|
|
59
|
+
total: u.total_tokens ?? ((u.input_tokens ?? 0) + (u.output_tokens ?? 0)),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
21
63
|
// Read the full series of cumulative token_count events from a rollout file.
|
|
22
64
|
// Each token_count event carries `info.total_token_usage` — the running total
|
|
23
65
|
// for the session at that point. We keep the whole series (with timestamps) so
|
|
@@ -34,25 +76,32 @@ async function readTokenSeries(filePath) {
|
|
|
34
76
|
fd = await open(filePath, "r");
|
|
35
77
|
const { size } = await fd.stat();
|
|
36
78
|
if (size === 0) return null;
|
|
37
|
-
const text = (await fd.readFile()).toString("utf8");
|
|
38
79
|
const series = [];
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
// A chunk at a time rather than the whole file: only the token_count
|
|
81
|
+
// numbers survive the pass, so buffering megabytes of prompt text (once as
|
|
82
|
+
// a Buffer, again as a string, a third time as the split array) bought
|
|
83
|
+
// nothing. StringDecoder carries a UTF-8 sequence that straddles a chunk
|
|
84
|
+
// boundary over to the next chunk.
|
|
85
|
+
const buf = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
86
|
+
const decoder = new StringDecoder("utf8");
|
|
87
|
+
let pending = "";
|
|
88
|
+
let pos = 0;
|
|
89
|
+
while (pos < size) {
|
|
90
|
+
const { bytesRead } = await fd.read(buf, 0, READ_CHUNK_BYTES, pos);
|
|
91
|
+
if (bytesRead <= 0) break;
|
|
92
|
+
pos += bytesRead;
|
|
93
|
+
pending += decoder.write(buf.subarray(0, bytesRead));
|
|
94
|
+
let from = 0;
|
|
95
|
+
let nl;
|
|
96
|
+
while ((nl = pending.indexOf("\n", from)) >= 0) {
|
|
97
|
+
foldTokenLine(series, pending.slice(from, nl));
|
|
98
|
+
from = nl + 1;
|
|
99
|
+
}
|
|
100
|
+
if (from > 0) pending = pending.slice(from);
|
|
55
101
|
}
|
|
102
|
+
// A rollout still being written can end without its final newline.
|
|
103
|
+
pending += decoder.end();
|
|
104
|
+
if (pending) foldTokenLine(series, pending);
|
|
56
105
|
return series.length ? series : null;
|
|
57
106
|
} catch { return null; }
|
|
58
107
|
finally { fd?.close().catch(() => {}); }
|
|
@@ -161,13 +210,16 @@ export async function fetchCodexUsage({ force = false } = {}) {
|
|
|
161
210
|
// longer drops active-but-old sessions or over-counts the pre-window tail.
|
|
162
211
|
const files = await listRolloutFiles(WINDOW_7D_MS);
|
|
163
212
|
|
|
164
|
-
|
|
213
|
+
// Bounded fan-out: a week of rollouts is an open-ended list, and opening
|
|
214
|
+
// every one of them at once also risked EMFILE, which readTokenSeries
|
|
215
|
+
// swallows into a silent undercount.
|
|
216
|
+
await forEachLimited(files, MAX_PARALLEL_READS, async ({ path }) => {
|
|
165
217
|
const series = await readTokenSeries(path);
|
|
166
218
|
if (!series) return;
|
|
167
219
|
// Same series feeds both windows; baseline differs per window start.
|
|
168
220
|
addTo(w5h, windowDelta(series, start5h));
|
|
169
221
|
addTo(w7d, windowDelta(series, start7d));
|
|
170
|
-
})
|
|
222
|
+
});
|
|
171
223
|
} catch (err) {
|
|
172
224
|
console.error("agents-deck codex-usage: scan failed:", err?.message ?? err);
|
|
173
225
|
const result = { ok: false, fetchedAt: now };
|
package/src/server/installer.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Both providers share the discovery dir at <claude config dir>/agent-dag/ so a
|
|
5
5
|
// single running server can receive events from either CLI. Re-runs are safe;
|
|
6
6
|
// entries are tagged with __agent-dag and de-duped.
|
|
7
|
-
import { readFile, writeFile, mkdir,
|
|
7
|
+
import { readFile, writeFile, mkdir, unlink, rename, open, stat, chmod } from "node:fs/promises";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join, resolve, dirname } from "node:path";
|
|
@@ -203,11 +203,33 @@ async function writeFileAtomic(target, text) {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Install one of the packaged hook scripts without ever exposing a partial one.
|
|
208
|
+
*
|
|
209
|
+
* copyFile truncates the destination and then fills it, and the destination here
|
|
210
|
+
* is a script every live Claude Code session runs on each tool call. Starting the
|
|
211
|
+
* deck while sessions are open — the normal way this is used — puts a hook
|
|
212
|
+
* invocation inside that window sooner or later, and what it executes is an
|
|
213
|
+
* empty or half-written program: a dropped event at best, a SyntaxError in the
|
|
214
|
+
* user's session at worst. Renaming a finished copy over the name closes it, so
|
|
215
|
+
* a session opens either the old script or the new one and both are whole.
|
|
216
|
+
*
|
|
217
|
+
* Re-installs are the common case and almost always produce the same bytes, so
|
|
218
|
+
* identical content skips the write and the file is not replaced at all.
|
|
219
|
+
*/
|
|
220
|
+
async function installScript(src, dst) {
|
|
221
|
+
const text = await readFile(src, "utf8");
|
|
222
|
+
const current = await readFile(dst, "utf8").catch(() => null);
|
|
223
|
+
if (current === text) return false;
|
|
224
|
+
await writeFileAtomic(dst, text);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
206
228
|
async function installHookScript(installDir) {
|
|
207
229
|
await ensureDir(installDir);
|
|
208
230
|
const src = join(PKG_ROOT, "hook", "hook.js");
|
|
209
231
|
const dst = join(installDir, "hook.js");
|
|
210
|
-
await
|
|
232
|
+
await installScript(src, dst);
|
|
211
233
|
return dst;
|
|
212
234
|
}
|
|
213
235
|
|
|
@@ -300,5 +322,7 @@ export { AGENT_DAG_DIR, CLAUDE_DIR, CODEX_DIR, CLAUDE_EVENTS, CODEX_EVENTS };
|
|
|
300
322
|
// Exported for the other modules that rewrite settings.json — the sound toggle
|
|
301
323
|
// today. Every one of them needs the same two guarantees: a file we cannot
|
|
302
324
|
// parse is never treated as an empty one, and the replacement is a single
|
|
303
|
-
// rename rather than a truncate a reader can land inside.
|
|
304
|
-
|
|
325
|
+
// rename rather than a truncate a reader can land inside. installScript carries
|
|
326
|
+
// the same guarantee to the hook scripts themselves, which are the files live
|
|
327
|
+
// sessions are actually executing.
|
|
328
|
+
export { readSettingsForWrite, writeFileAtomic, installScript };
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
// Only ever touches its own entry, tagged `__agent-dag-sound`. Hooks the user
|
|
11
11
|
// wrote themselves are left exactly as found — including the platform-specific
|
|
12
12
|
// ones this replaces, which are reported rather than deleted.
|
|
13
|
-
import { readFile, writeFile,
|
|
13
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
15
|
import { join, dirname } from "node:path";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { claudeConfigDir } from "./claude-dir.mjs";
|
|
19
|
-
import { readSettingsForWrite, writeFileAtomic } from "./installer.mjs";
|
|
19
|
+
import { readSettingsForWrite, writeFileAtomic, installScript } from "./installer.mjs";
|
|
20
20
|
|
|
21
21
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
22
22
|
const CLAUDE_DIR = claudeConfigDir();
|
|
@@ -229,7 +229,10 @@ export async function setSoundHook(enabled) {
|
|
|
229
229
|
|
|
230
230
|
if (enabled) {
|
|
231
231
|
if (!existsSync(INSTALL_DIR)) await mkdir(INSTALL_DIR, { recursive: true });
|
|
232
|
-
|
|
232
|
+
// Same reason the event forwarder is installed this way: the Stop hook fires
|
|
233
|
+
// notify.js from sessions that are already running, and toggling the sound
|
|
234
|
+
// on must not leave one of them executing a half-copied file.
|
|
235
|
+
await installScript(join(PKG_ROOT, "hook", "notify.js"), NOTIFY_PATH);
|
|
233
236
|
others.push({
|
|
234
237
|
[MARK]: true,
|
|
235
238
|
hooks: [{
|