mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 00:20:22 +00:00
feat: expose subagent session metadata in sessions list
This commit is contained in:
@@ -1468,6 +1468,24 @@ export function listDescendantRunsForRequester(rootSessionKey: string): Subagent
|
||||
);
|
||||
}
|
||||
|
||||
export function getSubagentRunByChildSessionKey(childSessionKey: string): SubagentRunRecord | null {
|
||||
const runIds = findRunIdsByChildSessionKeyFromRuns(subagentRuns, childSessionKey);
|
||||
if (runIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let latest: SubagentRunRecord | null = null;
|
||||
for (const runId of runIds) {
|
||||
const entry = subagentRuns.get(runId);
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
if (!latest || entry.createdAt > latest.createdAt) {
|
||||
latest = entry;
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
export function initSubagentRegistry() {
|
||||
restoreSubagentRunsOnce();
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ export type SessionListDeliveryContext = {
|
||||
accountId?: string;
|
||||
};
|
||||
|
||||
export type SessionRunStatus = "running" | "done" | "failed" | "killed";
|
||||
|
||||
export type SessionListRow = {
|
||||
key: string;
|
||||
kind: SessionKind;
|
||||
@@ -56,6 +58,11 @@ export type SessionListRow = {
|
||||
model?: string;
|
||||
contextTokens?: number | null;
|
||||
totalTokens?: number | null;
|
||||
status?: SessionRunStatus;
|
||||
startedAt?: number;
|
||||
endedAt?: number;
|
||||
runtimeMs?: number;
|
||||
childSessions?: string[];
|
||||
thinkingLevel?: string;
|
||||
verboseLevel?: string;
|
||||
systemSent?: boolean;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import {
|
||||
addSubagentRunForTests,
|
||||
resetSubagentRegistryForTests,
|
||||
} from "../agents/subagent-registry.js";
|
||||
import { clearConfigCache, writeConfigFile } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
@@ -82,6 +86,10 @@ function createLegacyRuntimeStore(model: string): Record<string, SessionEntry> {
|
||||
}
|
||||
|
||||
describe("gateway session utils", () => {
|
||||
afterEach(() => {
|
||||
resetSubagentRegistryForTests({ persist: false });
|
||||
});
|
||||
|
||||
test("capArrayByJsonBytes trims from the front", () => {
|
||||
const res = capArrayByJsonBytes(["a", "b", "c"], 10);
|
||||
expect(res.items).toEqual(["b", "c"]);
|
||||
@@ -830,6 +838,111 @@ describe("listSessionsFromStore search", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("listSessionsFromStore subagent metadata", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "main" },
|
||||
agents: { list: [{ id: "main", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
test("includes subagent status timing and direct child session keys", () => {
|
||||
const now = Date.now();
|
||||
const store: Record<string, SessionEntry> = {
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: now,
|
||||
} as SessionEntry,
|
||||
"agent:main:subagent:parent": {
|
||||
sessionId: "sess-parent",
|
||||
updatedAt: now - 2_000,
|
||||
spawnedBy: "agent:main:main",
|
||||
} as SessionEntry,
|
||||
"agent:main:subagent:child": {
|
||||
sessionId: "sess-child",
|
||||
updatedAt: now - 1_000,
|
||||
spawnedBy: "agent:main:subagent:parent",
|
||||
} as SessionEntry,
|
||||
"agent:main:subagent:failed": {
|
||||
sessionId: "sess-failed",
|
||||
updatedAt: now - 500,
|
||||
spawnedBy: "agent:main:main",
|
||||
} as SessionEntry,
|
||||
};
|
||||
|
||||
addSubagentRunForTests({
|
||||
runId: "run-parent",
|
||||
childSessionKey: "agent:main:subagent:parent",
|
||||
controllerSessionKey: "agent:main:main",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "parent task",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 10_000,
|
||||
startedAt: now - 9_000,
|
||||
model: "openai/gpt-5.4",
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-child",
|
||||
childSessionKey: "agent:main:subagent:child",
|
||||
controllerSessionKey: "agent:main:subagent:parent",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "child task",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 8_000,
|
||||
startedAt: now - 7_500,
|
||||
endedAt: now - 2_500,
|
||||
outcome: { status: "ok" },
|
||||
model: "openai/gpt-5.4",
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-failed",
|
||||
childSessionKey: "agent:main:subagent:failed",
|
||||
controllerSessionKey: "agent:main:main",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "failed task",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 6_000,
|
||||
startedAt: now - 5_500,
|
||||
endedAt: now - 500,
|
||||
outcome: { status: "error", error: "boom" },
|
||||
model: "openai/gpt-5.4",
|
||||
});
|
||||
|
||||
const result = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store,
|
||||
opts: {},
|
||||
});
|
||||
|
||||
const main = result.sessions.find((session) => session.key === "agent:main:main");
|
||||
expect(main?.childSessions).toEqual([
|
||||
"agent:main:subagent:parent",
|
||||
"agent:main:subagent:failed",
|
||||
]);
|
||||
expect(main?.status).toBeUndefined();
|
||||
|
||||
const parent = result.sessions.find((session) => session.key === "agent:main:subagent:parent");
|
||||
expect(parent?.status).toBe("running");
|
||||
expect(parent?.startedAt).toBe(now - 9_000);
|
||||
expect(parent?.endedAt).toBeUndefined();
|
||||
expect(parent?.runtimeMs).toBeGreaterThanOrEqual(9_000);
|
||||
expect(parent?.childSessions).toEqual(["agent:main:subagent:child"]);
|
||||
|
||||
const child = result.sessions.find((session) => session.key === "agent:main:subagent:child");
|
||||
expect(child?.status).toBe("done");
|
||||
expect(child?.startedAt).toBe(now - 7_500);
|
||||
expect(child?.endedAt).toBe(now - 2_500);
|
||||
expect(child?.runtimeMs).toBe(5_000);
|
||||
expect(child?.childSessions).toBeUndefined();
|
||||
|
||||
const failed = result.sessions.find((session) => session.key === "agent:main:subagent:failed");
|
||||
expect(failed?.status).toBe("failed");
|
||||
expect(failed?.runtimeMs).toBe(5_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadCombinedSessionStoreForGateway includes disk-only agents (#32804)", () => {
|
||||
test("ACP agent sessions are visible even when agents.list is configured", async () => {
|
||||
await withStateDirEnv("openclaw-acp-vis-", async ({ stateDir }) => {
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
resolveConfiguredModelRef,
|
||||
resolveDefaultModelForAgent,
|
||||
} from "../agents/model-selection.js";
|
||||
import {
|
||||
getSubagentRunByChildSessionKey,
|
||||
listSubagentRunsForController,
|
||||
} from "../agents/subagent-registry.js";
|
||||
import { type OpenClawConfig, loadConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import {
|
||||
@@ -177,6 +181,49 @@ export function deriveSessionTitle(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveSessionRunStatus(
|
||||
run: {
|
||||
endedAt?: number;
|
||||
outcome?: { status?: string };
|
||||
} | null,
|
||||
): "running" | "done" | "failed" | "killed" | undefined {
|
||||
if (!run) {
|
||||
return undefined;
|
||||
}
|
||||
if (!run.endedAt) {
|
||||
return "running";
|
||||
}
|
||||
const status = run.outcome?.status;
|
||||
if (status === "error") {
|
||||
return "failed";
|
||||
}
|
||||
if (status === "killed") {
|
||||
return "killed";
|
||||
}
|
||||
return "done";
|
||||
}
|
||||
|
||||
function resolveSessionRuntimeMs(
|
||||
run: { startedAt?: number; endedAt?: number } | null,
|
||||
now: number,
|
||||
) {
|
||||
if (!run?.startedAt) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.max(0, (run.endedAt ?? now) - run.startedAt);
|
||||
}
|
||||
|
||||
function resolveChildSessionKeys(controllerSessionKey: string): string[] | undefined {
|
||||
const childSessions = Array.from(
|
||||
new Set(
|
||||
listSubagentRunsForController(controllerSessionKey)
|
||||
.map((entry) => entry.childSessionKey)
|
||||
.filter((value) => typeof value === "string" && value.trim().length > 0),
|
||||
),
|
||||
);
|
||||
return childSessions.length > 0 ? childSessions : undefined;
|
||||
}
|
||||
|
||||
export function loadSessionEntry(sessionKey: string) {
|
||||
const cfg = loadConfig();
|
||||
const canonicalKey = resolveSessionStoreKey({ cfg, sessionKey });
|
||||
@@ -911,6 +958,8 @@ export function listSessionsFromStore(params: {
|
||||
const resolvedModel = resolveSessionModelIdentityRef(cfg, entry, sessionAgentId);
|
||||
const modelProvider = resolvedModel.provider;
|
||||
const model = resolvedModel.model ?? DEFAULT_MODEL;
|
||||
const subagentRun = getSubagentRunByChildSessionKey(key);
|
||||
const childSessions = resolveChildSessionKeys(key);
|
||||
return {
|
||||
key,
|
||||
spawnedBy: entry?.spawnedBy,
|
||||
@@ -938,6 +987,11 @@ export function listSessionsFromStore(params: {
|
||||
outputTokens: entry?.outputTokens,
|
||||
totalTokens: total,
|
||||
totalTokensFresh,
|
||||
status: resolveSessionRunStatus(subagentRun),
|
||||
startedAt: subagentRun?.startedAt,
|
||||
endedAt: subagentRun?.endedAt,
|
||||
runtimeMs: resolveSessionRuntimeMs(subagentRun, now),
|
||||
childSessions,
|
||||
responseUsage: entry?.responseUsage,
|
||||
modelProvider,
|
||||
model,
|
||||
|
||||
@@ -13,6 +13,8 @@ export type GatewaySessionsDefaults = {
|
||||
contextTokens: number | null;
|
||||
};
|
||||
|
||||
export type SessionRunStatus = "running" | "done" | "failed" | "killed";
|
||||
|
||||
export type GatewaySessionRow = {
|
||||
key: string;
|
||||
spawnedBy?: string;
|
||||
@@ -41,6 +43,11 @@ export type GatewaySessionRow = {
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
totalTokensFresh?: boolean;
|
||||
status?: SessionRunStatus;
|
||||
startedAt?: number;
|
||||
endedAt?: number;
|
||||
runtimeMs?: number;
|
||||
childSessions?: string[];
|
||||
responseUsage?: "on" | "off" | "tokens" | "full";
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
|
||||
Reference in New Issue
Block a user