mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(agents): use canonical subagent terminal status across views (#116991)
Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
committed by
GitHub
parent
47c39b185c
commit
f4f990a91b
@@ -6,6 +6,7 @@ import path from "node:path";
|
|||||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { replaceSessionEntry } from "../config/sessions/session-accessor.js";
|
import { replaceSessionEntry } from "../config/sessions/session-accessor.js";
|
||||||
|
import { SUBAGENT_ENDED_REASON_KILLED } from "./subagent-lifecycle-events.js";
|
||||||
import { buildSubagentList } from "./subagent-list.js";
|
import { buildSubagentList } from "./subagent-list.js";
|
||||||
import {
|
import {
|
||||||
addSubagentRunForTests,
|
addSubagentRunForTests,
|
||||||
@@ -108,6 +109,64 @@ describe("buildSubagentList", () => {
|
|||||||
expect(list.active[0]?.line).toContain("review_subagents: Review worker");
|
expect(list.active[0]?.line).toContain("review_subagents: Review worker");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: "a killed run with a provider failure",
|
||||||
|
endedReason: SUBAGENT_ENDED_REASON_KILLED,
|
||||||
|
outcome: { status: "error", error: "agent run aborted" } as const,
|
||||||
|
expectedStatus: "killed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a killed run with an earlier successful provider outcome",
|
||||||
|
endedReason: SUBAGENT_ENDED_REASON_KILLED,
|
||||||
|
outcome: { status: "ok" } as const,
|
||||||
|
expectedStatus: "killed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a failed run",
|
||||||
|
outcome: { status: "error", error: "provider rejected the request" } as const,
|
||||||
|
expectedStatus: "failed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a timed-out run",
|
||||||
|
outcome: { status: "timeout" } as const,
|
||||||
|
expectedStatus: "timeout",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a completed run",
|
||||||
|
outcome: { status: "ok" } as const,
|
||||||
|
expectedStatus: "done",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"projects the canonical terminal status for $name",
|
||||||
|
({ endedReason, outcome, expectedStatus }) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const run = {
|
||||||
|
runId: `run-status-${expectedStatus}`,
|
||||||
|
childSessionKey: `agent:main:subagent:status-${expectedStatus}`,
|
||||||
|
requesterSessionKey: "agent:main:main",
|
||||||
|
requesterDisplayKey: "main",
|
||||||
|
task: "report the actual child outcome",
|
||||||
|
cleanup: "keep",
|
||||||
|
createdAt: now - 2_000,
|
||||||
|
startedAt: now - 2_000,
|
||||||
|
endedAt: now - 1_000,
|
||||||
|
...(endedReason ? { endedReason } : {}),
|
||||||
|
outcome,
|
||||||
|
} satisfies SubagentRunRecord;
|
||||||
|
addSubagentRunForTests(run);
|
||||||
|
|
||||||
|
const list = buildSubagentList({
|
||||||
|
cfg: {} as OpenClawConfig,
|
||||||
|
runs: [run],
|
||||||
|
recentMinutes: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(list.recent[0]?.status).toBe(expectedStatus);
|
||||||
|
expect(list.recent[0]?.line).toContain(` ${expectedStatus}`);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("keeps ended orchestrators active while descendants remain pending", () => {
|
it("keeps ended orchestrators active while descendants remain pending", () => {
|
||||||
// Parent orchestrators can finish their own turn before child workers do;
|
// Parent orchestrators can finish their own turn before child workers do;
|
||||||
// list output should keep them active until descendants settle.
|
// list output should keep them active until descendants settle.
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ import {
|
|||||||
import { getSubagentRunsSnapshotForRead } from "./subagent-registry-state.js";
|
import { getSubagentRunsSnapshotForRead } from "./subagent-registry-state.js";
|
||||||
import type { SubagentRunRecord } from "./subagent-registry.types.js";
|
import type { SubagentRunRecord } from "./subagent-registry.types.js";
|
||||||
import {
|
import {
|
||||||
hasSubagentRunEnded,
|
|
||||||
isLiveUnendedSubagentRun,
|
isLiveUnendedSubagentRun,
|
||||||
shouldKeepSubagentRunChildLink,
|
shouldKeepSubagentRunChildLink,
|
||||||
} from "./subagent-run-liveness.js";
|
} from "./subagent-run-liveness.js";
|
||||||
|
import { resolveSubagentDisplayStatus } from "./subagent-session-metrics.js";
|
||||||
|
|
||||||
type SubagentListItem = {
|
type SubagentListItem = {
|
||||||
index: number;
|
index: number;
|
||||||
@@ -144,25 +144,6 @@ function isActiveSubagentRun(
|
|||||||
return isLiveUnendedSubagentRun(entry) || pendingDescendantCount(entry.childSessionKey) > 0;
|
return isLiveUnendedSubagentRun(entry) || 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 (!hasSubagentRunEnded(entry)) {
|
|
||||||
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) {
|
function resolveModelRef(entry?: SessionEntry, fallbackModel?: string) {
|
||||||
return resolveModelDisplayRef({
|
return resolveModelDisplayRef({
|
||||||
runtimeProvider: entry?.modelProvider,
|
runtimeProvider: entry?.modelProvider,
|
||||||
@@ -240,9 +221,7 @@ export function buildSubagentList(params: {
|
|||||||
const totalTokens = resolveTotalTokens(sessionEntry);
|
const totalTokens = resolveTotalTokens(sessionEntry);
|
||||||
const usageText = formatTokenUsageDisplay(sessionEntry);
|
const usageText = formatTokenUsageDisplay(sessionEntry);
|
||||||
const pendingDescendants = pendingDescendantCount(entry.childSessionKey);
|
const pendingDescendants = pendingDescendantCount(entry.childSessionKey);
|
||||||
const status = resolveRunStatus(entry, {
|
const status = resolveSubagentDisplayStatus(entry, pendingDescendants);
|
||||||
pendingDescendants,
|
|
||||||
});
|
|
||||||
const childSessions = childSessionsByController.get(entry.childSessionKey) ?? [];
|
const childSessions = childSessionsByController.get(entry.childSessionKey) ?? [];
|
||||||
const runtime = formatDurationCompact(runtimeMs) ?? "n/a";
|
const runtime = formatDurationCompact(runtimeMs) ?? "n/a";
|
||||||
const label = truncateLine(resolveSubagentLabel(entry), 48);
|
const label = truncateLine(resolveSubagentLabel(entry), 48);
|
||||||
|
|||||||
@@ -76,3 +76,16 @@ export function resolveSubagentSessionStatus(
|
|||||||
}
|
}
|
||||||
return "done";
|
return "done";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Formats the authoritative run status while preserving unfinished descendants. */
|
||||||
|
export function resolveSubagentDisplayStatus(
|
||||||
|
entry: Pick<SubagentRunRecord, "endedAt" | "endedReason" | "outcome">,
|
||||||
|
pendingDescendants = 0,
|
||||||
|
): string {
|
||||||
|
const pending = Math.max(0, pendingDescendants);
|
||||||
|
if (pending > 0) {
|
||||||
|
const childLabel = pending === 1 ? "child" : "children";
|
||||||
|
return `active (waiting on ${pending} ${childLabel})`;
|
||||||
|
}
|
||||||
|
return resolveSubagentSessionStatus(entry) ?? "done";
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import type { TaskRecord, TaskRuntime, TaskStatus } from "../../tasks/task-registry.types.js";
|
import type { TaskRecord, TaskRuntime, TaskStatus } from "../../tasks/task-registry.types.js";
|
||||||
import { TASK_STATUS_DETAIL_MAX_CHARS } from "../../tasks/task-status.js";
|
import { TASK_STATUS_DETAIL_MAX_CHARS } from "../../tasks/task-status.js";
|
||||||
|
import { SUBAGENT_ENDED_REASON_KILLED } from "../subagent-lifecycle-events.js";
|
||||||
|
import {
|
||||||
|
addSubagentRunForTests,
|
||||||
|
resetSubagentRegistryForTests,
|
||||||
|
} from "../subagent-registry.test-helpers.js";
|
||||||
|
import type { SubagentRunRecord } from "../subagent-registry.types.js";
|
||||||
import { createSubagentsTool } from "./subagents-tool.js";
|
import { createSubagentsTool } from "./subagents-tool.js";
|
||||||
|
|
||||||
function task(params: {
|
function task(params: {
|
||||||
@@ -47,6 +53,44 @@ describe("subagents tool", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reports a killed subagent truthfully through the actual list tool", async () => {
|
||||||
|
resetSubagentRegistryForTests();
|
||||||
|
const now = Date.now();
|
||||||
|
const run = {
|
||||||
|
runId: "run-tool-killed",
|
||||||
|
childSessionKey: "agent:main:subagent:tool-killed",
|
||||||
|
controllerSessionKey: "agent:main:main",
|
||||||
|
requesterSessionKey: "agent:main:main",
|
||||||
|
requesterDisplayKey: "main",
|
||||||
|
task: "report the killed child",
|
||||||
|
cleanup: "keep",
|
||||||
|
createdAt: now - 2_000,
|
||||||
|
startedAt: now - 2_000,
|
||||||
|
endedAt: now - 1_000,
|
||||||
|
endedReason: SUBAGENT_ENDED_REASON_KILLED,
|
||||||
|
outcome: { status: "error", error: "agent run aborted" },
|
||||||
|
} satisfies SubagentRunRecord;
|
||||||
|
addSubagentRunForTests(run);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tool = createSubagentsTool({
|
||||||
|
agentSessionKey: "agent:main:main",
|
||||||
|
config: {},
|
||||||
|
listTasks: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await tool.execute("list-killed", { action: "list" });
|
||||||
|
|
||||||
|
expect(result.details).toMatchObject({
|
||||||
|
status: "ok",
|
||||||
|
recent: [expect.objectContaining({ runId: run.runId, status: "killed" })],
|
||||||
|
});
|
||||||
|
expect((result.details as { text: string }).text).toContain(" killed");
|
||||||
|
} finally {
|
||||||
|
resetSubagentRegistryForTests();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("lists cross-runtime tasks in the caller session tree", async () => {
|
it("lists cross-runtime tasks in the caller session tree", async () => {
|
||||||
const tasks = [
|
const tasks = [
|
||||||
task({
|
task({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { SUBAGENT_ENDED_REASON_KILLED } from "../../agents/subagent-lifecycle-events.js";
|
||||||
import { subagentRuns } from "../../agents/subagent-registry-memory.js";
|
import { subagentRuns } from "../../agents/subagent-registry-memory.js";
|
||||||
import {
|
import {
|
||||||
countPendingDescendantRunsFromRuns,
|
countPendingDescendantRunsFromRuns,
|
||||||
@@ -26,6 +27,7 @@ import type { ReplyPayload } from "../types.js";
|
|||||||
import { buildSubagentsStatusLine } from "./commands-status-subagents.js";
|
import { buildSubagentsStatusLine } from "./commands-status-subagents.js";
|
||||||
import { extractMessageText } from "./commands-subagents-text.js";
|
import { extractMessageText } from "./commands-subagents-text.js";
|
||||||
import { handleSubagentsInfoAction } from "./commands-subagents/action-info.js";
|
import { handleSubagentsInfoAction } from "./commands-subagents/action-info.js";
|
||||||
|
import { handleSubagentsListAction } from "./commands-subagents/action-list.js";
|
||||||
import { handleSubagentsLogAction } from "./commands-subagents/action-log.js";
|
import { handleSubagentsLogAction } from "./commands-subagents/action-log.js";
|
||||||
import { resolveFocusTargetSession } from "./commands-subagents/shared.js";
|
import { resolveFocusTargetSession } from "./commands-subagents/shared.js";
|
||||||
import {
|
import {
|
||||||
@@ -233,6 +235,62 @@ describe("subagents info", () => {
|
|||||||
expect(text).toContain("Task summary: Completed the requested task");
|
expect(text).toContain("Task summary: Completed the requested task");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: "a killed run despite its provider error",
|
||||||
|
endedReason: SUBAGENT_ENDED_REASON_KILLED,
|
||||||
|
outcome: { status: "error", error: "agent run aborted" } as const,
|
||||||
|
expectedStatus: "killed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a killed run despite its earlier successful result",
|
||||||
|
endedReason: SUBAGENT_ENDED_REASON_KILLED,
|
||||||
|
outcome: { status: "ok" } as const,
|
||||||
|
expectedStatus: "killed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a failed run",
|
||||||
|
outcome: { status: "error", error: "provider rejected the request" } as const,
|
||||||
|
expectedStatus: "failed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a timed-out run",
|
||||||
|
outcome: { status: "timeout" } as const,
|
||||||
|
expectedStatus: "timeout",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"keeps /subagents info and list aligned for $name",
|
||||||
|
({ endedReason, outcome, expectedStatus }) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const run = {
|
||||||
|
runId: `commands-subagents-status-${expectedStatus}`,
|
||||||
|
childSessionKey: `agent:main:subagent:commands-status-${expectedStatus}`,
|
||||||
|
requesterSessionKey: "agent:main:main",
|
||||||
|
requesterDisplayKey: "main",
|
||||||
|
task: "report the actual child outcome",
|
||||||
|
cleanup: "keep",
|
||||||
|
createdAt: now - 2_000,
|
||||||
|
startedAt: now - 2_000,
|
||||||
|
endedAt: now - 1_000,
|
||||||
|
...(endedReason ? { endedReason } : {}),
|
||||||
|
outcome,
|
||||||
|
} satisfies SubagentRunRecord;
|
||||||
|
addSubagentRunForTests(run);
|
||||||
|
const context = buildInfoContext({
|
||||||
|
cfg: buildCommandTestConfig(),
|
||||||
|
runs: [run],
|
||||||
|
restTokens: ["1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requireReplyText(handleSubagentsInfoAction(context).reply)).toContain(
|
||||||
|
`Status: ${expectedStatus}`,
|
||||||
|
);
|
||||||
|
expect(requireReplyText(handleSubagentsListAction(context).reply)).toContain(
|
||||||
|
` ${expectedStatus}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("omits Date-invalid subagent timestamps", () => {
|
it("omits Date-invalid subagent timestamps", () => {
|
||||||
const runId = "commands-subagents-info-invalid-date-run";
|
const runId = "commands-subagents-info-invalid-date-run";
|
||||||
const childSessionKey = "agent:main:subagent:commands-info-invalid-date";
|
const childSessionKey = "agent:main:subagent:commands-info-invalid-date";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coer
|
|||||||
import { subagentRuns } from "../../../agents/subagent-registry-memory.js";
|
import { subagentRuns } from "../../../agents/subagent-registry-memory.js";
|
||||||
import { countPendingDescendantRunsFromRuns } from "../../../agents/subagent-registry-queries.js";
|
import { countPendingDescendantRunsFromRuns } from "../../../agents/subagent-registry-queries.js";
|
||||||
import { getSubagentRunsSnapshotForRead } from "../../../agents/subagent-registry-state.js";
|
import { getSubagentRunsSnapshotForRead } from "../../../agents/subagent-registry-state.js";
|
||||||
|
import { resolveSubagentDisplayStatus } from "../../../agents/subagent-session-metrics.js";
|
||||||
import { resolveStorePath } from "../../../config/sessions/paths.js";
|
import { resolveStorePath } from "../../../config/sessions/paths.js";
|
||||||
import { loadSessionEntryReadOnly } from "../../../config/sessions/session-accessor.js";
|
import { loadSessionEntryReadOnly } from "../../../config/sessions/session-accessor.js";
|
||||||
import { formatTimeAgo } from "../../../infra/format-time/format-relative.ts";
|
import { formatTimeAgo } from "../../../infra/format-time/format-relative.ts";
|
||||||
@@ -11,7 +12,7 @@ import { formatDurationCompact } from "../../../shared/subagents-format.js";
|
|||||||
import { findTaskByRunIdForOwner } from "../../../tasks/task-owner-access.js";
|
import { findTaskByRunIdForOwner } from "../../../tasks/task-owner-access.js";
|
||||||
import { sanitizeTaskStatusText } from "../../../tasks/task-status.js";
|
import { sanitizeTaskStatusText } from "../../../tasks/task-status.js";
|
||||||
import type { CommandHandlerResult } from "../commands-types.js";
|
import type { CommandHandlerResult } from "../commands-types.js";
|
||||||
import { formatRunLabel, formatRunStatus } from "../subagents-utils.js";
|
import { formatRunLabel } from "../subagents-utils.js";
|
||||||
import {
|
import {
|
||||||
resolveSubagentEntryForToken,
|
resolveSubagentEntryForToken,
|
||||||
stopWithText,
|
stopWithText,
|
||||||
@@ -29,19 +30,6 @@ function formatTimestampWithAge(valueMs?: number) {
|
|||||||
return `${timestamp} (${formatTimeAgo(Date.now() - valueMs, { fallback: "n/a" })})`;
|
return `${timestamp} (${formatTimeAgo(Date.now() - valueMs, { fallback: "n/a" })})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDisplayStatus(
|
|
||||||
entry: SubagentsCommandContext["runs"][number],
|
|
||||||
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})`;
|
|
||||||
}
|
|
||||||
const status = formatRunStatus(entry);
|
|
||||||
return status === "error" ? "failed" : status;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSubagentSessionEntry(params: SubagentsCommandContext["params"], childKey: string) {
|
function loadSubagentSessionEntry(params: SubagentsCommandContext["params"], childKey: string) {
|
||||||
const parsed = parseAgentSessionKey(childKey);
|
const parsed = parseAgentSessionKey(childKey);
|
||||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||||
@@ -91,12 +79,13 @@ export function handleSubagentsInfoAction(ctx: SubagentsCommandContext): Command
|
|||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
"ℹ️ Subagent info",
|
"ℹ️ Subagent info",
|
||||||
`Status: ${resolveDisplayStatus(run, {
|
`Status: ${resolveSubagentDisplayStatus(
|
||||||
pendingDescendants: countPendingDescendantRunsFromRuns(
|
run,
|
||||||
|
countPendingDescendantRunsFromRuns(
|
||||||
getSubagentRunsSnapshotForRead(subagentRuns),
|
getSubagentRunsSnapshotForRead(subagentRuns),
|
||||||
run.childSessionKey,
|
run.childSessionKey,
|
||||||
),
|
),
|
||||||
})}`,
|
)}`,
|
||||||
`Label: ${formatRunLabel(run)}`,
|
`Label: ${formatRunLabel(run)}`,
|
||||||
`Task: ${taskText}`,
|
`Task: ${taskText}`,
|
||||||
`Run: ${run.runId}`,
|
`Run: ${run.runId}`,
|
||||||
|
|||||||
@@ -14,12 +14,7 @@ import {
|
|||||||
import type { TemplateContext } from "../templating.js";
|
import type { TemplateContext } from "../templating.js";
|
||||||
import { buildThreadingToolContext } from "./agent-runner-utils.js";
|
import { buildThreadingToolContext } from "./agent-runner-utils.js";
|
||||||
import { applyReplyThreading } from "./reply-payloads.js";
|
import { applyReplyThreading } from "./reply-payloads.js";
|
||||||
import {
|
import { formatRunLabel, resolveSubagentLabel, sortSubagentRuns } from "./subagents-utils.js";
|
||||||
formatRunLabel,
|
|
||||||
formatRunStatus,
|
|
||||||
resolveSubagentLabel,
|
|
||||||
sortSubagentRuns,
|
|
||||||
} from "./subagents-utils.js";
|
|
||||||
|
|
||||||
function createSlackThreadingPlugin(): ChannelPlugin {
|
function createSlackThreadingPlugin(): ChannelPlugin {
|
||||||
return {
|
return {
|
||||||
@@ -484,14 +479,6 @@ describe("subagents utils", () => {
|
|||||||
expect(sorted.map((run) => run.runId)).toEqual(["run-2", "run-1", "run-3"]);
|
expect(sorted.map((run) => run.runId)).toEqual(["run-2", "run-1", "run-3"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("formats run status from outcome and timestamps", () => {
|
|
||||||
expect(formatRunStatus({ ...baseRun })).toBe("running");
|
|
||||||
expect(formatRunStatus({ ...baseRun, endedAt: 2000, outcome: { status: "ok" } })).toBe("done");
|
|
||||||
expect(formatRunStatus({ ...baseRun, endedAt: 2000, outcome: { status: "timeout" } })).toBe(
|
|
||||||
"timeout",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("formats duration compact for seconds and minutes", () => {
|
it("formats duration compact for seconds and minutes", () => {
|
||||||
expect(formatDurationCompact(45_000)).toBe("45s");
|
expect(formatDurationCompact(45_000)).toBe("45s");
|
||||||
expect(formatDurationCompact(65_000)).toBe("1m5s");
|
expect(formatDurationCompact(65_000)).toBe("1m5s");
|
||||||
|
|||||||
@@ -21,14 +21,6 @@ export function formatRunLabel(entry: SubagentRunRecord, options?: { maxLength?:
|
|||||||
return raw.length > maxLength ? `${truncateUtf16Safe(raw, maxLength).trimEnd()}…` : raw;
|
return raw.length > maxLength ? `${truncateUtf16Safe(raw, maxLength).trimEnd()}…` : raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatRunStatus(entry: SubagentRunRecord) {
|
|
||||||
if (!entry.endedAt) {
|
|
||||||
return "running";
|
|
||||||
}
|
|
||||||
const status = entry.outcome?.status ?? "done";
|
|
||||||
return status === "ok" ? "done" : status;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sortSubagentRuns(runs: SubagentRunRecord[]) {
|
export function sortSubagentRuns(runs: SubagentRunRecord[]) {
|
||||||
return [...runs].toSorted((a, b) => {
|
return [...runs].toSorted((a, b) => {
|
||||||
const aTime = a.startedAt ?? a.createdAt ?? 0;
|
const aTime = a.startedAt ?? a.createdAt ?? 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user