@monster-orange/den 0.4.27 → 0.4.28
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/CHANGELOG.md +47 -0
- package/docs/distributions.md +73 -2
- package/package.json +1 -1
- package/src/shared/agents/claude-helpers.ts +623 -0
- package/src/shared/agents/den-claude.ts +54 -0
- package/src/shared/agents/helpers.ts +175 -0
- package/src/shared/agents/runtime.ts +13 -0
- package/src/shared/den-config.ts +42 -1
- package/src/shared/hub.ts +10 -0
- package/src/ui/cli/agent-ops.ts +144 -0
- package/src/ui/cli/agents-command.ts +127 -1
- package/src/ui/mcp/api.ts +8 -2
- package/src/ui/mcp/server.ts +13 -1
- package/src/ui/mcp/tools.ts +50 -0
- package/src/ui/server/agent-routes.ts +56 -2
- package/src/ui/server/hub-connect.ts +69 -3
- package/src/ui/server/index.ts +7 -1
- package/src/ui/server/public/agent-helpers.js +311 -0
- package/src/ui/server/public/agents-page.js +76 -9
- package/src/ui/server/public/i18n/es.js +39 -0
- package/src/ui/server/public/i18n/ja.js +37 -0
- package/src/ui/server/public/settings-registry.js +37 -1
- package/src/ui/server/public/shell.js +200 -0
- package/src/ui/server/public/transcript-view.js +202 -0
- package/src/ui/server/request-guard.ts +3 -1
- package/src/ui/server/route-classes.ts +16 -0
- package/src/ui/server/session-routes.ts +71 -1
- package/src/ui/server/ui-assets.ts +3 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D32: Claude Code's subagents for one conversation, read from its own
|
|
3
|
+
* files (read-only: Den never writes there).
|
|
4
|
+
*
|
|
5
|
+
* <projects>/<enc cwd>/<token>.jsonl the agent's transcript
|
|
6
|
+
* <projects>/<enc cwd>/<token>/subagents/agent-<id>.jsonl a helper's
|
|
7
|
+
* <projects>/<enc cwd>/<token>/subagents/agent-<id>.meta.json its meta
|
|
8
|
+
* <projects>/<enc cwd>/<token>/workflows/wf_<run>.json a workflow
|
|
9
|
+
*
|
|
10
|
+
* None of this is a published format, so everything is optional: unknown
|
|
11
|
+
* fields are ignored, a field of the wrong type is as good as missing, a
|
|
12
|
+
* meta that is missing or broken leaves the bare id, and nothing here
|
|
13
|
+
* throws. Only the ends of a helper's file are read for the list; the
|
|
14
|
+
* agent's own transcript is read once and then only what it appends.
|
|
15
|
+
*
|
|
16
|
+
* Status, strongest evidence first:
|
|
17
|
+
* 1. the meta says the person stopped it (stoppedByUser);
|
|
18
|
+
* 2. the workflow it ran in says so (done, error; a killed run stops it);
|
|
19
|
+
* 3. the agent's task notification for it (<tool-use-id> + <status>);
|
|
20
|
+
* 4. a foreground helper's tool result in the agent's transcript;
|
|
21
|
+
* 5. the meta says finished;
|
|
22
|
+
* 6. its last entry is a finished answer (end_turn) and it went quiet;
|
|
23
|
+
* 7. otherwise, by its last activity: running if recent, else idle.
|
|
24
|
+
* A helper of an agent that isn't running any more can't be running.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from "fs";
|
|
27
|
+
import * as path from "path";
|
|
28
|
+
import {
|
|
29
|
+
HELPER_DEFAULTS,
|
|
30
|
+
sortHelpers,
|
|
31
|
+
type Helper,
|
|
32
|
+
type HelperSettings,
|
|
33
|
+
type HelperStatus,
|
|
34
|
+
type HelperTranscript,
|
|
35
|
+
type HelperTranscriptItem,
|
|
36
|
+
} from "./helpers.js";
|
|
37
|
+
|
|
38
|
+
const ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
39
|
+
/** How much of each end of a helper's file the list reads. */
|
|
40
|
+
const END_BYTES = 64 * 1024;
|
|
41
|
+
/** A transcript view reads at most this much of a helper's file (its end). */
|
|
42
|
+
const VIEW_BYTES = 16 * 1024 * 1024;
|
|
43
|
+
/** A line longer than this in the agent's transcript is skipped. */
|
|
44
|
+
const MAX_LINE = 4 * 1024 * 1024;
|
|
45
|
+
/** A finished answer this quiet means done, though nothing else said so. */
|
|
46
|
+
const QUIET_MS = 30_000;
|
|
47
|
+
|
|
48
|
+
export interface ReadHelpersOptions {
|
|
49
|
+
now?: number;
|
|
50
|
+
settings?: Partial<HelperSettings>;
|
|
51
|
+
/** Whether the agent itself is running (its helpers can't outlive it). */
|
|
52
|
+
agentLive?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const str = (v: unknown, max = 300): string | undefined =>
|
|
56
|
+
typeof v === "string" && v.trim() ? v.trim().slice(0, max) : undefined;
|
|
57
|
+
const num = (v: unknown): number | undefined =>
|
|
58
|
+
typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
59
|
+
const time = (v: unknown): number | undefined => {
|
|
60
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
61
|
+
if (typeof v === "string") {
|
|
62
|
+
const t = Date.parse(v);
|
|
63
|
+
return Number.isFinite(t) ? t : undefined;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
};
|
|
67
|
+
const obj = (v: unknown): Record<string, unknown> | null =>
|
|
68
|
+
v && typeof v === "object" && !Array.isArray(v)
|
|
69
|
+
? (v as Record<string, unknown>)
|
|
70
|
+
: null;
|
|
71
|
+
|
|
72
|
+
function readJson(file: string): Record<string, unknown> | null {
|
|
73
|
+
try {
|
|
74
|
+
return obj(JSON.parse(fs.readFileSync(file, "utf-8")));
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The JSON lines in `bytes` of `file` from `start` (whole lines only). */
|
|
81
|
+
function readLines(
|
|
82
|
+
file: string,
|
|
83
|
+
start: number,
|
|
84
|
+
bytes: number,
|
|
85
|
+
): Record<string, unknown>[] {
|
|
86
|
+
let fd: number | null = null;
|
|
87
|
+
try {
|
|
88
|
+
fd = fs.openSync(file, "r");
|
|
89
|
+
const buf = Buffer.alloc(bytes);
|
|
90
|
+
const n = fs.readSync(fd, buf, 0, bytes, start);
|
|
91
|
+
const lines = buf.subarray(0, n).toString("utf-8").split("\n");
|
|
92
|
+
if (start > 0) lines.shift(); // a cut line
|
|
93
|
+
if (start + n < fs.fstatSync(fd).size) lines.pop();
|
|
94
|
+
const out: Record<string, unknown>[] = [];
|
|
95
|
+
for (const l of lines) {
|
|
96
|
+
if (!l.trim()) continue;
|
|
97
|
+
try {
|
|
98
|
+
const e = obj(JSON.parse(l));
|
|
99
|
+
if (e) out.push(e);
|
|
100
|
+
} catch {
|
|
101
|
+
/* a line that isn't JSON: skipped */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
} catch {
|
|
106
|
+
return [];
|
|
107
|
+
} finally {
|
|
108
|
+
if (fd !== null) fs.closeSync(fd);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** What the ends of a helper's file say: when, the model, the last entry. */
|
|
113
|
+
function fileEnds(file: string): {
|
|
114
|
+
startedAt?: number;
|
|
115
|
+
lastAt?: number;
|
|
116
|
+
model?: string;
|
|
117
|
+
endTurn: boolean;
|
|
118
|
+
lastTool?: string;
|
|
119
|
+
mtime?: number;
|
|
120
|
+
} {
|
|
121
|
+
let size = 0;
|
|
122
|
+
let mtime: number | undefined;
|
|
123
|
+
try {
|
|
124
|
+
const st = fs.statSync(file);
|
|
125
|
+
size = st.size;
|
|
126
|
+
mtime = st.mtimeMs;
|
|
127
|
+
} catch {
|
|
128
|
+
return { endTurn: false };
|
|
129
|
+
}
|
|
130
|
+
const head = readLines(file, 0, Math.min(END_BYTES, size));
|
|
131
|
+
const tail =
|
|
132
|
+
size > END_BYTES ? readLines(file, size - END_BYTES, END_BYTES) : head;
|
|
133
|
+
let startedAt: number | undefined;
|
|
134
|
+
for (const e of head) {
|
|
135
|
+
startedAt = time(e.timestamp);
|
|
136
|
+
if (startedAt !== undefined) break;
|
|
137
|
+
}
|
|
138
|
+
let lastAt: number | undefined;
|
|
139
|
+
let model: string | undefined;
|
|
140
|
+
let endTurn = false;
|
|
141
|
+
let lastTool: string | undefined;
|
|
142
|
+
let sawLast = false;
|
|
143
|
+
for (let i = tail.length - 1; i >= 0; i--) {
|
|
144
|
+
const e = tail[i];
|
|
145
|
+
lastAt ??= time(e.timestamp);
|
|
146
|
+
const m = obj(e.message);
|
|
147
|
+
if (e.type === "assistant" && m) {
|
|
148
|
+
model ??= str(m.model, 80);
|
|
149
|
+
if (!sawLast) {
|
|
150
|
+
sawLast = true;
|
|
151
|
+
endTurn = m.stop_reason === "end_turn";
|
|
152
|
+
}
|
|
153
|
+
if (!lastTool && Array.isArray(m.content))
|
|
154
|
+
for (const c of [...m.content].reverse()) {
|
|
155
|
+
const o = obj(c);
|
|
156
|
+
if (o?.type === "tool_use" && str(o.name, 80)) {
|
|
157
|
+
lastTool = str(o.name, 80);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else if (e.type === "user" && !sawLast && m) {
|
|
162
|
+
// A user turn after the last answer: it is working on it.
|
|
163
|
+
sawLast = true;
|
|
164
|
+
endTurn = false;
|
|
165
|
+
}
|
|
166
|
+
if (lastAt !== undefined && model && sawLast && lastTool) break;
|
|
167
|
+
}
|
|
168
|
+
if (!model)
|
|
169
|
+
for (const e of head) {
|
|
170
|
+
const m = obj(e.message);
|
|
171
|
+
if (e.type === "assistant" && m && str(m.model, 80)) {
|
|
172
|
+
model = str(m.model, 80);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { startedAt, lastAt, model, endTurn, lastTool, mtime };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** What the agent's own transcript says about its helpers (read on as it grows). */
|
|
180
|
+
interface ParentFacts {
|
|
181
|
+
/** toolUseId → the last task-notification status. */
|
|
182
|
+
notified: Map<string, string>;
|
|
183
|
+
/** toolUseId → a tool result came back (error or not). */
|
|
184
|
+
results: Map<string, boolean>;
|
|
185
|
+
}
|
|
186
|
+
const parentCache = new Map<
|
|
187
|
+
string,
|
|
188
|
+
{ offset: number; rest: string; facts: ParentFacts; ino: number }
|
|
189
|
+
>();
|
|
190
|
+
|
|
191
|
+
const NOTE_RE =
|
|
192
|
+
/<tool-use-id>([A-Za-z0-9_-]{1,128})<\/tool-use-id>[\s\S]*?<status>([a-z_]{1,32})<\/status>/g;
|
|
193
|
+
|
|
194
|
+
function parentFacts(file: string): ParentFacts {
|
|
195
|
+
let st: fs.Stats;
|
|
196
|
+
try {
|
|
197
|
+
st = fs.statSync(file);
|
|
198
|
+
} catch {
|
|
199
|
+
return { notified: new Map(), results: new Map() };
|
|
200
|
+
}
|
|
201
|
+
let c = parentCache.get(file);
|
|
202
|
+
if (!c || c.ino !== st.ino || st.size < c.offset) {
|
|
203
|
+
c = {
|
|
204
|
+
offset: 0,
|
|
205
|
+
rest: "",
|
|
206
|
+
ino: st.ino,
|
|
207
|
+
facts: { notified: new Map(), results: new Map() },
|
|
208
|
+
};
|
|
209
|
+
parentCache.set(file, c);
|
|
210
|
+
}
|
|
211
|
+
if (st.size === c.offset) return c.facts;
|
|
212
|
+
let fd: number | null = null;
|
|
213
|
+
try {
|
|
214
|
+
fd = fs.openSync(file, "r");
|
|
215
|
+
const chunk = 8 * 1024 * 1024;
|
|
216
|
+
const buf = Buffer.alloc(chunk);
|
|
217
|
+
while (c.offset < st.size) {
|
|
218
|
+
const n = fs.readSync(
|
|
219
|
+
fd,
|
|
220
|
+
buf,
|
|
221
|
+
0,
|
|
222
|
+
Math.min(chunk, st.size - c.offset),
|
|
223
|
+
c.offset,
|
|
224
|
+
);
|
|
225
|
+
if (n <= 0) break;
|
|
226
|
+
c.offset += n;
|
|
227
|
+
const text = c.rest + buf.subarray(0, n).toString("utf-8");
|
|
228
|
+
const lines = text.split("\n");
|
|
229
|
+
c.rest = lines.pop() ?? "";
|
|
230
|
+
if (c.rest.length > MAX_LINE) c.rest = "";
|
|
231
|
+
for (const l of lines) scanParentLine(l, c.facts);
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
/* what was read stands */
|
|
235
|
+
} finally {
|
|
236
|
+
if (fd !== null) fs.closeSync(fd);
|
|
237
|
+
}
|
|
238
|
+
return c.facts;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function scanParentLine(l: string, f: ParentFacts): void {
|
|
242
|
+
if (l.length > MAX_LINE) return;
|
|
243
|
+
if (l.includes("<task-notification>"))
|
|
244
|
+
for (const m of l.matchAll(NOTE_RE)) f.notified.set(m[1], m[2]);
|
|
245
|
+
if (!l.includes('"tool_result"')) return;
|
|
246
|
+
let e: Record<string, unknown> | null = null;
|
|
247
|
+
try {
|
|
248
|
+
e = obj(JSON.parse(l));
|
|
249
|
+
} catch {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const content = obj(e?.message)?.content;
|
|
253
|
+
if (!Array.isArray(content)) return;
|
|
254
|
+
for (const x of content) {
|
|
255
|
+
const o = obj(x);
|
|
256
|
+
const id = str(o?.tool_use_id, 128);
|
|
257
|
+
if (o?.type === "tool_result" && id) f.results.set(id, o.is_error === true);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Forget what was read of agents' transcripts (tests). */
|
|
262
|
+
export function resetHelperCache(): void {
|
|
263
|
+
parentCache.clear();
|
|
264
|
+
workflowCache.clear();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface WorkflowAgent {
|
|
268
|
+
runId: string;
|
|
269
|
+
name?: string;
|
|
270
|
+
phase?: string;
|
|
271
|
+
label?: string;
|
|
272
|
+
state?: string;
|
|
273
|
+
runStatus?: string;
|
|
274
|
+
model?: string;
|
|
275
|
+
startedAt?: number;
|
|
276
|
+
durationMs?: number;
|
|
277
|
+
lastTool?: string;
|
|
278
|
+
}
|
|
279
|
+
const workflowCache = new Map<
|
|
280
|
+
string,
|
|
281
|
+
{ mtime: number; agents: Map<string, WorkflowAgent> }
|
|
282
|
+
>();
|
|
283
|
+
|
|
284
|
+
/** agentId → where it ran in a workflow, from every wf_*.json in `dir`. */
|
|
285
|
+
function workflowAgents(dir: string): Map<string, WorkflowAgent> {
|
|
286
|
+
const out = new Map<string, WorkflowAgent>();
|
|
287
|
+
let names: string[] = [];
|
|
288
|
+
try {
|
|
289
|
+
names = fs.readdirSync(dir).filter((n) => /^wf_[\w-]+\.json$/.test(n));
|
|
290
|
+
} catch {
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
for (const n of names) {
|
|
294
|
+
const file = path.join(dir, n);
|
|
295
|
+
let mtime = 0;
|
|
296
|
+
try {
|
|
297
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
298
|
+
} catch {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
let c = workflowCache.get(file);
|
|
302
|
+
if (!c || c.mtime !== mtime) {
|
|
303
|
+
const agents = new Map<string, WorkflowAgent>();
|
|
304
|
+
const d = readJson(file);
|
|
305
|
+
const runId = str(d?.runId, 128) ?? n.replace(/\.json$/, "");
|
|
306
|
+
const progress = Array.isArray(d?.workflowProgress)
|
|
307
|
+
? d.workflowProgress
|
|
308
|
+
: [];
|
|
309
|
+
for (const p of progress) {
|
|
310
|
+
const o = obj(p);
|
|
311
|
+
const id = str(o?.agentId, 128);
|
|
312
|
+
if (o?.type !== "workflow_agent" || !id) continue;
|
|
313
|
+
agents.set(id, {
|
|
314
|
+
runId,
|
|
315
|
+
name: str(d?.workflowName, 120),
|
|
316
|
+
phase: str(o.phaseTitle, 120),
|
|
317
|
+
label: str(o.label, 160),
|
|
318
|
+
state: str(o.state, 32),
|
|
319
|
+
runStatus: str(d?.status, 32),
|
|
320
|
+
model: str(o.model, 80),
|
|
321
|
+
startedAt: time(o.startedAt),
|
|
322
|
+
durationMs: num(o.durationMs),
|
|
323
|
+
lastTool: str(o.lastToolName, 80),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
c = { mtime, agents };
|
|
327
|
+
workflowCache.set(file, c);
|
|
328
|
+
}
|
|
329
|
+
for (const [k, v] of c.agents) out.set(k, v);
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The folder Claude Code keeps conversation `transcript`'s helpers in. */
|
|
335
|
+
export function helpersDir(transcript: string): string {
|
|
336
|
+
return transcript.replace(/\.jsonl$/, "");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The helpers of the conversation whose transcript is `transcript`. Never throws. */
|
|
340
|
+
export function readClaudeHelpers(
|
|
341
|
+
transcript: string,
|
|
342
|
+
opts: ReadHelpersOptions = {},
|
|
343
|
+
): Helper[] {
|
|
344
|
+
try {
|
|
345
|
+
return sortHelpers(readAll(transcript, opts));
|
|
346
|
+
} catch {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function readAll(transcript: string, opts: ReadHelpersOptions): Helper[] {
|
|
352
|
+
const now = opts.now ?? Date.now();
|
|
353
|
+
const s = { ...HELPER_DEFAULTS, ...(opts.settings ?? {}) };
|
|
354
|
+
const activeMs = Math.max(1, s.activeMinutes) * 60_000;
|
|
355
|
+
const base = helpersDir(transcript);
|
|
356
|
+
const sub = path.join(base, "subagents");
|
|
357
|
+
let names: string[] = [];
|
|
358
|
+
try {
|
|
359
|
+
names = fs.readdirSync(sub);
|
|
360
|
+
} catch {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
const ids = new Set<string>();
|
|
364
|
+
for (const n of names) {
|
|
365
|
+
const m = /^agent-([A-Za-z0-9_-]{1,128})\.(?:meta\.json|jsonl)$/.exec(n);
|
|
366
|
+
if (m) ids.add(m[1]);
|
|
367
|
+
}
|
|
368
|
+
if (!ids.size) return [];
|
|
369
|
+
const wf = workflowAgents(path.join(base, "workflows"));
|
|
370
|
+
const facts = parentFacts(transcript);
|
|
371
|
+
const out: Helper[] = [];
|
|
372
|
+
for (const id of ids) {
|
|
373
|
+
const meta = readJson(path.join(sub, `agent-${id}.meta.json`)) ?? {};
|
|
374
|
+
const ends = fileEnds(path.join(sub, `agent-${id}.jsonl`));
|
|
375
|
+
const w = wf.get(id);
|
|
376
|
+
const toolUseId = str(meta.toolUseId, 128);
|
|
377
|
+
const shape = str(meta.requestShape, 32);
|
|
378
|
+
const lastAt = ends.lastAt ?? ends.mtime;
|
|
379
|
+
const startedAt = w?.startedAt ?? ends.startedAt;
|
|
380
|
+
let status: HelperStatus;
|
|
381
|
+
let from: string;
|
|
382
|
+
const note = toolUseId && !w ? facts.notified.get(toolUseId) : undefined;
|
|
383
|
+
const result = toolUseId ? facts.results.get(toolUseId) : undefined;
|
|
384
|
+
if (meta.stoppedByUser === true) {
|
|
385
|
+
status = "stopped";
|
|
386
|
+
from = "stopped by the person";
|
|
387
|
+
} else if (w && (w.state === "done" || w.state === "error")) {
|
|
388
|
+
status = w.state === "done" ? "done" : "failed";
|
|
389
|
+
from = "its workflow";
|
|
390
|
+
} else if (w && w.runStatus && w.runStatus !== "running") {
|
|
391
|
+
status = w.runStatus === "completed" ? "done" : "stopped";
|
|
392
|
+
from = `its workflow (${w.runStatus})`;
|
|
393
|
+
} else if (note && note !== "running") {
|
|
394
|
+
status =
|
|
395
|
+
note === "completed"
|
|
396
|
+
? "done"
|
|
397
|
+
: note === "failed"
|
|
398
|
+
? "failed"
|
|
399
|
+
: "stopped";
|
|
400
|
+
from = `the agent's notification (${note})`;
|
|
401
|
+
} else if (shape !== "background" && result !== undefined) {
|
|
402
|
+
status = result ? "failed" : "done";
|
|
403
|
+
from = "its result in the agent's transcript";
|
|
404
|
+
} else if (meta.finished === true) {
|
|
405
|
+
status = "done";
|
|
406
|
+
from = "its meta (finished)";
|
|
407
|
+
} else if (
|
|
408
|
+
ends.endTurn &&
|
|
409
|
+
lastAt !== undefined &&
|
|
410
|
+
now - lastAt > QUIET_MS
|
|
411
|
+
) {
|
|
412
|
+
status = "done";
|
|
413
|
+
from = "its last answer";
|
|
414
|
+
} else if (lastAt !== undefined && now - lastAt <= activeMs) {
|
|
415
|
+
status = "running";
|
|
416
|
+
from = "recent activity";
|
|
417
|
+
} else {
|
|
418
|
+
status = "idle";
|
|
419
|
+
from = "no recent activity";
|
|
420
|
+
}
|
|
421
|
+
if (
|
|
422
|
+
opts.agentLive === false &&
|
|
423
|
+
(status === "running" || status === "idle")
|
|
424
|
+
) {
|
|
425
|
+
status = "stopped";
|
|
426
|
+
from = "its agent isn't running";
|
|
427
|
+
}
|
|
428
|
+
const ended = status !== "running" && status !== "idle";
|
|
429
|
+
const end = ended ? (lastAt ?? now) : now;
|
|
430
|
+
const parentId = str(meta.parentAgentId, 128);
|
|
431
|
+
const depth = Math.max(1, Math.min(9, num(meta.spawnDepth) ?? 1));
|
|
432
|
+
out.push({
|
|
433
|
+
id,
|
|
434
|
+
...(str(meta.description, 200) || w?.label
|
|
435
|
+
? { description: str(meta.description, 200) ?? w?.label }
|
|
436
|
+
: {}),
|
|
437
|
+
...(str(meta.agentType, 120) ? { type: str(meta.agentType, 120) } : {}),
|
|
438
|
+
...(str(meta.model, 80) ||
|
|
439
|
+
str(meta.pickedEffortModel, 80) ||
|
|
440
|
+
w?.model ||
|
|
441
|
+
ends.model
|
|
442
|
+
? {
|
|
443
|
+
model:
|
|
444
|
+
str(meta.model, 80) ??
|
|
445
|
+
str(meta.pickedEffortModel, 80) ??
|
|
446
|
+
w?.model ??
|
|
447
|
+
ends.model,
|
|
448
|
+
}
|
|
449
|
+
: {}),
|
|
450
|
+
...(str(meta.worktreeBranch, 200)
|
|
451
|
+
? { branch: str(meta.worktreeBranch, 200) }
|
|
452
|
+
: {}),
|
|
453
|
+
...(str(meta.worktreePath, 500)
|
|
454
|
+
? { worktree: str(meta.worktreePath, 500) }
|
|
455
|
+
: {}),
|
|
456
|
+
...(shape ? { background: shape === "background" } : {}),
|
|
457
|
+
depth,
|
|
458
|
+
...(parentId && ID_RE.test(parentId) ? { parentId } : {}),
|
|
459
|
+
...(w
|
|
460
|
+
? {
|
|
461
|
+
workflow: {
|
|
462
|
+
runId: w.runId,
|
|
463
|
+
...(w.name ? { name: w.name } : {}),
|
|
464
|
+
...(w.phase ? { phase: w.phase } : {}),
|
|
465
|
+
...(w.label ? { label: w.label } : {}),
|
|
466
|
+
},
|
|
467
|
+
}
|
|
468
|
+
: {}),
|
|
469
|
+
status,
|
|
470
|
+
statusFrom: from,
|
|
471
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
472
|
+
...(lastAt !== undefined ? { lastActivityAt: lastAt } : {}),
|
|
473
|
+
...(startedAt !== undefined
|
|
474
|
+
? {
|
|
475
|
+
durationMs:
|
|
476
|
+
ended && w?.durationMs !== undefined
|
|
477
|
+
? w.durationMs
|
|
478
|
+
: Math.max(0, end - startedAt),
|
|
479
|
+
}
|
|
480
|
+
: {}),
|
|
481
|
+
...(ends.lastTool || w?.lastTool
|
|
482
|
+
? { lastTool: ends.lastTool ?? w?.lastTool }
|
|
483
|
+
: {}),
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** A tool call in a few words: its command, file, pattern or description. */
|
|
490
|
+
export function summarizeToolInput(name: string, input: unknown): string {
|
|
491
|
+
const i = obj(input) ?? {};
|
|
492
|
+
// A search (Grep, Glob): what it looks for, then where.
|
|
493
|
+
const pattern = str(i.pattern, 400);
|
|
494
|
+
if (pattern) {
|
|
495
|
+
const where = str(i.path, 400);
|
|
496
|
+
return oneLine(where ? `${pattern} in ${where}` : pattern, 200);
|
|
497
|
+
}
|
|
498
|
+
for (const k of [
|
|
499
|
+
"description",
|
|
500
|
+
"command",
|
|
501
|
+
"file_path",
|
|
502
|
+
"path",
|
|
503
|
+
"url",
|
|
504
|
+
"query",
|
|
505
|
+
"prompt",
|
|
506
|
+
]) {
|
|
507
|
+
const v = str(i[k], 400);
|
|
508
|
+
if (v) return oneLine(v, 200);
|
|
509
|
+
}
|
|
510
|
+
const keys = Object.keys(i).slice(0, 4);
|
|
511
|
+
return keys.length ? `${name}(${keys.join(", ")})` : name;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const oneLine = (s: string, max: number) => {
|
|
515
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
516
|
+
return t.length > max ? `${t.slice(0, max - 1)}…` : t;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const textOf = (content: unknown): string => {
|
|
520
|
+
if (typeof content === "string") return content;
|
|
521
|
+
if (!Array.isArray(content)) return "";
|
|
522
|
+
return content
|
|
523
|
+
.map((c) => {
|
|
524
|
+
const o = obj(c);
|
|
525
|
+
return o?.type === "text" && typeof o.text === "string" ? o.text : "";
|
|
526
|
+
})
|
|
527
|
+
.filter(Boolean)
|
|
528
|
+
.join("\n");
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Helper `id`'s transcript, tool calls summarized (thinking and the
|
|
533
|
+
* runtime's own bookkeeping left out), the last `limit` lines. Null when
|
|
534
|
+
* there is no such helper. Never throws.
|
|
535
|
+
*/
|
|
536
|
+
export function readClaudeHelperTranscript(
|
|
537
|
+
transcript: string,
|
|
538
|
+
id: string,
|
|
539
|
+
opts: ReadHelpersOptions & { limit?: number } = {},
|
|
540
|
+
): HelperTranscript | null {
|
|
541
|
+
try {
|
|
542
|
+
if (!ID_RE.test(id)) return null;
|
|
543
|
+
const helper = readClaudeHelpers(transcript, opts).find((h) => h.id === id);
|
|
544
|
+
if (!helper) return null;
|
|
545
|
+
const file = path.join(
|
|
546
|
+
helpersDir(transcript),
|
|
547
|
+
"subagents",
|
|
548
|
+
`agent-${id}.jsonl`,
|
|
549
|
+
);
|
|
550
|
+
let size = 0;
|
|
551
|
+
try {
|
|
552
|
+
size = fs.statSync(file).size;
|
|
553
|
+
} catch {
|
|
554
|
+
return { helper, items: [], omitted: 0 };
|
|
555
|
+
}
|
|
556
|
+
const start = Math.max(0, size - VIEW_BYTES);
|
|
557
|
+
const entries = readLines(file, start, size - start);
|
|
558
|
+
const items: HelperTranscriptItem[] = [];
|
|
559
|
+
if (start > 0)
|
|
560
|
+
items.push({
|
|
561
|
+
kind: "note",
|
|
562
|
+
text: "Earlier lines are too long ago to show.",
|
|
563
|
+
});
|
|
564
|
+
let first = true;
|
|
565
|
+
for (const e of entries) {
|
|
566
|
+
const at = time(e.timestamp);
|
|
567
|
+
const m = obj(e.message);
|
|
568
|
+
if (!m) continue;
|
|
569
|
+
if (e.type === "user") {
|
|
570
|
+
if (typeof m.content === "string" || first) {
|
|
571
|
+
const t = textOf(m.content);
|
|
572
|
+
if (t.trim()) {
|
|
573
|
+
items.push({
|
|
574
|
+
...(at !== undefined ? { at } : {}),
|
|
575
|
+
kind: first ? "prompt" : "text",
|
|
576
|
+
text: oneLine(t, first ? 4000 : 1000),
|
|
577
|
+
});
|
|
578
|
+
first = false;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (Array.isArray(m.content))
|
|
583
|
+
for (const c of m.content) {
|
|
584
|
+
const o = obj(c);
|
|
585
|
+
if (o?.type !== "tool_result") continue;
|
|
586
|
+
const t =
|
|
587
|
+
textOf(o.content) ||
|
|
588
|
+
(typeof o.content === "string" ? o.content : "");
|
|
589
|
+
items.push({
|
|
590
|
+
...(at !== undefined ? { at } : {}),
|
|
591
|
+
kind: "result",
|
|
592
|
+
text: oneLine(t || "(no output)", 300),
|
|
593
|
+
...(o.is_error === true ? { error: true } : {}),
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
} else if (e.type === "assistant" && Array.isArray(m.content)) {
|
|
597
|
+
for (const c of m.content) {
|
|
598
|
+
const o = obj(c);
|
|
599
|
+
if (o?.type === "text" && typeof o.text === "string" && o.text.trim())
|
|
600
|
+
items.push({
|
|
601
|
+
...(at !== undefined ? { at } : {}),
|
|
602
|
+
kind: "text",
|
|
603
|
+
text: oneLine(o.text, 2000),
|
|
604
|
+
});
|
|
605
|
+
else if (o?.type === "tool_use") {
|
|
606
|
+
const name = str(o.name, 80) ?? "tool";
|
|
607
|
+
items.push({
|
|
608
|
+
...(at !== undefined ? { at } : {}),
|
|
609
|
+
kind: "tool",
|
|
610
|
+
tool: name,
|
|
611
|
+
text: summarizeToolInput(name, o.input),
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const limit = Math.max(1, Math.min(5000, opts.limit ?? 500));
|
|
618
|
+
const omitted = Math.max(0, items.length - limit);
|
|
619
|
+
return { helper, items: items.slice(-limit), omitted };
|
|
620
|
+
} catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
* install's versions/<version>, or the npm package's package.json); the
|
|
46
46
|
* Start dialog and `agents doctor` ask `claude --version` once as well.
|
|
47
47
|
*/
|
|
48
|
+
import {
|
|
49
|
+
readClaudeHelperTranscript,
|
|
50
|
+
readClaudeHelpers,
|
|
51
|
+
} from "./claude-helpers.js";
|
|
52
|
+
import { sortHelpers, type Helper } from "./helpers.js";
|
|
53
|
+
import type { AgentRecord } from "./records.js";
|
|
48
54
|
import { randomUUID } from "crypto";
|
|
49
55
|
import * as fs from "fs";
|
|
50
56
|
import * as path from "path";
|
|
@@ -473,6 +479,25 @@ function nameArg(label: string | undefined): string[] {
|
|
|
473
479
|
|
|
474
480
|
const TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
475
481
|
|
|
482
|
+
/** D32: an agent's conversations: those before each relaunch, then its current one. */
|
|
483
|
+
function conversationsOf(
|
|
484
|
+
rec: AgentRecord,
|
|
485
|
+
): { token: string; current: boolean }[] {
|
|
486
|
+
const seen = new Set<string>();
|
|
487
|
+
const out: { token: string; current: boolean }[] = [];
|
|
488
|
+
for (const r of rec.rotations ?? [])
|
|
489
|
+
if (
|
|
490
|
+
r.conversation &&
|
|
491
|
+
r.conversation !== rec.resumeToken &&
|
|
492
|
+
!seen.has(r.conversation)
|
|
493
|
+
) {
|
|
494
|
+
seen.add(r.conversation);
|
|
495
|
+
out.push({ token: r.conversation, current: false });
|
|
496
|
+
}
|
|
497
|
+
if (rec.resumeToken) out.push({ token: rec.resumeToken, current: true });
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
|
|
476
501
|
/**
|
|
477
502
|
* Claude Code's transcript for conversation `token`: <token>.jsonl in any
|
|
478
503
|
* project under its projects dir, or null (a bad token, none written yet).
|
|
@@ -589,6 +614,35 @@ export const denClaude: Runtime = {
|
|
|
589
614
|
return null;
|
|
590
615
|
}
|
|
591
616
|
},
|
|
617
|
+
// D32: its helpers, from Claude Code's subagent files beside each of its
|
|
618
|
+
// conversations' transcripts (the current one and those before a
|
|
619
|
+
// relaunch). Read-only; never throws.
|
|
620
|
+
helpers(rec, opts = {}) {
|
|
621
|
+
const out: Helper[] = [];
|
|
622
|
+
for (const c of conversationsOf(rec)) {
|
|
623
|
+
const file = transcriptFile(c.token, opts.env);
|
|
624
|
+
if (!file) continue;
|
|
625
|
+
for (const h of readClaudeHelpers(file, {
|
|
626
|
+
...opts,
|
|
627
|
+
agentLive: c.current ? opts.agentLive : false,
|
|
628
|
+
}))
|
|
629
|
+
out.push(c.current ? h : { ...h, earlier: true });
|
|
630
|
+
}
|
|
631
|
+
return sortHelpers(out);
|
|
632
|
+
},
|
|
633
|
+
helperTranscript(rec, id, opts = {}) {
|
|
634
|
+
for (const c of conversationsOf(rec)) {
|
|
635
|
+
const file = transcriptFile(c.token, opts.env);
|
|
636
|
+
if (!file) continue;
|
|
637
|
+
const t = readClaudeHelperTranscript(file, id, {
|
|
638
|
+
...opts,
|
|
639
|
+
agentLive: c.current ? opts.agentLive : false,
|
|
640
|
+
});
|
|
641
|
+
if (t)
|
|
642
|
+
return c.current ? t : { ...t, helper: { ...t.helper, earlier: true } };
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
},
|
|
592
646
|
launchFailures: {
|
|
593
647
|
noConversation: /no conversation found/i,
|
|
594
648
|
idInUse: /already in use/i,
|