agent-dag 1.14.2 → 1.15.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.
@@ -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-CkZuDTIx.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DZeh3MHD.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-Bwj0xSAo.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.14.2",
3
+ "version": "1.15.0",
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": {
@@ -73,32 +73,52 @@ async function maybeRotatePersistFile() {
73
73
  // payloads for that session before broadcasting, (b) emit a synthetic
74
74
  // `ModelObserved` event so the client backfills agents created before
75
75
  // the model was resolved.
76
- const modelBySession = new Map(); // sessionId -> "claude-…"
76
+ const modelBySession = new Map(); // sessionId -> { rootModel, subsSig }
77
77
  const pendingTranscriptReads = new Set(); // sessionId currently being read
78
+ const modelLastReadAt = new Map(); // sessionId -> ms timestamp (re-read throttle)
79
+ const MODEL_READ_THROTTLE_MS = 2500;
78
80
 
79
81
  async function readModelFromTranscript(path) {
82
+ // Returns { rootModel, subagentModels } — root is the most recent
83
+ // non-sidechain assistant model, subagents are attributed by the Task
84
+ // tool_use id that owns them. CC transcripts mark subagent messages
85
+ // with `isSidechain:true` and (for current schemas) a `parentToolUseID`
86
+ // or `parent_tool_use_id` referencing the Task invocation.
80
87
  try {
81
88
  const s = await stat(path);
82
89
  if (s.size === 0) return null;
83
- // Read up to last 128 KB — plenty for the most-recent model
84
- // declaration. Reading from the tail handles sessions that switched
85
- // model mid-conversation (we want the current one).
86
- const TAIL = 128 * 1024;
87
- const start = Math.max(0, s.size - TAIL);
88
90
  const fh = await open(path, "r");
91
+ let text;
89
92
  try {
90
- const len = s.size - start;
91
- const buf = Buffer.alloc(len);
92
- await fh.read(buf, 0, len, start);
93
- const text = buf.toString("utf8");
94
- // Scan all matches and return the LAST one — most recent model used.
95
- const re = /"model"\s*:\s*"(claude[-_][^"]+)"/gi;
96
- let last = null;
97
- for (const m of text.matchAll(re)) last = m[1];
98
- return last;
93
+ const buf = Buffer.alloc(s.size);
94
+ await fh.read(buf, 0, s.size, 0);
95
+ text = buf.toString("utf8");
99
96
  } finally {
100
97
  await fh.close();
101
98
  }
99
+ let rootModel = null;
100
+ const subagentModels = {};
101
+ for (const line of text.split("\n")) {
102
+ if (!line) continue;
103
+ let obj;
104
+ try { obj = JSON.parse(line); } catch { continue; }
105
+ const msg = obj && obj.message;
106
+ // Assistant model entries live on either obj.model or obj.message.model
107
+ // depending on CC schema version. Accept either.
108
+ const model = (msg && typeof msg.model === "string" && /^claude[-_]/i.test(msg.model)) ? msg.model
109
+ : (typeof obj.model === "string" && /^claude[-_]/i.test(obj.model)) ? obj.model
110
+ : null;
111
+ if (!model) continue;
112
+ const isSide = obj.isSidechain === true || obj.is_sidechain === true;
113
+ const ptid = obj.parentToolUseID || obj.parent_tool_use_id || obj.parentToolUseId || null;
114
+ if (isSide && ptid) {
115
+ subagentModels[ptid] = model;
116
+ } else if (!isSide) {
117
+ rootModel = model;
118
+ }
119
+ }
120
+ if (!rootModel && Object.keys(subagentModels).length === 0) return null;
121
+ return { rootModel, subagentModels };
102
122
  } catch {
103
123
  return null;
104
124
  }
@@ -109,16 +129,29 @@ function maybeResolveModel(payload) {
109
129
  const sid = payload.session_id;
110
130
  const tp = payload.transcript_path;
111
131
  if (!sid || !tp) return;
112
- if (modelBySession.has(sid)) return;
132
+ // Re-read on every event for this session — the cache was preventing us
133
+ // from picking up subagent models that arrive after the root is known.
134
+ // Throttle so we don't thrash the filesystem.
113
135
  if (pendingTranscriptReads.has(sid)) return;
136
+ const now = Date.now();
137
+ const last = modelLastReadAt.get(sid) ?? 0;
138
+ if (now - last < MODEL_READ_THROTTLE_MS) return;
139
+ modelLastReadAt.set(sid, now);
114
140
  pendingTranscriptReads.add(sid);
115
141
  readModelFromTranscript(tp)
116
- .then(model => {
117
- if (!model) return;
118
- modelBySession.set(sid, model);
119
- // Synthetic enrichment event — reducer applies to every agent in
120
- // this session, including ones created before we resolved.
121
- pushEvent({ hook_event_name: "ModelObserved", session_id: sid, model }, "internal");
142
+ .then(result => {
143
+ if (!result) return;
144
+ const { rootModel, subagentModels } = result;
145
+ const prev = modelBySession.get(sid);
146
+ const subsSig = JSON.stringify(subagentModels);
147
+ if (prev && prev.rootModel === rootModel && prev.subsSig === subsSig) return;
148
+ modelBySession.set(sid, { rootModel, subsSig });
149
+ pushEvent({
150
+ hook_event_name: "ModelObserved",
151
+ session_id: sid,
152
+ model: rootModel,
153
+ subagentModels,
154
+ }, "internal");
122
155
  })
123
156
  .catch(() => {})
124
157
  .finally(() => pendingTranscriptReads.delete(sid));