diff --git a/CHANGELOG.md b/CHANGELOG.md index 248ee48fdd0..3b558adc16e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ Docs: https://docs.openclaw.ai - Agents/MCP: dispose bundled MCP runtimes after one-shot `openclaw agent --local` runs finish, while preserving bundled MCP state across in-run retries so local JSON runs exit cleanly without restarting stateful MCP tools mid-run. - Gateway/OpenAI HTTP: restore default operator scopes for bearer-authenticated requests that omit `x-openclaw-scopes`, so headless `/v1/chat/completions` and session-history callers work again after the recent method-scope hardening. (#57596) Thanks @openperf. - Gateway/attachments: offload large inbound images without leaking `media://` markers into text-only runs, preserve mixed attachment order for model input/transcripts, and fail closed when model image capability cannot be resolved. (#55513) Thanks @Syysean. +- Agents/subagents: fix interim subagent runtime display so `/subagents list` and `/subagents info` stop inflating short runtimes and show second-level durations correctly. (#57739) Thanks @samzong. ## 2026.3.28 diff --git a/src/agents/subagent-control.ts b/src/agents/subagent-control.ts index 9127d88f113..fcc73f59ca1 100644 --- a/src/agents/subagent-control.ts +++ b/src/agents/subagent-control.ts @@ -345,7 +345,7 @@ export function buildSubagentList(params: { }), ), ); - const runtime = formatDurationCompact(runtimeMs); + 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}${task.toLowerCase() !== label.toLowerCase() ? ` - ${task}` : ""}`; diff --git a/src/auto-reply/reply/commands-subagents/shared.ts b/src/auto-reply/reply/commands-subagents/shared.ts index 3d2b9726da3..7da9ca3a69d 100644 --- a/src/auto-reply/reply/commands-subagents/shared.ts +++ b/src/auto-reply/reply/commands-subagents/shared.ts @@ -148,7 +148,7 @@ export function formatSubagentListLine(params: { const usageText = formatTokenUsageDisplay(params.sessionEntry); const label = truncateLine(formatRunLabel(params.entry, { maxLength: 48 }), 48); const task = formatTaskPreview(params.entry.task); - const runtime = formatDurationCompact(params.runtimeMs); + const runtime = formatDurationCompact(params.runtimeMs) ?? "n/a"; const status = resolveDisplayStatus(params.entry, { pendingDescendants: params.pendingDescendants, }); diff --git a/src/shared/subagents-format.test.ts b/src/shared/subagents-format.test.ts index c058c19ccd1..1dac19dbbe4 100644 --- a/src/shared/subagents-format.test.ts +++ b/src/shared/subagents-format.test.ts @@ -9,9 +9,10 @@ import { } from "./subagents-format.js"; describe("shared/subagents-format", () => { - it("formats compact durations across minute, hour, and day buckets", () => { - expect(formatDurationCompact()).toBe("n/a"); - expect(formatDurationCompact(30_000)).toBe("1m"); + it("re-exports the canonical formatter with second-level precision", () => { + expect(formatDurationCompact()).toBeUndefined(); + expect(formatDurationCompact(30_000)).toBe("30s"); + expect(formatDurationCompact(90_000)).toBe("1m30s"); expect(formatDurationCompact(60 * 60_000)).toBe("1h"); expect(formatDurationCompact(61 * 60_000)).toBe("1h1m"); expect(formatDurationCompact(24 * 60 * 60_000)).toBe("1d"); diff --git a/src/shared/subagents-format.ts b/src/shared/subagents-format.ts index 643c4b58ca5..c24042a0a0d 100644 --- a/src/shared/subagents-format.ts +++ b/src/shared/subagents-format.ts @@ -1,20 +1,4 @@ -export function formatDurationCompact(valueMs?: number) { - if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) { - return "n/a"; - } - const minutes = Math.max(1, Math.round(valueMs / 60_000)); - if (minutes < 60) { - return `${minutes}m`; - } - const hours = Math.floor(minutes / 60); - const minutesRemainder = minutes % 60; - if (hours < 24) { - return minutesRemainder > 0 ? `${hours}h${minutesRemainder}m` : `${hours}h`; - } - const days = Math.floor(hours / 24); - const hoursRemainder = hours % 24; - return hoursRemainder > 0 ? `${days}d${hoursRemainder}h` : `${days}d`; -} +export { formatDurationCompact } from "../infra/format-time/format-duration.ts"; export function formatTokenShort(value?: number) { if (!value || !Number.isFinite(value) || value <= 0) {