mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-30 18:30:21 +00:00
279 lines
9.1 KiB
TypeScript
279 lines
9.1 KiB
TypeScript
import { resolveSubagentLabel, sortSubagentRuns } from "../auto-reply/reply/subagents-utils.js";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { resolveStorePath } from "../config/sessions/paths.js";
|
|
import { loadSessionStore } from "../config/sessions/store-load.js";
|
|
import type { SessionEntry } from "../config/sessions/types.js";
|
|
import { parseAgentSessionKey, type ParsedAgentSessionKey } from "../routing/session-key.js";
|
|
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
|
import {
|
|
formatDurationCompact,
|
|
formatTokenUsageDisplay,
|
|
resolveTotalTokens,
|
|
truncateLine,
|
|
} from "../shared/subagents-format.js";
|
|
import { resolveModelDisplayName, resolveModelDisplayRef } from "./model-selection-display.js";
|
|
import { subagentRuns } from "./subagent-registry-memory.js";
|
|
import { countPendingDescendantRunsFromRuns } from "./subagent-registry-queries.js";
|
|
import {
|
|
getSubagentSessionRuntimeMs,
|
|
getSubagentSessionStartedAt,
|
|
} from "./subagent-registry-read.js";
|
|
import { getSubagentRunsSnapshotForRead } from "./subagent-registry-state.js";
|
|
import type { SubagentRunRecord } from "./subagent-registry.types.js";
|
|
|
|
export type SubagentListItem = {
|
|
index: number;
|
|
line: string;
|
|
runId: string;
|
|
sessionKey: string;
|
|
label: string;
|
|
task: string;
|
|
status: string;
|
|
pendingDescendants: number;
|
|
runtime: string;
|
|
runtimeMs: number;
|
|
childSessions?: string[];
|
|
model?: string;
|
|
totalTokens?: number;
|
|
startedAt?: number;
|
|
endedAt?: number;
|
|
};
|
|
|
|
export type BuiltSubagentList = {
|
|
total: number;
|
|
active: SubagentListItem[];
|
|
recent: SubagentListItem[];
|
|
text: string;
|
|
};
|
|
|
|
export type SessionEntryResolution = {
|
|
storePath: string;
|
|
entry: SessionEntry | undefined;
|
|
};
|
|
|
|
function resolveStorePathForKey(
|
|
cfg: OpenClawConfig,
|
|
key: string,
|
|
parsed?: ParsedAgentSessionKey | null,
|
|
) {
|
|
return resolveStorePath(cfg.session?.store, {
|
|
agentId: parsed?.agentId,
|
|
});
|
|
}
|
|
|
|
export function resolveSessionEntryForKey(params: {
|
|
cfg: OpenClawConfig;
|
|
key: string;
|
|
cache: Map<string, Record<string, SessionEntry>>;
|
|
}): SessionEntryResolution {
|
|
const parsed = parseAgentSessionKey(params.key);
|
|
const storePath = resolveStorePathForKey(params.cfg, params.key, parsed);
|
|
let store = params.cache.get(storePath);
|
|
if (!store) {
|
|
store = loadSessionStore(storePath);
|
|
params.cache.set(storePath, store);
|
|
}
|
|
return {
|
|
storePath,
|
|
entry: store[params.key],
|
|
};
|
|
}
|
|
|
|
export function buildLatestSubagentRunIndex(runs: Map<string, SubagentRunRecord>) {
|
|
const latestByChildSessionKey = new Map<string, SubagentRunRecord>();
|
|
for (const entry of runs.values()) {
|
|
const childSessionKey = entry.childSessionKey?.trim();
|
|
if (!childSessionKey) {
|
|
continue;
|
|
}
|
|
const existing = latestByChildSessionKey.get(childSessionKey);
|
|
if (!existing || entry.createdAt > existing.createdAt) {
|
|
latestByChildSessionKey.set(childSessionKey, entry);
|
|
}
|
|
}
|
|
|
|
const childSessionsByController = new Map<string, string[]>();
|
|
for (const [childSessionKey, entry] of latestByChildSessionKey.entries()) {
|
|
const controllerSessionKey =
|
|
entry.controllerSessionKey?.trim() || entry.requesterSessionKey?.trim();
|
|
if (!controllerSessionKey) {
|
|
continue;
|
|
}
|
|
const existing = childSessionsByController.get(controllerSessionKey);
|
|
if (existing) {
|
|
existing.push(childSessionKey);
|
|
continue;
|
|
}
|
|
childSessionsByController.set(controllerSessionKey, [childSessionKey]);
|
|
}
|
|
for (const childSessions of childSessionsByController.values()) {
|
|
childSessions.sort();
|
|
}
|
|
|
|
return {
|
|
latestByChildSessionKey,
|
|
childSessionsByController,
|
|
};
|
|
}
|
|
|
|
export function createPendingDescendantCounter(runsSnapshot?: Map<string, SubagentRunRecord>) {
|
|
const pendingDescendantCache = new Map<string, number>();
|
|
return (sessionKey: string) => {
|
|
if (pendingDescendantCache.has(sessionKey)) {
|
|
return pendingDescendantCache.get(sessionKey) ?? 0;
|
|
}
|
|
const snapshot = runsSnapshot ?? getSubagentRunsSnapshotForRead(subagentRuns);
|
|
const pending = Math.max(0, countPendingDescendantRunsFromRuns(snapshot, sessionKey));
|
|
pendingDescendantCache.set(sessionKey, pending);
|
|
return pending;
|
|
};
|
|
}
|
|
|
|
export function isActiveSubagentRun(
|
|
entry: SubagentRunRecord,
|
|
pendingDescendantCount: (sessionKey: string) => number,
|
|
) {
|
|
return !entry.endedAt || pendingDescendantCount(entry.childSessionKey) > 0;
|
|
}
|
|
|
|
function resolveRunStatus(entry: SubagentRunRecord, options?: { pendingDescendants?: number }) {
|
|
const pendingDescendants = Math.max(0, options?.pendingDescendants ?? 0);
|
|
if (pendingDescendants > 0) {
|
|
const childLabel = pendingDescendants === 1 ? "child" : "children";
|
|
return `active (waiting on ${pendingDescendants} ${childLabel})`;
|
|
}
|
|
if (!entry.endedAt) {
|
|
return "running";
|
|
}
|
|
const status = entry.outcome?.status ?? "done";
|
|
if (status === "ok") {
|
|
return "done";
|
|
}
|
|
if (status === "error") {
|
|
return "failed";
|
|
}
|
|
return status;
|
|
}
|
|
|
|
function resolveModelRef(entry?: SessionEntry, fallbackModel?: string) {
|
|
return resolveModelDisplayRef({
|
|
runtimeProvider: entry?.modelProvider,
|
|
runtimeModel: entry?.model,
|
|
overrideProvider: entry?.providerOverride,
|
|
overrideModel: entry?.modelOverride,
|
|
fallbackModel,
|
|
});
|
|
}
|
|
|
|
function resolveModelDisplay(entry?: SessionEntry, fallbackModel?: string) {
|
|
return resolveModelDisplayName({
|
|
runtimeProvider: entry?.modelProvider,
|
|
runtimeModel: entry?.model,
|
|
overrideProvider: entry?.providerOverride,
|
|
overrideModel: entry?.modelOverride,
|
|
fallbackModel,
|
|
});
|
|
}
|
|
|
|
function buildListText(params: {
|
|
active: Array<{ line: string }>;
|
|
recent: Array<{ line: string }>;
|
|
recentMinutes: number;
|
|
}) {
|
|
const lines: string[] = [];
|
|
lines.push("active subagents:");
|
|
if (params.active.length === 0) {
|
|
lines.push("(none)");
|
|
} else {
|
|
lines.push(...params.active.map((entry) => entry.line));
|
|
}
|
|
lines.push("");
|
|
lines.push(`recent (last ${params.recentMinutes}m):`);
|
|
if (params.recent.length === 0) {
|
|
lines.push("(none)");
|
|
} else {
|
|
lines.push(...params.recent.map((entry) => entry.line));
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export function buildSubagentList(params: {
|
|
cfg: OpenClawConfig;
|
|
runs: SubagentRunRecord[];
|
|
recentMinutes: number;
|
|
taskMaxChars?: number;
|
|
}): BuiltSubagentList {
|
|
const now = Date.now();
|
|
const recentCutoff = now - params.recentMinutes * 60_000;
|
|
const dedupedRuns: SubagentRunRecord[] = [];
|
|
const seenChildSessionKeys = new Set<string>();
|
|
for (const entry of sortSubagentRuns(params.runs)) {
|
|
if (seenChildSessionKeys.has(entry.childSessionKey)) {
|
|
continue;
|
|
}
|
|
seenChildSessionKeys.add(entry.childSessionKey);
|
|
dedupedRuns.push(entry);
|
|
}
|
|
const cache = new Map<string, Record<string, SessionEntry>>();
|
|
const snapshot = getSubagentRunsSnapshotForRead(subagentRuns);
|
|
const { childSessionsByController } = buildLatestSubagentRunIndex(snapshot);
|
|
const pendingDescendantCount = createPendingDescendantCounter(snapshot);
|
|
let index = 1;
|
|
const buildListEntry = (entry: SubagentRunRecord, runtimeMs: number) => {
|
|
const sessionEntry = resolveSessionEntryForKey({
|
|
cfg: params.cfg,
|
|
key: entry.childSessionKey,
|
|
cache,
|
|
}).entry;
|
|
const totalTokens = resolveTotalTokens(sessionEntry);
|
|
const usageText = formatTokenUsageDisplay(sessionEntry);
|
|
const pendingDescendants = pendingDescendantCount(entry.childSessionKey);
|
|
const status = resolveRunStatus(entry, {
|
|
pendingDescendants,
|
|
});
|
|
const childSessions = childSessionsByController.get(entry.childSessionKey) ?? [];
|
|
const runtime = formatDurationCompact(runtimeMs) ?? "n/a";
|
|
const label = truncateLine(resolveSubagentLabel(entry), 48);
|
|
const task = truncateLine(entry.task.trim(), params.taskMaxChars ?? 72);
|
|
const line = `${index}. ${label} (${resolveModelDisplay(sessionEntry, entry.model)}, ${runtime}${usageText ? `, ${usageText}` : ""}) ${status}${normalizeLowercaseStringOrEmpty(task) !== normalizeLowercaseStringOrEmpty(label) ? ` - ${task}` : ""}`;
|
|
const view: SubagentListItem = {
|
|
index,
|
|
line,
|
|
runId: entry.runId,
|
|
sessionKey: entry.childSessionKey,
|
|
label,
|
|
task,
|
|
status,
|
|
pendingDescendants,
|
|
runtime,
|
|
runtimeMs,
|
|
...(childSessions.length > 0 ? { childSessions } : {}),
|
|
model: resolveModelRef(sessionEntry, entry.model),
|
|
totalTokens,
|
|
startedAt: getSubagentSessionStartedAt(entry),
|
|
...(entry.endedAt ? { endedAt: entry.endedAt } : {}),
|
|
};
|
|
index += 1;
|
|
return view;
|
|
};
|
|
const active = dedupedRuns
|
|
.filter((entry) => isActiveSubagentRun(entry, pendingDescendantCount))
|
|
.map((entry) => buildListEntry(entry, getSubagentSessionRuntimeMs(entry, now) ?? 0));
|
|
const recent = dedupedRuns
|
|
.filter(
|
|
(entry) =>
|
|
!isActiveSubagentRun(entry, pendingDescendantCount) &&
|
|
!!entry.endedAt &&
|
|
(entry.endedAt ?? 0) >= recentCutoff,
|
|
)
|
|
.map((entry) =>
|
|
buildListEntry(entry, getSubagentSessionRuntimeMs(entry, entry.endedAt ?? now) ?? 0),
|
|
);
|
|
return {
|
|
total: dedupedRuns.length,
|
|
active,
|
|
recent,
|
|
text: buildListText({ active, recent, recentMinutes: params.recentMinutes }),
|
|
};
|
|
}
|