Centralize date/time formatting utilities (#11831)

This commit is contained in:
max
2026-02-08 04:53:31 -08:00
committed by GitHub
parent 74fbbda283
commit a1123dd9be
77 changed files with 1508 additions and 1075 deletions
+2 -2
View File
@@ -1,5 +1,6 @@
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import { formatDurationCompact } from "../infra/format-time/format-duration.ts";
import {
deleteSession,
drainSession,
@@ -12,7 +13,6 @@ import {
} from "./bash-process-registry.js";
import {
deriveSessionName,
formatDuration,
killSession,
pad,
sliceLogLines,
@@ -118,7 +118,7 @@ export function createProcessTool(
.toSorted((a, b) => b.startedAt - a.startedAt)
.map((s) => {
const label = s.name ? truncateMiddle(s.name, 80) : truncateMiddle(s.command, 120);
return `${s.sessionId} ${pad(s.status, 9)} ${formatDuration(s.runtimeMs)} :: ${label}`;
return `${s.sessionId} ${pad(s.status, 9)} ${formatDurationCompact(s.runtimeMs) ?? "n/a"} :: ${label}`;
});
return {
content: [
-13
View File
@@ -244,19 +244,6 @@ function stripQuotes(value: string): string {
return trimmed;
}
export function formatDuration(ms: number) {
if (ms < 1000) {
return `${ms}ms`;
}
const seconds = Math.floor(ms / 1000);
if (seconds < 60) {
return `${seconds}s`;
}
const minutes = Math.floor(seconds / 60);
const rem = seconds % 60;
return `${minutes}m${rem.toString().padStart(2, "0")}s`;
}
export function pad(str: string, width: number) {
if (str.length >= width) {
return str;
+2 -18
View File
@@ -9,6 +9,7 @@ import {
resolveStorePath,
} from "../config/sessions.js";
import { callGateway } from "../gateway/call.js";
import { formatDurationCompact } from "../infra/format-time/format-duration.ts";
import { normalizeMainKey } from "../routing/session-key.js";
import { defaultRuntime } from "../runtime.js";
import {
@@ -25,23 +26,6 @@ import {
import { type AnnounceQueueItem, enqueueAnnounce } from "./subagent-announce-queue.js";
import { readLatestAssistantReply } from "./tools/agent-step.js";
function formatDurationShort(valueMs?: number) {
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
return undefined;
}
const totalSeconds = Math.round(valueMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}h${minutes}m`;
}
if (minutes > 0) {
return `${minutes}m${seconds}s`;
}
return `${seconds}s`;
}
function formatTokenCount(value?: number) {
if (!value || !Number.isFinite(value)) {
return "0";
@@ -267,7 +251,7 @@ async function buildSubagentStatsLine(params: {
: undefined;
const parts: string[] = [];
const runtime = formatDurationShort(runtimeMs);
const runtime = formatDurationCompact(runtimeMs);
parts.push(`runtime ${runtime ?? "n/a"}`);
if (typeof total === "number") {
const inputText = typeof input === "number" ? formatTokenCount(input) : "n/a";
+22 -81
View File
@@ -2,6 +2,12 @@ import type { OpenClawConfig } from "../config/config.js";
import { resolveUserTimezone } from "../agents/date-time.js";
import { normalizeChatType } from "../channels/chat-type.js";
import { resolveSenderLabel, type SenderLabelParams } from "../channels/sender-label.js";
import {
resolveTimezone,
formatUtcTimestamp,
formatZonedTimestamp,
} from "../infra/format-time/format-datetime.ts";
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
export type AgentEnvelopeParams = {
channel: string;
@@ -66,15 +72,6 @@ function normalizeEnvelopeOptions(options?: EnvelopeFormatOptions): NormalizedEn
};
}
function resolveExplicitTimezone(value: string): string | undefined {
try {
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
return value;
} catch {
return undefined;
}
}
function resolveEnvelopeTimezone(options: NormalizedEnvelopeOptions): ResolvedEnvelopeTimezone {
const trimmed = options.timezone?.trim();
if (!trimmed) {
@@ -90,46 +87,10 @@ function resolveEnvelopeTimezone(options: NormalizedEnvelopeOptions): ResolvedEn
if (lowered === "user") {
return { mode: "iana", timeZone: resolveUserTimezone(options.userTimezone) };
}
const explicit = resolveExplicitTimezone(trimmed);
const explicit = resolveTimezone(trimmed);
return explicit ? { mode: "iana", timeZone: explicit } : { mode: "utc" };
}
function formatUtcTimestamp(date: Date): string {
const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
const dd = String(date.getUTCDate()).padStart(2, "0");
const hh = String(date.getUTCHours()).padStart(2, "0");
const min = String(date.getUTCMinutes()).padStart(2, "0");
return `${yyyy}-${mm}-${dd}T${hh}:${min}Z`;
}
export function formatZonedTimestamp(date: Date, timeZone?: string): string | undefined {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
timeZoneName: "short",
}).formatToParts(date);
const pick = (type: string) => parts.find((part) => part.type === type)?.value;
const yyyy = pick("year");
const mm = pick("month");
const dd = pick("day");
const hh = pick("hour");
const min = pick("minute");
const tz = [...parts]
.toReversed()
.find((part) => part.type === "timeZoneName")
?.value?.trim();
if (!yyyy || !mm || !dd || !hh || !min) {
return undefined;
}
return `${yyyy}-${mm}-${dd} ${hh}:${min}${tz ? ` ${tz}` : ""}`;
}
function formatTimestamp(
ts: number | Date | undefined,
options?: EnvelopeFormatOptions,
@@ -152,47 +113,27 @@ function formatTimestamp(
if (zone.mode === "local") {
return formatZonedTimestamp(date);
}
return formatZonedTimestamp(date, zone.timeZone);
}
function formatElapsedTime(currentMs: number, previousMs: number): string | undefined {
const elapsedMs = currentMs - previousMs;
if (!Number.isFinite(elapsedMs) || elapsedMs < 0) {
return undefined;
}
const seconds = Math.floor(elapsedMs / 1000);
if (seconds < 60) {
return `${seconds}s`;
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
return `${hours}h`;
}
const days = Math.floor(hours / 24);
return `${days}d`;
return formatZonedTimestamp(date, { timeZone: zone.timeZone });
}
export function formatAgentEnvelope(params: AgentEnvelopeParams): string {
const channel = params.channel?.trim() || "Channel";
const parts: string[] = [channel];
const resolved = normalizeEnvelopeOptions(params.envelope);
const elapsed =
resolved.includeElapsed && params.timestamp && params.previousTimestamp
? formatElapsedTime(
params.timestamp instanceof Date ? params.timestamp.getTime() : params.timestamp,
params.previousTimestamp instanceof Date
? params.previousTimestamp.getTime()
: params.previousTimestamp,
)
: undefined;
let elapsed: string | undefined;
if (resolved.includeElapsed && params.timestamp && params.previousTimestamp) {
const currentMs =
params.timestamp instanceof Date ? params.timestamp.getTime() : params.timestamp;
const previousMs =
params.previousTimestamp instanceof Date
? params.previousTimestamp.getTime()
: params.previousTimestamp;
const elapsedMs = currentMs - previousMs;
elapsed =
Number.isFinite(elapsedMs) && elapsedMs >= 0
? formatTimeAgo(elapsedMs, { suffix: false })
: undefined;
}
if (params.from?.trim()) {
const from = params.from.trim();
parts.push(elapsed ? `${from} +${elapsed}` : from);
+7 -11
View File
@@ -14,17 +14,13 @@ import {
import { loadSessionStore, resolveStorePath, updateSessionStore } from "../../config/sessions.js";
import { callGateway } from "../../gateway/call.js";
import { logVerbose } from "../../globals.js";
import { formatDurationCompact } from "../../infra/format-time/format-duration.ts";
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import { stopSubagentsForRequester } from "./abort.js";
import { clearSessionQueues } from "./queue.js";
import {
formatAgeShort,
formatDurationShort,
formatRunLabel,
formatRunStatus,
sortSubagentRuns,
} from "./subagents-utils.js";
import { formatRunLabel, formatRunStatus, sortSubagentRuns } from "./subagents-utils.js";
type SubagentTargetResolution = {
entry?: SubagentRunRecord;
@@ -45,7 +41,7 @@ function formatTimestampWithAge(valueMs?: number) {
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
return "n/a";
}
return `${formatTimestamp(valueMs)} (${formatAgeShort(Date.now() - valueMs)})`;
return `${formatTimestamp(valueMs)} (${formatTimeAgo(Date.now() - valueMs, { fallback: "n/a" })})`;
}
function resolveRequesterSessionKey(params: Parameters<CommandHandler>[0]): string | undefined {
@@ -214,8 +210,8 @@ export const handleSubagentsCommand: CommandHandler = async (params, allowTextCo
const label = formatRunLabel(entry);
const runtime =
entry.endedAt && entry.startedAt
? formatDurationShort(entry.endedAt - entry.startedAt)
: formatAgeShort(Date.now() - (entry.startedAt ?? entry.createdAt));
? (formatDurationCompact(entry.endedAt - entry.startedAt) ?? "n/a")
: formatTimeAgo(Date.now() - (entry.startedAt ?? entry.createdAt), { fallback: "n/a" });
const runId = entry.runId.slice(0, 8);
lines.push(
`${index + 1}) ${status} · ${label} · ${runtime} · run ${runId} · ${entry.childSessionKey}`,
@@ -296,7 +292,7 @@ export const handleSubagentsCommand: CommandHandler = async (params, allowTextCo
const { entry: sessionEntry } = loadSubagentSessionEntry(params, run.childSessionKey);
const runtime =
run.startedAt && Number.isFinite(run.startedAt)
? formatDurationShort((run.endedAt ?? Date.now()) - run.startedAt)
? (formatDurationCompact((run.endedAt ?? Date.now()) - run.startedAt) ?? "n/a")
: "n/a";
const outcome = run.outcome
? `${run.outcome.status}${run.outcome.error ? ` (${run.outcome.error})` : ""}`
+12 -52
View File
@@ -5,6 +5,11 @@ import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
import { ensureSkillsWatcher, getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
import { buildChannelSummary } from "../../infra/channel-summary.js";
import {
resolveTimezone,
formatUtcTimestamp,
formatZonedTimestamp,
} from "../../infra/format-time/format-datetime.ts";
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
import { drainSystemEventEntries } from "../../infra/system-events.js";
@@ -39,15 +44,6 @@ export async function prependSystemEvents(params: {
return trimmed;
};
const resolveExplicitTimezone = (value: string): string | undefined => {
try {
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
return value;
} catch {
return undefined;
}
};
const resolveSystemEventTimezone = (cfg: OpenClawConfig) => {
const raw = cfg.agents?.defaults?.envelopeTimezone?.trim();
if (!raw) {
@@ -66,49 +62,10 @@ export async function prependSystemEvents(params: {
timeZone: resolveUserTimezone(cfg.agents?.defaults?.userTimezone),
};
}
const explicit = resolveExplicitTimezone(raw);
const explicit = resolveTimezone(raw);
return explicit ? { mode: "iana" as const, timeZone: explicit } : { mode: "local" as const };
};
const formatUtcTimestamp = (date: Date): string => {
const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
const dd = String(date.getUTCDate()).padStart(2, "0");
const hh = String(date.getUTCHours()).padStart(2, "0");
const min = String(date.getUTCMinutes()).padStart(2, "0");
const sec = String(date.getUTCSeconds()).padStart(2, "0");
return `${yyyy}-${mm}-${dd}T${hh}:${min}:${sec}Z`;
};
const formatZonedTimestamp = (date: Date, timeZone?: string): string | undefined => {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
timeZoneName: "short",
}).formatToParts(date);
const pick = (type: string) => parts.find((part) => part.type === type)?.value;
const yyyy = pick("year");
const mm = pick("month");
const dd = pick("day");
const hh = pick("hour");
const min = pick("minute");
const sec = pick("second");
const tz = [...parts]
.toReversed()
.find((part) => part.type === "timeZoneName")
?.value?.trim();
if (!yyyy || !mm || !dd || !hh || !min || !sec) {
return undefined;
}
return `${yyyy}-${mm}-${dd} ${hh}:${min}:${sec}${tz ? ` ${tz}` : ""}`;
};
const formatSystemEventTimestamp = (ts: number, cfg: OpenClawConfig) => {
const date = new Date(ts);
if (Number.isNaN(date.getTime())) {
@@ -116,12 +73,15 @@ export async function prependSystemEvents(params: {
}
const zone = resolveSystemEventTimezone(cfg);
if (zone.mode === "utc") {
return formatUtcTimestamp(date);
return formatUtcTimestamp(date, { displaySeconds: true });
}
if (zone.mode === "local") {
return formatZonedTimestamp(date) ?? "unknown-time";
return formatZonedTimestamp(date, { displaySeconds: true }) ?? "unknown-time";
}
return formatZonedTimestamp(date, zone.timeZone) ?? "unknown-time";
return (
formatZonedTimestamp(date, { timeZone: zone.timeZone, displaySeconds: true }) ??
"unknown-time"
);
};
const systemLines: string[] = [];
+4 -4
View File
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
import { formatDurationCompact } from "../../infra/format-time/format-duration.js";
import {
formatDurationShort,
formatRunLabel,
formatRunStatus,
resolveSubagentLabel,
@@ -54,8 +54,8 @@ describe("subagents utils", () => {
);
});
it("formats duration short for seconds and minutes", () => {
expect(formatDurationShort(45_000)).toBe("45s");
expect(formatDurationShort(65_000)).toBe("1m5s");
it("formats duration compact for seconds and minutes", () => {
expect(formatDurationCompact(45_000)).toBe("45s");
expect(formatDurationCompact(65_000)).toBe("1m5s");
});
});
-36
View File
@@ -1,42 +1,6 @@
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
import { truncateUtf16Safe } from "../../utils.js";
export function formatDurationShort(valueMs?: number) {
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
return "n/a";
}
const totalSeconds = Math.round(valueMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}h${minutes}m`;
}
if (minutes > 0) {
return `${minutes}m${seconds}s`;
}
return `${seconds}s`;
}
export function formatAgeShort(valueMs?: number) {
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
return "n/a";
}
const minutes = Math.round(valueMs / 60_000);
if (minutes < 1) {
return "just now";
}
if (minutes < 60) {
return `${minutes}m ago`;
}
const hours = Math.round(minutes / 60);
if (hours < 48) {
return `${hours}h ago`;
}
const days = Math.round(hours / 24);
return `${days}d ago`;
}
export function resolveSubagentLabel(entry: SubagentRunRecord, fallback = "subagent") {
const raw = entry.label?.trim() || entry.task?.trim() || "";
return raw || fallback;
+2 -20
View File
@@ -16,6 +16,7 @@ import {
type SessionEntry,
type SessionScope,
} from "../config/sessions.js";
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
import { resolveCommitHash } from "../infra/git-commit.js";
import { listPluginCommands } from "../plugins/commands.js";
import {
@@ -134,25 +135,6 @@ export const formatContextUsageShort = (
contextTokens: number | null | undefined,
) => `Context ${formatTokens(total, contextTokens ?? null)}`;
const formatAge = (ms?: number | null) => {
if (!ms || ms < 0) {
return "unknown";
}
const minutes = Math.round(ms / 60_000);
if (minutes < 1) {
return "just now";
}
if (minutes < 60) {
return `${minutes}m ago`;
}
const hours = Math.round(minutes / 60);
if (hours < 48) {
return `${hours}h ago`;
}
const days = Math.round(hours / 24);
return `${days}d ago`;
};
const formatQueueDetails = (queue?: QueueStatus) => {
if (!queue) {
return "";
@@ -386,7 +368,7 @@ export function buildStatusMessage(args: StatusArgs): string {
const updatedAt = entry?.updatedAt;
const sessionLine = [
`Session: ${args.sessionKey ?? "unknown"}`,
typeof updatedAt === "number" ? `updated ${formatAge(now - updatedAt)}` : "no activity",
typeof updatedAt === "number" ? `updated ${formatTimeAgo(now - updatedAt)}` : "no activity",
]
.filter(Boolean)
.join(" • ");
+2 -14
View File
@@ -2,6 +2,7 @@ import type { CronJob, CronSchedule } from "../../cron/types.js";
import type { GatewayRpcOpts } from "../gateway-rpc.js";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import { parseAbsoluteTimeMs } from "../../cron/parse.js";
import { formatDurationHuman } from "../../infra/format-time/format-duration.ts";
import { defaultRuntime } from "../../runtime.js";
import { colorize, isRich, theme } from "../../terminal/theme.js";
import { callGatewayFromCli } from "../gateway-rpc.js";
@@ -107,19 +108,6 @@ const formatIsoMinute = (iso: string) => {
return `${isoStr.slice(0, 10)} ${isoStr.slice(11, 16)}Z`;
};
const formatDuration = (ms: number) => {
if (ms < 60_000) {
return `${Math.max(1, Math.round(ms / 1000))}s`;
}
if (ms < 3_600_000) {
return `${Math.round(ms / 60_000)}m`;
}
if (ms < 86_400_000) {
return `${Math.round(ms / 3_600_000)}h`;
}
return `${Math.round(ms / 86_400_000)}d`;
};
const formatSpan = (ms: number) => {
if (ms < 60_000) {
return "<1m";
@@ -147,7 +135,7 @@ const formatSchedule = (schedule: CronSchedule) => {
return `at ${formatIsoMinute(schedule.at)}`;
}
if (schedule.kind === "every") {
return `every ${formatDuration(schedule.everyMs)}`;
return `every ${formatDurationHuman(schedule.everyMs)}`;
}
return schedule.tz ? `cron ${schedule.expr} @ ${schedule.tz}` : `cron ${schedule.expr}`;
};
+2 -18
View File
@@ -1,5 +1,6 @@
import type { Command } from "commander";
import { callGateway } from "../gateway/call.js";
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
import { defaultRuntime } from "../runtime.js";
import { renderTable } from "../terminal/table.js";
import { theme } from "../terminal/theme.js";
@@ -49,23 +50,6 @@ type DevicePairingList = {
paired?: PairedDevice[];
};
function formatAge(msAgo: number) {
const s = Math.max(0, Math.floor(msAgo / 1000));
if (s < 60) {
return `${s}s`;
}
const m = Math.floor(s / 60);
if (m < 60) {
return `${m}m`;
}
const h = Math.floor(m / 60);
if (h < 24) {
return `${h}h`;
}
const d = Math.floor(h / 24);
return `${d}d`;
}
const devicesCallOpts = (cmd: Command, defaults?: { timeoutMs?: number }) =>
cmd
.option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)")
@@ -147,7 +131,7 @@ export function registerDevicesCli(program: Command) {
Device: req.displayName || req.deviceId,
Role: req.role ?? "",
IP: req.remoteIp ?? "",
Age: typeof req.ts === "number" ? `${formatAge(Date.now() - req.ts)} ago` : "",
Age: typeof req.ts === "number" ? formatTimeAgo(Date.now() - req.ts) : "",
Flags: req.isRepair ? "repair" : "",
})),
}).trimEnd(),
+2 -18
View File
@@ -8,6 +8,7 @@ import {
type ExecApprovalsAgent,
type ExecApprovalsFile,
} from "../infra/exec-approvals.js";
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js";
import { renderTable } from "../terminal/table.js";
@@ -31,23 +32,6 @@ type ExecApprovalsCliOpts = NodesRpcOpts & {
agent?: string;
};
function formatAge(msAgo: number) {
const s = Math.max(0, Math.floor(msAgo / 1000));
if (s < 60) {
return `${s}s`;
}
const m = Math.floor(s / 60);
if (m < 60) {
return `${m}m`;
}
const h = Math.floor(m / 60);
if (h < 24) {
return `${h}h`;
}
const d = Math.floor(h / 24);
return `${d}d`;
}
async function readStdin(): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) {
@@ -142,7 +126,7 @@ function renderApprovalsSnapshot(snapshot: ExecApprovalsSnapshot, targetLabel: s
Target: targetLabel,
Agent: agentId,
Pattern: pattern,
LastUsed: lastUsedAt ? `${formatAge(Math.max(0, now - lastUsedAt))} ago` : muted("unknown"),
LastUsed: lastUsedAt ? formatTimeAgo(Math.max(0, now - lastUsedAt)) : muted("unknown"),
});
}
}
-17
View File
@@ -1,22 +1,5 @@
import type { NodeListNode, PairedNode, PairingList, PendingRequest } from "./types.js";
export function formatAge(msAgo: number) {
const s = Math.max(0, Math.floor(msAgo / 1000));
if (s < 60) {
return `${s}s`;
}
const m = Math.floor(s / 60);
if (m < 60) {
return `${m}m`;
}
const h = Math.floor(m / 60);
if (h < 24) {
return `${h}h`;
}
const d = Math.floor(h / 24);
return `${d}d`;
}
export function parsePairingList(value: unknown): PairingList {
const obj = typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
const pending = Array.isArray(obj.pending) ? (obj.pending as PendingRequest[]) : [];
+3 -4
View File
@@ -1,9 +1,10 @@
import type { Command } from "commander";
import type { NodesRpcOpts } from "./types.js";
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
import { defaultRuntime } from "../../runtime.js";
import { renderTable } from "../../terminal/table.js";
import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
import { formatAge, parsePairingList } from "./format.js";
import { parsePairingList } from "./format.js";
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
export function registerNodesPairingCommands(nodes: Command) {
@@ -32,9 +33,7 @@ export function registerNodesPairingCommands(nodes: Command) {
Node: r.displayName?.trim() ? r.displayName.trim() : r.nodeId,
IP: r.remoteIp ?? "",
Requested:
typeof r.ts === "number"
? `${formatAge(Math.max(0, now - r.ts))} ago`
: muted("unknown"),
typeof r.ts === "number" ? formatTimeAgo(Math.max(0, now - r.ts)) : muted("unknown"),
Repair: r.isRepair ? warn("yes") : "",
}));
defaultRuntime.log(heading("Pending"));
+5 -4
View File
@@ -1,11 +1,12 @@
import type { Command } from "commander";
import type { NodesRpcOpts } from "./types.js";
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
import { defaultRuntime } from "../../runtime.js";
import { renderTable } from "../../terminal/table.js";
import { shortenHomeInString } from "../../utils.js";
import { parseDurationMs } from "../parse-duration.js";
import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
import { formatAge, formatPermissions, parseNodeList, parsePairingList } from "./format.js";
import { formatPermissions, parseNodeList, parsePairingList } from "./format.js";
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
function formatVersionLabel(raw: string) {
@@ -178,7 +179,7 @@ export function registerNodesStatusCommands(nodes: Command) {
const connected = n.connected ? ok("connected") : muted("disconnected");
const since =
typeof n.connectedAtMs === "number"
? ` (${formatAge(Math.max(0, now - n.connectedAtMs))} ago)`
? ` (${formatTimeAgo(Math.max(0, now - n.connectedAtMs))})`
: "";
return {
@@ -361,7 +362,7 @@ export function registerNodesStatusCommands(nodes: Command) {
IP: r.remoteIp ?? "",
Requested:
typeof r.ts === "number"
? `${formatAge(Math.max(0, now - r.ts))} ago`
? formatTimeAgo(Math.max(0, now - r.ts))
: muted("unknown"),
Repair: r.isRepair ? warn("yes") : "",
}));
@@ -397,7 +398,7 @@ export function registerNodesStatusCommands(nodes: Command) {
IP: n.remoteIp ?? "",
LastConnect:
typeof lastConnectedAtMs === "number"
? `${formatAge(Math.max(0, now - lastConnectedAtMs))} ago`
? formatTimeAgo(Math.max(0, now - lastConnectedAtMs))
: muted("unknown"),
};
});
+4 -11
View File
@@ -16,6 +16,7 @@ import {
} from "../commands/status.update.js";
import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import { formatDurationPrecise } from "../infra/format-time/format-duration.ts";
import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js";
import { trimLogTail } from "../infra/restart-sentinel.js";
import { parseSemver } from "../infra/runtime-guard.js";
@@ -575,7 +576,7 @@ function createUpdateProgress(enabled: boolean): ProgressController {
}
const label = getStepLabel(step);
const duration = theme.muted(`(${formatDuration(step.durationMs)})`);
const duration = theme.muted(`(${formatDurationPrecise(step.durationMs)})`);
const icon = step.exitCode === 0 ? theme.success("\u2713") : theme.error("\u2717");
currentSpinner.stop(`${icon} ${label} ${duration}`);
@@ -603,14 +604,6 @@ function createUpdateProgress(enabled: boolean): ProgressController {
};
}
function formatDuration(ms: number): string {
if (ms < 1000) {
return `${ms}ms`;
}
const seconds = (ms / 1000).toFixed(1);
return `${seconds}s`;
}
function formatStepStatus(exitCode: number | null): string {
if (exitCode === 0) {
return theme.success("\u2713");
@@ -668,7 +661,7 @@ function printResult(result: UpdateRunResult, opts: PrintResultOptions) {
defaultRuntime.log(theme.heading("Steps:"));
for (const step of result.steps) {
const status = formatStepStatus(step.exitCode);
const duration = theme.muted(`(${formatDuration(step.durationMs)})`);
const duration = theme.muted(`(${formatDurationPrecise(step.durationMs)})`);
defaultRuntime.log(` ${status} ${step.name} ${duration}`);
if (step.exitCode !== 0 && step.stderrTail) {
@@ -683,7 +676,7 @@ function printResult(result: UpdateRunResult, opts: PrintResultOptions) {
}
defaultRuntime.log("");
defaultRuntime.log(`Total time: ${theme.muted(formatDuration(result.durationMs))}`);
defaultRuntime.log(`Total time: ${theme.muted(formatDurationPrecise(result.durationMs))}`);
}
export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
+3 -3
View File
@@ -5,8 +5,8 @@ import { formatCliCommand } from "../../cli/command-format.js";
import { withProgress } from "../../cli/progress.js";
import { type OpenClawConfig, readConfigFileSnapshot } from "../../config/config.js";
import { callGateway } from "../../gateway/call.js";
import { formatAge } from "../../infra/channel-summary.js";
import { collectChannelStatusIssues } from "../../infra/channels-status-issues.js";
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
@@ -48,10 +48,10 @@ export function formatGatewayChannelsStatusLines(payload: Record<string, unknown
? account.lastOutboundAt
: null;
if (inboundAt) {
bits.push(`in:${formatAge(Date.now() - inboundAt)}`);
bits.push(`in:${formatTimeAgo(Date.now() - inboundAt)}`);
}
if (outboundAt) {
bits.push(`out:${formatAge(Date.now() - outboundAt)}`);
bits.push(`out:${formatTimeAgo(Date.now() - outboundAt)}`);
}
if (typeof account.mode === "string" && account.mode.length > 0) {
bits.push(`mode:${account.mode}`);
+14 -10
View File
@@ -5,12 +5,8 @@
import type { SandboxBrowserInfo, SandboxContainerInfo } from "../agents/sandbox.js";
import type { RuntimeEnv } from "../runtime.js";
import { formatCliCommand } from "../cli/command-format.js";
import {
formatAge,
formatImageMatch,
formatSimpleStatus,
formatStatus,
} from "./sandbox-formatters.js";
import { formatDurationCompact } from "../infra/format-time/format-duration.ts";
import { formatImageMatch, formatSimpleStatus, formatStatus } from "./sandbox-formatters.js";
type DisplayConfig<T> = {
emptyMessage: string;
@@ -40,8 +36,12 @@ export function displayContainers(containers: SandboxContainerInfo[], runtime: R
rt.log(` ${container.containerName}`);
rt.log(` Status: ${formatStatus(container.running)}`);
rt.log(` Image: ${container.image} ${formatImageMatch(container.imageMatch)}`);
rt.log(` Age: ${formatAge(Date.now() - container.createdAtMs)}`);
rt.log(` Idle: ${formatAge(Date.now() - container.lastUsedAtMs)}`);
rt.log(
` Age: ${formatDurationCompact(Date.now() - container.createdAtMs, { spaced: true }) ?? "0s"}`,
);
rt.log(
` Idle: ${formatDurationCompact(Date.now() - container.lastUsedAtMs, { spaced: true }) ?? "0s"}`,
);
rt.log(` Session: ${container.sessionKey}`);
rt.log("");
},
@@ -64,8 +64,12 @@ export function displayBrowsers(browsers: SandboxBrowserInfo[], runtime: Runtime
if (browser.noVncPort) {
rt.log(` noVNC: ${browser.noVncPort}`);
}
rt.log(` Age: ${formatAge(Date.now() - browser.createdAtMs)}`);
rt.log(` Idle: ${formatAge(Date.now() - browser.lastUsedAtMs)}`);
rt.log(
` Age: ${formatDurationCompact(Date.now() - browser.createdAtMs, { spaced: true }) ?? "0s"}`,
);
rt.log(
` Idle: ${formatDurationCompact(Date.now() - browser.lastUsedAtMs, { spaced: true }) ?? "0s"}`,
);
rt.log(` Session: ${browser.sessionKey}`);
rt.log("");
},
+12 -9
View File
@@ -1,13 +1,16 @@
import { describe, expect, it } from "vitest";
import { formatDurationCompact } from "../infra/format-time/format-duration.js";
import {
countMismatches,
countRunning,
formatAge,
formatImageMatch,
formatSimpleStatus,
formatStatus,
} from "./sandbox-formatters.js";
/** Helper matching old formatAge behavior: spaced compound duration */
const formatAge = (ms: number) => formatDurationCompact(ms, { spaced: true }) ?? "0s";
describe("sandbox-formatters", () => {
describe("formatStatus", () => {
it("should format running status", () => {
@@ -47,21 +50,21 @@ describe("sandbox-formatters", () => {
it("should format minutes", () => {
expect(formatAge(60000)).toBe("1m");
expect(formatAge(90000)).toBe("1m");
expect(formatAge(90000)).toBe("1m 30s"); // 90 seconds = 1m 30s
expect(formatAge(300000)).toBe("5m");
});
it("should format hours and minutes", () => {
expect(formatAge(3600000)).toBe("1h 0m");
expect(formatAge(3600000)).toBe("1h");
expect(formatAge(3660000)).toBe("1h 1m");
expect(formatAge(7200000)).toBe("2h 0m");
expect(formatAge(7200000)).toBe("2h");
expect(formatAge(5400000)).toBe("1h 30m");
});
it("should format days and hours", () => {
expect(formatAge(86400000)).toBe("1d 0h");
expect(formatAge(86400000)).toBe("1d");
expect(formatAge(90000000)).toBe("1d 1h");
expect(formatAge(172800000)).toBe("2d 0h");
expect(formatAge(172800000)).toBe("2d");
expect(formatAge(183600000)).toBe("2d 3h");
});
@@ -70,9 +73,9 @@ describe("sandbox-formatters", () => {
});
it("should handle edge cases", () => {
expect(formatAge(59999)).toBe("59s"); // Just under 1 minute
expect(formatAge(3599999)).toBe("59m"); // Just under 1 hour
expect(formatAge(86399999)).toBe("23h 59m"); // Just under 1 day
expect(formatAge(59999)).toBe("1m"); // Rounds to 1 minute exactly
expect(formatAge(3599999)).toBe("1h"); // Rounds to 1 hour exactly
expect(formatAge(86399999)).toBe("1d"); // Rounds to 1 day exactly
});
});
-18
View File
@@ -14,24 +14,6 @@ export function formatImageMatch(matches: boolean): string {
return matches ? "✓" : "⚠️ mismatch";
}
export function formatAge(ms: number): string {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h`;
}
if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
}
if (minutes > 0) {
return `${minutes}m`;
}
return `${seconds}s`;
}
/**
* Type guard and counter utilities
*/
+2 -20
View File
@@ -5,6 +5,7 @@ import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import { loadConfig } from "../config/config.js";
import { loadSessionStore, resolveStorePath, type SessionEntry } from "../config/sessions.js";
import { info } from "../globals.js";
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
import { isRich, theme } from "../terminal/theme.js";
type SessionRow = {
@@ -90,7 +91,7 @@ const formatKindCell = (kind: SessionRow["kind"], rich: boolean) => {
};
const formatAgeCell = (updatedAt: number | null | undefined, rich: boolean) => {
const ageLabel = updatedAt ? formatAge(Date.now() - updatedAt) : "unknown";
const ageLabel = updatedAt ? formatTimeAgo(Date.now() - updatedAt) : "unknown";
const padded = ageLabel.padEnd(AGE_PAD);
return rich ? theme.muted(padded) : padded;
};
@@ -116,25 +117,6 @@ const formatFlagsCell = (row: SessionRow, rich: boolean) => {
return label.length === 0 ? "" : rich ? theme.muted(label) : label;
};
const formatAge = (ms: number | null | undefined) => {
if (!ms || ms < 0) {
return "unknown";
}
const minutes = Math.round(ms / 60_000);
if (minutes < 1) {
return "just now";
}
if (minutes < 60) {
return `${minutes}m ago`;
}
const hours = Math.round(minutes / 60);
if (hours < 48) {
return `${hours}h ago`;
}
const days = Math.round(hours / 24);
return `${days}d ago`;
};
function classifyKey(key: string, entry?: SessionEntry): SessionRow["kind"] {
if (key === "global") {
return "global";
+2 -2
View File
@@ -28,7 +28,7 @@ import { VERSION } from "../version.js";
import { resolveControlUiLinks } from "./onboard-helpers.js";
import { getAgentLocalStatuses } from "./status-all/agents.js";
import { buildChannelsTable } from "./status-all/channels.js";
import { formatDuration, formatGatewayAuthUsed } from "./status-all/format.js";
import { formatDurationPrecise, formatGatewayAuthUsed } from "./status-all/format.js";
import { pickGatewaySelfPresence } from "./status-all/gateway.js";
import { buildStatusAllReportLines } from "./status-all/report-lines.js";
@@ -354,7 +354,7 @@ export async function statusAllCommand(
const gatewayTarget = remoteUrlMissing ? `fallback ${connection.url}` : connection.url;
const gatewayStatus = gatewayReachable
? `reachable ${formatDuration(gatewayProbe?.connectLatencyMs)}`
? `reachable ${formatDurationPrecise(gatewayProbe?.connectLatencyMs ?? 0)}`
: gatewayProbe?.error
? `unreachable (${gatewayProbe.error})`
: "unreachable";
+2 -2
View File
@@ -8,7 +8,7 @@ import type { OpenClawConfig } from "../../config/config.js";
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import { sha256HexPrefix } from "../../logging/redact-identifier.js";
import { formatAge } from "./format.js";
import { formatTimeAgo } from "./format.js";
export type ChannelRow = {
id: ChannelId;
@@ -436,7 +436,7 @@ export async function buildChannelsTable(
extra.push(link.selfE164);
}
if (link.linked && link.authAgeMs != null && link.authAgeMs >= 0) {
extra.push(`auth ${formatAge(link.authAgeMs)}`);
extra.push(`auth ${formatTimeAgo(link.authAgeMs)}`);
}
if (accounts.length > 1 || plugin.meta.forceAccountBinding) {
extra.push(`accounts ${accounts.length || 1}`);
+2 -2
View File
@@ -5,7 +5,7 @@ import {
type RestartSentinelPayload,
summarizeRestartSentinel,
} from "../../infra/restart-sentinel.js";
import { formatAge, redactSecrets } from "./format.js";
import { formatTimeAgo, redactSecrets } from "./format.js";
import { readFileTailLines, summarizeLogTail } from "./gateway.js";
type ConfigIssueLike = { path: string; message: string };
@@ -106,7 +106,7 @@ export async function appendStatusAllDiagnosis(params: {
if (params.sentinel?.payload) {
emitCheck("Restart sentinel present", "warn");
lines.push(
` ${muted(`${summarizeRestartSentinel(params.sentinel.payload)} · ${formatAge(Date.now() - params.sentinel.payload.ts)}`)}`,
` ${muted(`${summarizeRestartSentinel(params.sentinel.payload)} · ${formatTimeAgo(Date.now() - params.sentinel.payload.ts)}`)}`,
);
} else {
emitCheck("Restart sentinel: none", "ok");
+2 -28
View File
@@ -1,31 +1,5 @@
export const formatAge = (ms: number | null | undefined) => {
if (!ms || ms < 0) {
return "unknown";
}
const minutes = Math.round(ms / 60_000);
if (minutes < 1) {
return "just now";
}
if (minutes < 60) {
return `${minutes}m ago`;
}
const hours = Math.round(minutes / 60);
if (hours < 48) {
return `${hours}h ago`;
}
const days = Math.round(hours / 24);
return `${days}d ago`;
};
export const formatDuration = (ms: number | null | undefined) => {
if (ms == null || !Number.isFinite(ms)) {
return "unknown";
}
if (ms < 1000) {
return `${Math.round(ms)}ms`;
}
return `${(ms / 1000).toFixed(1)}s`;
};
export { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
export { formatDurationPrecise } from "../../infra/format-time/format-duration.ts";
export function formatGatewayAuthUsed(
auth: {
+2 -2
View File
@@ -2,7 +2,7 @@ import type { ProgressReporter } from "../../cli/progress.js";
import { renderTable } from "../../terminal/table.js";
import { isRich, theme } from "../../terminal/theme.js";
import { appendStatusAllDiagnosis } from "./diagnosis.js";
import { formatAge } from "./format.js";
import { formatTimeAgo } from "./format.js";
type OverviewRow = { Item: string; Value: string };
@@ -128,7 +128,7 @@ export async function buildStatusAllReportLines(params: {
? ok("OK")
: "unknown",
Sessions: String(a.sessionsCount),
Active: a.lastActiveAgeMs != null ? formatAge(a.lastActiveAgeMs) : "unknown",
Active: a.lastActiveAgeMs != null ? formatTimeAgo(a.lastActiveAgeMs) : "unknown",
Store: a.sessionsPath,
}));
+4 -4
View File
@@ -5,6 +5,7 @@ import { withProgress } from "../cli/progress.js";
import { resolveGatewayPort } from "../config/config.js";
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
import { info } from "../globals.js";
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
import { formatUsageReportLines, loadProviderUsageSummary } from "../infra/provider-usage.js";
import {
formatUpdateChannelLabel,
@@ -26,7 +27,6 @@ import { statusAllCommand } from "./status-all.js";
import { formatGatewayAuthUsed } from "./status-all/format.js";
import { getDaemonStatusSummary, getNodeDaemonStatusSummary } from "./status.daemon.js";
import {
formatAge,
formatDuration,
formatKTokens,
formatTokensCompact,
@@ -239,7 +239,7 @@ export async function statusCommand(
? `${agentStatus.bootstrapPendingCount} bootstrapping`
: "no bootstraps";
const def = agentStatus.agents.find((a) => a.id === agentStatus.defaultId);
const defActive = def?.lastActiveAgeMs != null ? formatAge(def.lastActiveAgeMs) : "unknown";
const defActive = def?.lastActiveAgeMs != null ? formatTimeAgo(def.lastActiveAgeMs) : "unknown";
const defSuffix = def ? ` · default ${def.id} active ${defActive}` : "";
return `${agentStatus.agents.length} · ${pending} · sessions ${agentStatus.totalSessions}${defSuffix}`;
})();
@@ -294,7 +294,7 @@ export async function statusCommand(
if (!lastHeartbeat) {
return muted("none");
}
const age = formatAge(Date.now() - lastHeartbeat.ts);
const age = formatTimeAgo(Date.now() - lastHeartbeat.ts);
const channel = lastHeartbeat.channel ?? "unknown";
const accountLabel = lastHeartbeat.accountId ? `account ${lastHeartbeat.accountId}` : null;
return [lastHeartbeat.status, `${age} ago`, channel, accountLabel].filter(Boolean).join(" · ");
@@ -527,7 +527,7 @@ export async function statusCommand(
? summary.sessions.recent.map((sess) => ({
Key: shortenText(sess.key, 32),
Kind: sess.kind,
Age: sess.updatedAt ? formatAge(sess.age) : "no activity",
Age: sess.updatedAt ? formatTimeAgo(sess.age) : "no activity",
Model: sess.model ?? "unknown",
Tokens: formatTokensCompact(sess),
}))
+2 -23
View File
@@ -1,35 +1,14 @@
import type { SessionStatus } from "./status.types.js";
import { formatDurationPrecise } from "../infra/format-time/format-duration.ts";
export const formatKTokens = (value: number) =>
`${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`;
export const formatAge = (ms: number | null | undefined) => {
if (!ms || ms < 0) {
return "unknown";
}
const minutes = Math.round(ms / 60_000);
if (minutes < 1) {
return "just now";
}
if (minutes < 60) {
return `${minutes}m ago`;
}
const hours = Math.round(minutes / 60);
if (hours < 48) {
return `${hours}h ago`;
}
const days = Math.round(hours / 24);
return `${days}d ago`;
};
export const formatDuration = (ms: number | null | undefined) => {
if (ms == null || !Number.isFinite(ms)) {
return "unknown";
}
if (ms < 1000) {
return `${Math.round(ms)}ms`;
}
return `${(ms / 1000).toFixed(1)}s`;
return formatDurationPrecise(ms, { decimals: 1 });
};
export const shortenText = (value: string, maxLen: number) => {
+1 -1
View File
@@ -7,7 +7,7 @@ import {
PresenceUpdateListener,
} from "@buape/carbon";
import { danger } from "../../globals.js";
import { formatDurationSeconds } from "../../infra/format-duration.js";
import { formatDurationSeconds } from "../../infra/format-time/format-duration.ts";
import { enqueueSystemEvent } from "../../infra/system-events.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
+1 -1
View File
@@ -4,7 +4,7 @@ import path from "node:path";
import process from "node:process";
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
import { installProcessWarningFilter } from "./infra/warnings.js";
import { installProcessWarningFilter } from "./infra/warning-filter.js";
import { attachChildProcessBridge } from "./process/child-process-bridge.js";
process.title = "openclaw";
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { formatZonedTimestamp } from "../../auto-reply/envelope.js";
import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.js";
import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js";
describe("injectTimestamp", () => {
@@ -23,7 +23,7 @@ describe("injectTimestamp", () => {
it("uses channel envelope format with DOW prefix", () => {
const now = new Date();
const expected = formatZonedTimestamp(now, "America/New_York");
const expected = formatZonedTimestamp(now, { timeZone: "America/New_York" });
const result = injectTimestamp("hello", { timezone: "America/New_York" });
@@ -1,6 +1,6 @@
import type { OpenClawConfig } from "../../config/types.js";
import { resolveUserTimezone } from "../../agents/date-time.js";
import { formatZonedTimestamp } from "../../auto-reply/envelope.js";
import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.ts";
/**
* Cron jobs inject "Current time: ..." into their messages.
@@ -56,7 +56,7 @@ export function injectTimestamp(message: string, opts?: TimestampInjectionOption
const now = opts?.now ?? new Date();
const timezone = opts?.timezone ?? "UTC";
const formatted = formatZonedTimestamp(now, timezone);
const formatted = formatZonedTimestamp(now, { timeZone: timezone });
if (!formatted) {
return message;
}
+2 -20
View File
@@ -3,6 +3,7 @@ import { listChannelPlugins } from "../channels/plugins/index.js";
import { type OpenClawConfig, loadConfig } from "../config/config.js";
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
import { theme } from "../terminal/theme.js";
import { formatTimeAgo } from "./format-time/format-relative.ts";
export type ChannelSummaryOptions = {
colorize?: boolean;
@@ -224,7 +225,7 @@ export async function buildChannelSummary(
line += ` ${self.e164}`;
}
if (authAgeMs != null && authAgeMs >= 0) {
line += ` auth ${formatAge(authAgeMs)}`;
line += ` auth ${formatTimeAgo(authAgeMs)}`;
}
lines.push(tint(line, statusColor));
@@ -252,22 +253,3 @@ export async function buildChannelSummary(
return lines;
}
export function formatAge(ms: number): string {
if (ms < 0) {
return "unknown";
}
const minutes = Math.round(ms / 60_000);
if (minutes < 1) {
return "just now";
}
if (minutes < 60) {
return `${minutes}m ago`;
}
const hours = Math.round(minutes / 60);
if (hours < 48) {
return `${hours}h ago`;
}
const days = Math.round(hours / 24);
return `${days}d ago`;
}
-37
View File
@@ -1,37 +0,0 @@
export type FormatDurationSecondsOptions = {
decimals?: number;
unit?: "s" | "seconds";
};
export function formatDurationSeconds(
ms: number,
options: FormatDurationSecondsOptions = {},
): string {
if (!Number.isFinite(ms)) {
return "unknown";
}
const decimals = options.decimals ?? 1;
const unit = options.unit ?? "s";
const seconds = Math.max(0, ms) / 1000;
const fixed = seconds.toFixed(Math.max(0, decimals));
const trimmed = fixed.replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1");
return unit === "seconds" ? `${trimmed} seconds` : `${trimmed}s`;
}
export type FormatDurationMsOptions = {
decimals?: number;
unit?: "s" | "seconds";
};
export function formatDurationMs(ms: number, options: FormatDurationMsOptions = {}): string {
if (!Number.isFinite(ms)) {
return "unknown";
}
if (ms < 1000) {
return `${ms}ms`;
}
return formatDurationSeconds(ms, {
decimals: options.decimals ?? 2,
unit: options.unit ?? "s",
});
}
+94
View File
@@ -0,0 +1,94 @@
/**
* Centralized date/time formatting utilities.
*
* All formatters are timezone-aware, using Intl.DateTimeFormat.
* Consolidates duplicated formatUtcTimestamp / formatZonedTimestamp / resolveExplicitTimezone
* that previously lived in envelope.ts and session-updates.ts.
*/
/**
* Validate an IANA timezone string. Returns the string if valid, undefined otherwise.
*/
export function resolveTimezone(value: string): string | undefined {
try {
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
return value;
} catch {
return undefined;
}
}
export type FormatTimestampOptions = {
/** Include seconds in the output. Default: false */
displaySeconds?: boolean;
};
export type FormatZonedTimestampOptions = FormatTimestampOptions & {
/** IANA timezone string (e.g., 'America/New_York'). Default: system timezone */
timeZone?: string;
};
/**
* Format a Date as a UTC timestamp string.
*
* Without seconds: `2024-01-15T14:30Z`
* With seconds: `2024-01-15T14:30:05Z`
*/
export function formatUtcTimestamp(date: Date, options?: FormatTimestampOptions): string {
const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
const dd = String(date.getUTCDate()).padStart(2, "0");
const hh = String(date.getUTCHours()).padStart(2, "0");
const min = String(date.getUTCMinutes()).padStart(2, "0");
if (!options?.displaySeconds) {
return `${yyyy}-${mm}-${dd}T${hh}:${min}Z`;
}
const sec = String(date.getUTCSeconds()).padStart(2, "0");
return `${yyyy}-${mm}-${dd}T${hh}:${min}:${sec}Z`;
}
/**
* Format a Date with timezone display using Intl.DateTimeFormat.
*
* Without seconds: `2024-01-15 14:30 EST`
* With seconds: `2024-01-15 14:30:05 EST`
*
* Returns undefined if Intl formatting fails.
*/
export function formatZonedTimestamp(
date: Date,
options?: FormatZonedTimestampOptions,
): string | undefined {
const intlOptions: Intl.DateTimeFormatOptions = {
timeZone: options?.timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
timeZoneName: "short",
};
if (options?.displaySeconds) {
intlOptions.second = "2-digit";
}
const parts = new Intl.DateTimeFormat("en-US", intlOptions).formatToParts(date);
const pick = (type: string) => parts.find((part) => part.type === type)?.value;
const yyyy = pick("year");
const mm = pick("month");
const dd = pick("day");
const hh = pick("hour");
const min = pick("minute");
const sec = options?.displaySeconds ? pick("second") : undefined;
const tz = [...parts]
.toReversed()
.find((part) => part.type === "timeZoneName")
?.value?.trim();
if (!yyyy || !mm || !dd || !hh || !min) {
return undefined;
}
if (options?.displaySeconds && sec) {
return `${yyyy}-${mm}-${dd} ${hh}:${min}:${sec}${tz ? ` ${tz}` : ""}`;
}
return `${yyyy}-${mm}-${dd} ${hh}:${min}${tz ? ` ${tz}` : ""}`;
}
+103
View File
@@ -0,0 +1,103 @@
export type FormatDurationSecondsOptions = {
decimals?: number;
unit?: "s" | "seconds";
};
export type FormatDurationCompactOptions = {
/** Add space between units: "2m 5s" instead of "2m5s". Default: false */
spaced?: boolean;
};
export function formatDurationSeconds(
ms: number,
options: FormatDurationSecondsOptions = {},
): string {
if (!Number.isFinite(ms)) {
return "unknown";
}
const decimals = options.decimals ?? 1;
const unit = options.unit ?? "s";
const seconds = Math.max(0, ms) / 1000;
const fixed = seconds.toFixed(Math.max(0, decimals));
const trimmed = fixed.replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1");
return unit === "seconds" ? `${trimmed} seconds` : `${trimmed}s`;
}
/** Precise decimal-seconds output: "500ms" or "1.23s". Input is milliseconds. */
export function formatDurationPrecise(
ms: number,
options: FormatDurationSecondsOptions = {},
): string {
if (!Number.isFinite(ms)) {
return "unknown";
}
if (ms < 1000) {
return `${ms}ms`;
}
return formatDurationSeconds(ms, {
decimals: options.decimals ?? 2,
unit: options.unit ?? "s",
});
}
/**
* Compact compound duration: "500ms", "45s", "2m5s", "1h30m".
* With `spaced`: "45s", "2m 5s", "1h 30m".
* Omits trailing zero components: "1m" not "1m 0s", "2h" not "2h 0m".
* Returns undefined for null/undefined/non-finite/non-positive input.
*/
export function formatDurationCompact(
ms?: number | null,
options?: FormatDurationCompactOptions,
): string | undefined {
if (ms == null || !Number.isFinite(ms) || ms <= 0) {
return undefined;
}
if (ms < 1000) {
return `${Math.round(ms)}ms`;
}
const sep = options?.spaced ? " " : "";
const totalSeconds = Math.round(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours >= 24) {
const days = Math.floor(hours / 24);
const remainingHours = hours % 24;
return remainingHours > 0 ? `${days}d${sep}${remainingHours}h` : `${days}d`;
}
if (hours > 0) {
return minutes > 0 ? `${hours}h${sep}${minutes}m` : `${hours}h`;
}
if (minutes > 0) {
return seconds > 0 ? `${minutes}m${sep}${seconds}s` : `${minutes}m`;
}
return `${seconds}s`;
}
/**
* Rounded single-unit duration for display: "500ms", "5s", "3m", "2h", "5d".
* Returns fallback string for null/undefined/non-finite input.
*/
export function formatDurationHuman(ms?: number | null, fallback = "n/a"): string {
if (ms == null || !Number.isFinite(ms) || ms < 0) {
return fallback;
}
if (ms < 1000) {
return `${Math.round(ms)}ms`;
}
const sec = Math.round(ms / 1000);
if (sec < 60) {
return `${sec}s`;
}
const min = Math.round(sec / 60);
if (min < 60) {
return `${min}m`;
}
const hr = Math.round(min / 60);
if (hr < 24) {
return `${hr}h`;
}
const day = Math.round(hr / 24);
return `${day}d`;
}
+112
View File
@@ -0,0 +1,112 @@
/**
* Centralized relative-time formatting utilities.
*
* Consolidates 7+ scattered implementations (formatAge, formatAgeShort, formatAgo,
* formatRelativeTime, formatElapsedTime) into two functions:
*
* - `formatTimeAgo(durationMs)` format a duration as "5m ago" / "5m" (for known elapsed time)
* - `formatRelativeTimestamp(epochMs)` format an epoch timestamp relative to now (handles future)
*/
export type FormatTimeAgoOptions = {
/** Append "ago" suffix. Default: true. When false, returns bare unit: "5m", "2h" */
suffix?: boolean;
/** Return value for invalid/null/negative input. Default: "unknown" */
fallback?: string;
};
/**
* Format a duration (in ms) as a human-readable relative time.
*
* Input: how many milliseconds ago something happened.
*
* With suffix (default): "just now", "5m ago", "3h ago", "2d ago"
* Without suffix: "0s", "5m", "3h", "2d"
*/
export function formatTimeAgo(
durationMs: number | null | undefined,
options?: FormatTimeAgoOptions,
): string {
const suffix = options?.suffix !== false;
const fallback = options?.fallback ?? "unknown";
if (durationMs == null || !Number.isFinite(durationMs) || durationMs < 0) {
return fallback;
}
const totalSeconds = Math.round(durationMs / 1000);
const minutes = Math.round(totalSeconds / 60);
if (minutes < 1) {
return suffix ? "just now" : `${totalSeconds}s`;
}
if (minutes < 60) {
return suffix ? `${minutes}m ago` : `${minutes}m`;
}
const hours = Math.round(minutes / 60);
if (hours < 48) {
return suffix ? `${hours}h ago` : `${hours}h`;
}
const days = Math.round(hours / 24);
return suffix ? `${days}d ago` : `${days}d`;
}
export type FormatRelativeTimestampOptions = {
/** If true, fall back to short date (e.g. "Oct 5") for timestamps >7 days. Default: false */
dateFallback?: boolean;
/** IANA timezone for date fallback display */
timezone?: string;
/** Return value for invalid/null input. Default: "n/a" */
fallback?: string;
};
/**
* Format an epoch timestamp relative to now.
*
* Handles both past ("5m ago") and future ("in 5m") timestamps.
* Optionally falls back to a short date for timestamps older than 7 days.
*/
export function formatRelativeTimestamp(
timestampMs: number | null | undefined,
options?: FormatRelativeTimestampOptions,
): string {
const fallback = options?.fallback ?? "n/a";
if (timestampMs == null || !Number.isFinite(timestampMs)) {
return fallback;
}
const diff = Date.now() - timestampMs;
const absDiff = Math.abs(diff);
const isPast = diff >= 0;
const sec = Math.round(absDiff / 1000);
if (sec < 60) {
return isPast ? "just now" : "in <1m";
}
const min = Math.round(sec / 60);
if (min < 60) {
return isPast ? `${min}m ago` : `in ${min}m`;
}
const hr = Math.round(min / 60);
if (hr < 48) {
return isPast ? `${hr}h ago` : `in ${hr}h`;
}
const day = Math.round(hr / 24);
if (!options?.dateFallback || day <= 7) {
return isPast ? `${day}d ago` : `in ${day}d`;
}
// Fall back to short date display for old timestamps
try {
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
...(options.timezone ? { timeZone: options.timezone } : {}),
}).format(new Date(timestampMs));
} catch {
return `${day}d ago`;
}
}
+221
View File
@@ -0,0 +1,221 @@
import { describe, expect, it } from "vitest";
import { formatUtcTimestamp, formatZonedTimestamp, resolveTimezone } from "./format-datetime.js";
import {
formatDurationCompact,
formatDurationHuman,
formatDurationPrecise,
formatDurationSeconds,
} from "./format-duration.js";
import { formatTimeAgo, formatRelativeTimestamp } from "./format-relative.js";
describe("format-duration", () => {
describe("formatDurationCompact", () => {
it("returns undefined for null/undefined/non-positive", () => {
expect(formatDurationCompact(null)).toBeUndefined();
expect(formatDurationCompact(undefined)).toBeUndefined();
expect(formatDurationCompact(0)).toBeUndefined();
expect(formatDurationCompact(-100)).toBeUndefined();
});
it("formats milliseconds for sub-second durations", () => {
expect(formatDurationCompact(500)).toBe("500ms");
expect(formatDurationCompact(999)).toBe("999ms");
});
it("formats seconds", () => {
expect(formatDurationCompact(1000)).toBe("1s");
expect(formatDurationCompact(45000)).toBe("45s");
expect(formatDurationCompact(59000)).toBe("59s");
});
it("formats minutes and seconds", () => {
expect(formatDurationCompact(60000)).toBe("1m");
expect(formatDurationCompact(65000)).toBe("1m5s");
expect(formatDurationCompact(90000)).toBe("1m30s");
});
it("omits trailing zero components", () => {
expect(formatDurationCompact(60000)).toBe("1m"); // not "1m0s"
expect(formatDurationCompact(3600000)).toBe("1h"); // not "1h0m"
expect(formatDurationCompact(86400000)).toBe("1d"); // not "1d0h"
});
it("formats hours and minutes", () => {
expect(formatDurationCompact(3660000)).toBe("1h1m");
expect(formatDurationCompact(5400000)).toBe("1h30m");
});
it("formats days and hours", () => {
expect(formatDurationCompact(90000000)).toBe("1d1h");
expect(formatDurationCompact(172800000)).toBe("2d");
});
it("supports spaced option", () => {
expect(formatDurationCompact(65000, { spaced: true })).toBe("1m 5s");
expect(formatDurationCompact(3660000, { spaced: true })).toBe("1h 1m");
expect(formatDurationCompact(90000000, { spaced: true })).toBe("1d 1h");
});
it("rounds at boundaries", () => {
// 59.5 seconds rounds to 60s = 1m
expect(formatDurationCompact(59500)).toBe("1m");
// 59.4 seconds rounds to 59s
expect(formatDurationCompact(59400)).toBe("59s");
});
});
describe("formatDurationHuman", () => {
it("returns fallback for invalid input", () => {
expect(formatDurationHuman(null)).toBe("n/a");
expect(formatDurationHuman(undefined)).toBe("n/a");
expect(formatDurationHuman(-100)).toBe("n/a");
expect(formatDurationHuman(null, "unknown")).toBe("unknown");
});
it("formats single unit", () => {
expect(formatDurationHuman(500)).toBe("500ms");
expect(formatDurationHuman(5000)).toBe("5s");
expect(formatDurationHuman(180000)).toBe("3m");
expect(formatDurationHuman(7200000)).toBe("2h");
expect(formatDurationHuman(172800000)).toBe("2d");
});
it("uses 24h threshold for days", () => {
expect(formatDurationHuman(23 * 3600000)).toBe("23h");
expect(formatDurationHuman(24 * 3600000)).toBe("1d");
expect(formatDurationHuman(25 * 3600000)).toBe("1d"); // rounds
});
});
describe("formatDurationPrecise", () => {
it("shows milliseconds for sub-second", () => {
expect(formatDurationPrecise(500)).toBe("500ms");
expect(formatDurationPrecise(999)).toBe("999ms");
});
it("shows decimal seconds for >=1s", () => {
expect(formatDurationPrecise(1000)).toBe("1s");
expect(formatDurationPrecise(1500)).toBe("1.5s");
expect(formatDurationPrecise(1234)).toBe("1.23s");
});
it("returns unknown for non-finite", () => {
expect(formatDurationPrecise(NaN)).toBe("unknown");
expect(formatDurationPrecise(Infinity)).toBe("unknown");
});
});
describe("formatDurationSeconds", () => {
it("formats with configurable decimals", () => {
expect(formatDurationSeconds(1500, { decimals: 1 })).toBe("1.5s");
expect(formatDurationSeconds(1234, { decimals: 2 })).toBe("1.23s");
expect(formatDurationSeconds(1000, { decimals: 0 })).toBe("1s");
});
it("supports seconds unit", () => {
expect(formatDurationSeconds(2000, { unit: "seconds" })).toBe("2 seconds");
});
});
});
describe("format-datetime", () => {
describe("resolveTimezone", () => {
it("returns valid IANA timezone strings", () => {
expect(resolveTimezone("America/New_York")).toBe("America/New_York");
expect(resolveTimezone("Europe/London")).toBe("Europe/London");
expect(resolveTimezone("UTC")).toBe("UTC");
});
it("returns undefined for invalid timezones", () => {
expect(resolveTimezone("Invalid/Timezone")).toBeUndefined();
expect(resolveTimezone("garbage")).toBeUndefined();
expect(resolveTimezone("")).toBeUndefined();
});
});
describe("formatUtcTimestamp", () => {
it("formats without seconds by default", () => {
const date = new Date("2024-01-15T14:30:45.000Z");
expect(formatUtcTimestamp(date)).toBe("2024-01-15T14:30Z");
});
it("includes seconds when requested", () => {
const date = new Date("2024-01-15T14:30:45.000Z");
expect(formatUtcTimestamp(date, { displaySeconds: true })).toBe("2024-01-15T14:30:45Z");
});
});
describe("formatZonedTimestamp", () => {
it("formats with timezone abbreviation", () => {
const date = new Date("2024-01-15T14:30:00.000Z");
const result = formatZonedTimestamp(date, { timeZone: "UTC" });
expect(result).toMatch(/2024-01-15 14:30/);
});
it("includes seconds when requested", () => {
const date = new Date("2024-01-15T14:30:45.000Z");
const result = formatZonedTimestamp(date, { timeZone: "UTC", displaySeconds: true });
expect(result).toMatch(/2024-01-15 14:30:45/);
});
});
});
describe("format-relative", () => {
describe("formatTimeAgo", () => {
it("returns fallback for invalid input", () => {
expect(formatTimeAgo(null)).toBe("unknown");
expect(formatTimeAgo(undefined)).toBe("unknown");
expect(formatTimeAgo(-100)).toBe("unknown");
expect(formatTimeAgo(null, { fallback: "n/a" })).toBe("n/a");
});
it("formats with 'ago' suffix by default", () => {
expect(formatTimeAgo(0)).toBe("just now");
expect(formatTimeAgo(29000)).toBe("just now"); // rounds to <1m
expect(formatTimeAgo(30000)).toBe("1m ago"); // 30s rounds to 1m
expect(formatTimeAgo(300000)).toBe("5m ago");
expect(formatTimeAgo(7200000)).toBe("2h ago");
expect(formatTimeAgo(172800000)).toBe("2d ago");
});
it("omits suffix when suffix: false", () => {
expect(formatTimeAgo(0, { suffix: false })).toBe("0s");
expect(formatTimeAgo(300000, { suffix: false })).toBe("5m");
expect(formatTimeAgo(7200000, { suffix: false })).toBe("2h");
});
it("uses 48h threshold before switching to days", () => {
expect(formatTimeAgo(47 * 3600000)).toBe("47h ago");
expect(formatTimeAgo(48 * 3600000)).toBe("2d ago");
});
});
describe("formatRelativeTimestamp", () => {
it("returns fallback for invalid input", () => {
expect(formatRelativeTimestamp(null)).toBe("n/a");
expect(formatRelativeTimestamp(undefined)).toBe("n/a");
expect(formatRelativeTimestamp(null, { fallback: "unknown" })).toBe("unknown");
});
it("formats past timestamps", () => {
const now = Date.now();
expect(formatRelativeTimestamp(now - 10000)).toBe("just now");
expect(formatRelativeTimestamp(now - 300000)).toBe("5m ago");
expect(formatRelativeTimestamp(now - 7200000)).toBe("2h ago");
});
it("formats future timestamps", () => {
const now = Date.now();
expect(formatRelativeTimestamp(now + 30000)).toBe("in <1m");
expect(formatRelativeTimestamp(now + 300000)).toBe("in 5m");
expect(formatRelativeTimestamp(now + 7200000)).toBe("in 2h");
});
it("falls back to date for old timestamps when enabled", () => {
const oldDate = Date.now() - 30 * 24 * 3600000; // 30 days ago
const result = formatRelativeTimestamp(oldDate, { dateFallback: true });
// Should be a short date like "Jan 9" not "30d ago"
expect(result).toMatch(/[A-Z][a-z]{2} \d{1,2}/);
});
});
});
-1
View File
@@ -1 +0,0 @@
export { installProcessWarningFilter } from "./warning-filter.js";
+16 -6
View File
@@ -59,7 +59,9 @@ function getVoyageHeaders(
}
function splitVoyageBatchRequests(requests: VoyageBatchRequest[]): VoyageBatchRequest[][] {
if (requests.length <= VOYAGE_BATCH_MAX_REQUESTS) return [requests];
if (requests.length <= VOYAGE_BATCH_MAX_REQUESTS) {
return [requests];
}
const groups: VoyageBatchRequest[][] = [];
for (let i = 0; i < requests.length; i += VOYAGE_BATCH_MAX_REQUESTS) {
groups.push(requests.slice(i, i + VOYAGE_BATCH_MAX_REQUESTS));
@@ -170,7 +172,9 @@ async function readVoyageBatchError(params: {
throw new Error(`voyage batch error file content failed: ${res.status} ${text}`);
}
const text = await res.text();
if (!text.trim()) return undefined;
if (!text.trim()) {
return undefined;
}
const lines = text
.split("\n")
.map((line) => line.trim())
@@ -246,7 +250,9 @@ export async function runVoyageEmbeddingBatches(params: {
concurrency: number;
debug?: (message: string, data?: Record<string, unknown>) => void;
}): Promise<Map<string, number[]>> {
if (params.requests.length === 0) return new Map();
if (params.requests.length === 0) {
return new Map();
}
const groups = splitVoyageBatchRequests(params.requests);
const byCustomId = new Map<string, number[]>();
@@ -307,15 +313,19 @@ export async function runVoyageEmbeddingBatches(params: {
if (contentRes.body) {
const reader = createInterface({
input: Readable.fromWeb(contentRes.body as any),
input: Readable.fromWeb(contentRes.body as unknown as import("stream/web").ReadableStream),
terminal: false,
});
for await (const rawLine of reader) {
if (!rawLine.trim()) continue;
if (!rawLine.trim()) {
continue;
}
const line = JSON.parse(rawLine) as VoyageBatchOutputLine;
const customId = line.custom_id;
if (!customId) continue;
if (!customId) {
continue;
}
remaining.delete(customId);
if (line.error?.message) {
errors.push(`${customId}: ${line.error.message}`);
+3 -1
View File
@@ -3,7 +3,9 @@ import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("../agents/model-auth.js", () => ({
resolveApiKeyForProvider: vi.fn(),
requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => {
if (auth?.apiKey) return auth.apiKey;
if (auth?.apiKey) {
return auth.apiKey;
}
throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`);
},
}));
+12 -4
View File
@@ -12,8 +12,12 @@ const DEFAULT_VOYAGE_BASE_URL = "https://api.voyageai.com/v1";
export function normalizeVoyageModel(model: string): string {
const trimmed = model.trim();
if (!trimmed) return DEFAULT_VOYAGE_EMBEDDING_MODEL;
if (trimmed.startsWith("voyage/")) return trimmed.slice("voyage/".length);
if (!trimmed) {
return DEFAULT_VOYAGE_EMBEDDING_MODEL;
}
if (trimmed.startsWith("voyage/")) {
return trimmed.slice("voyage/".length);
}
return trimmed;
}
@@ -24,12 +28,16 @@ export async function createVoyageEmbeddingProvider(
const url = `${client.baseUrl.replace(/\/$/, "")}/embeddings`;
const embed = async (input: string[], input_type?: "query" | "document"): Promise<number[][]> => {
if (input.length === 0) return [];
if (input.length === 0) {
return [];
}
const body: { model: string; input: string[]; input_type?: "query" | "document" } = {
model: client.model,
input,
};
if (input_type) body.input_type = input_type;
if (input_type) {
body.input_type = input_type;
}
const res = await fetch(url, {
method: "POST",
+12 -4
View File
@@ -280,7 +280,9 @@ export async function runWithConcurrency<T>(
tasks: Array<() => Promise<T>>,
limit: number,
): Promise<T[]> {
if (tasks.length === 0) return [];
if (tasks.length === 0) {
return [];
}
const resolvedLimit = Math.max(1, Math.min(limit, tasks.length));
const results: T[] = Array.from({ length: tasks.length });
let next = 0;
@@ -288,10 +290,14 @@ export async function runWithConcurrency<T>(
const workers = Array.from({ length: resolvedLimit }, async () => {
while (true) {
if (firstError) return;
if (firstError) {
return;
}
const index = next;
next += 1;
if (index >= tasks.length) return;
if (index >= tasks.length) {
return;
}
try {
results[index] = await tasks[index]();
} catch (err) {
@@ -302,6 +308,8 @@ export async function runWithConcurrency<T>(
});
await Promise.allSettled(workers);
if (firstError) throw firstError;
if (firstError) {
throw firstError;
}
return results;
}
+12 -4
View File
@@ -1890,7 +1890,9 @@ export class MemoryIndexManager implements MemorySearchManager {
if (!voyage) {
return this.embedChunksInBatches(chunks);
}
if (chunks.length === 0) return [];
if (chunks.length === 0) {
return [];
}
const cached = this.loadEmbeddingCache(chunks.map((chunk) => chunk.hash));
const embeddings: number[][] = Array.from({ length: chunks.length }, () => []);
const missing: Array<{ index: number; chunk: MemoryChunk }> = [];
@@ -1905,7 +1907,9 @@ export class MemoryIndexManager implements MemorySearchManager {
}
}
if (missing.length === 0) return embeddings;
if (missing.length === 0) {
return embeddings;
}
const requests: VoyageBatchRequest[] = [];
const mapping = new Map<string, { index: number; hash: string }>();
@@ -1937,13 +1941,17 @@ export class MemoryIndexManager implements MemorySearchManager {
}),
fallback: async () => await this.embedChunksInBatches(chunks),
});
if (Array.isArray(batchResult)) return batchResult;
if (Array.isArray(batchResult)) {
return batchResult;
}
const byCustomId = batchResult;
const toCache: Array<{ hash: string; embedding: number[] }> = [];
for (const [customId, embedding] of byCustomId.entries()) {
const mapped = mapping.get(customId);
if (!mapped) continue;
if (!mapped) {
continue;
}
embeddings[mapped.index] = embedding;
toCache.push({ hash: mapped.hash, embedding });
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { createRequire } from "node:module";
import { installProcessWarningFilter } from "../infra/warnings.js";
import { installProcessWarningFilter } from "../infra/warning-filter.js";
const require = createRequire(import.meta.url);
+2 -2
View File
@@ -5,7 +5,7 @@ import { resolveAgentMaxConcurrent } from "../config/agent-limits.js";
import { loadConfig } from "../config/config.js";
import { computeBackoff, sleepWithAbort } from "../infra/backoff.js";
import { formatErrorMessage } from "../infra/errors.js";
import { formatDurationMs } from "../infra/format-duration.js";
import { formatDurationPrecise } from "../infra/format-time/format-duration.ts";
import { registerUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
import { resolveTelegramAccount } from "./accounts.js";
import { resolveTelegramAllowedUpdates } from "./allowed-updates.js";
@@ -195,7 +195,7 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
const reason = isConflict ? "getUpdates conflict" : "network error";
const errMsg = formatErrorMessage(err);
(opts.runtime?.error ?? console.error)(
`Telegram ${reason}: ${errMsg}; retrying in ${formatDurationMs(delayMs)}.`,
`Telegram ${reason}: ${errMsg}; retrying in ${formatDurationPrecise(delayMs)}.`,
);
try {
await sleepWithAbort(delayMs, opts.abortSignal);
+4 -2
View File
@@ -14,8 +14,8 @@ import {
normalizeUsageDisplay,
resolveResponseUsageMode,
} from "../auto-reply/thinking.js";
import { formatRelativeTimestamp } from "../infra/format-time/format-relative.ts";
import { normalizeAgentId } from "../routing/session-key.js";
import { formatRelativeTime } from "../utils/time-format.js";
import { helpText, parseCommand } from "./commands.js";
import {
createFilterableSelectList,
@@ -158,7 +158,9 @@ export function createCommandHandlers(context: CommandHandlerContext) {
// Avoid redundant "title (key)" when title matches key
const label = title && title !== formattedKey ? `${title} (${formattedKey})` : formattedKey;
// Build description: time + message preview
const timePart = session.updatedAt ? formatRelativeTime(session.updatedAt) : "";
const timePart = session.updatedAt
? formatRelativeTimestamp(session.updatedAt, { dateFallback: true, fallback: "" })
: "";
const preview = session.lastMessagePreview?.replace(/\s+/g, " ").trim();
const description =
timePart && preview ? `${timePart} · ${preview}` : (preview ?? timePart);
+3 -3
View File
@@ -1,5 +1,5 @@
import type { GatewayStatusSummary } from "./tui-types.js";
import { formatAge } from "../infra/channel-summary.js";
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
import { formatTokenCount } from "../utils/usage-format.js";
import { formatContextUsageLine } from "./tui-formatters.js";
@@ -14,7 +14,7 @@ export function formatStatusSummary(summary: GatewayStatusSummary) {
const linked = summary.linkChannel.linked === true;
const authAge =
linked && typeof summary.linkChannel.authAgeMs === "number"
? ` (last refreshed ${formatAge(summary.linkChannel.authAgeMs)})`
? ` (last refreshed ${formatTimeAgo(summary.linkChannel.authAgeMs)})`
: "";
lines.push(`${linkLabel}: ${linked ? "linked" : "not linked"}${authAge}`);
}
@@ -63,7 +63,7 @@ export function formatStatusSummary(summary: GatewayStatusSummary) {
if (recent.length > 0) {
lines.push("Recent sessions:");
for (const entry of recent) {
const ageLabel = typeof entry.age === "number" ? formatAge(entry.age) : "no activity";
const ageLabel = typeof entry.age === "number" ? formatTimeAgo(entry.age) : "no activity";
const model = entry.model ?? "unknown";
const usage = formatContextUsageLine({
total: entry.totalTokens ?? null,
+4 -23
View File
@@ -1,25 +1,6 @@
export function formatRelativeTime(timestamp: number): string {
const now = Date.now();
const diff = now - timestamp;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
import { formatRelativeTimestamp } from "../infra/format-time/format-relative.ts";
if (seconds < 60) {
return "just now";
}
if (minutes < 60) {
return `${minutes}m ago`;
}
if (hours < 24) {
return `${hours}h ago`;
}
if (days === 1) {
return "Yesterday";
}
if (days < 7) {
return `${days}d ago`;
}
return new Date(timestamp).toLocaleDateString(undefined, { month: "short", day: "numeric" });
/** Delegates to centralized formatRelativeTimestamp with date fallback for >7d. */
export function formatRelativeTime(timestamp: number): string {
return formatRelativeTimestamp(timestamp, { dateFallback: true, fallback: "unknown" });
}
+2 -2
View File
@@ -7,7 +7,7 @@ import { formatCliCommand } from "../../cli/command-format.js";
import { waitForever } from "../../cli/wait.js";
import { loadConfig } from "../../config/config.js";
import { logVerbose } from "../../globals.js";
import { formatDurationMs } from "../../infra/format-duration.js";
import { formatDurationPrecise } from "../../infra/format-time/format-duration.ts";
import { enqueueSystemEvent } from "../../infra/system-events.js";
import { registerUnhandledRejectionHandler } from "../../infra/unhandled-rejections.js";
import { getChildLogger } from "../../logging.js";
@@ -432,7 +432,7 @@ export async function monitorWebChannel(
"web reconnect: scheduling retry",
);
runtime.error(
`WhatsApp Web connection closed (status ${statusCode}). Retry ${reconnectAttempts}/${reconnectPolicy.maxAttempts || "∞"} in ${formatDurationMs(delay)}… (${errorStr})`,
`WhatsApp Web connection closed (status ${statusCode}). Retry ${reconnectAttempts}/${reconnectPolicy.maxAttempts || "∞"} in ${formatDurationPrecise(delay)}… (${errorStr})`,
);
await closeListener();
try {