agent-dag 1.4.0 → 1.4.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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>agent-dag</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-Bh_-ACC1.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DXdvsUWV.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-DnQUMgzS.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.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Live DAG of Claude Code agents — watch parallel subagents fork, call tools, and return on one calm canvas.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,73 @@ const sseClients = new Set(); // res handles
33
33
 
34
34
  let persistPath = null; // absolute path to events.jsonl, or null
35
35
 
36
+ // ─── Model enrichment ────────────────────────────────────────────────────
37
+ // CC's hook payloads never carry the `model` field — but every hook
38
+ // references a `transcript_path` JSONL that contains lines like
39
+ // `"model":"claude-opus-4-7"`. We read the tail of that file once per
40
+ // session, cache the result, and (a) inject `model` into subsequent
41
+ // payloads for that session before broadcasting, (b) emit a synthetic
42
+ // `ModelObserved` event so the client backfills agents created before
43
+ // the model was resolved.
44
+ const modelBySession = new Map(); // sessionId -> "claude-…"
45
+ const pendingTranscriptReads = new Set(); // sessionId currently being read
46
+
47
+ async function readModelFromTranscript(path) {
48
+ try {
49
+ const s = await stat(path);
50
+ if (s.size === 0) return null;
51
+ // Read up to last 128 KB — plenty for the most-recent model
52
+ // declaration. Reading from the tail handles sessions that switched
53
+ // model mid-conversation (we want the current one).
54
+ const TAIL = 128 * 1024;
55
+ const start = Math.max(0, s.size - TAIL);
56
+ const fh = await open(path, "r");
57
+ try {
58
+ const len = s.size - start;
59
+ const buf = Buffer.alloc(len);
60
+ await fh.read(buf, 0, len, start);
61
+ const text = buf.toString("utf8");
62
+ // Scan all matches and return the LAST one — most recent model used.
63
+ const re = /"model"\s*:\s*"(claude[-_][^"]+)"/gi;
64
+ let last = null;
65
+ for (const m of text.matchAll(re)) last = m[1];
66
+ return last;
67
+ } finally {
68
+ await fh.close();
69
+ }
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function maybeResolveModel(payload) {
76
+ if (!payload || typeof payload !== "object") return;
77
+ const sid = payload.session_id;
78
+ const tp = payload.transcript_path;
79
+ if (!sid || !tp) return;
80
+ if (modelBySession.has(sid)) return;
81
+ if (pendingTranscriptReads.has(sid)) return;
82
+ pendingTranscriptReads.add(sid);
83
+ readModelFromTranscript(tp)
84
+ .then(model => {
85
+ if (!model) return;
86
+ modelBySession.set(sid, model);
87
+ // Synthetic enrichment event — reducer applies to every agent in
88
+ // this session, including ones created before we resolved.
89
+ pushEvent({ hook_event_name: "ModelObserved", session_id: sid, model }, "internal");
90
+ })
91
+ .catch(() => {})
92
+ .finally(() => pendingTranscriptReads.delete(sid));
93
+ }
94
+
36
95
  function pushEvent(raw, source, opts = {}) {
96
+ // Synchronous enrichment: if we already know this session's model, stamp
97
+ // it on the payload so the client's recursive scanner picks it up.
98
+ if (raw && typeof raw === "object" && raw.session_id && !raw.model) {
99
+ const cached = modelBySession.get(raw.session_id);
100
+ if (cached) raw.model = cached;
101
+ }
102
+
37
103
  const seq = nextSeq++;
38
104
  const evt = {
39
105
  seq,
@@ -54,6 +120,11 @@ function pushEvent(raw, source, opts = {}) {
54
120
  appendFile(persistPath, JSON.stringify(evt) + "\n", "utf8").catch(() => {});
55
121
  }
56
122
 
123
+ // Kick off async transcript scan for unknown sessions. The result lands
124
+ // as a synthetic ModelObserved event a few ms later that backfills any
125
+ // agents already on the canvas.
126
+ if (source === "hook" && !opts.replay) maybeResolveModel(raw);
127
+
57
128
  return evt;
58
129
  }
59
130