From 4e4aeacae4a87f2fe09125d462d88cd71a18cb66 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 08:13:45 +0100 Subject: [PATCH] perf: slim hot test imports --- ...tize-lastgood-round-robin-ordering.test.ts | 2 +- ...normalizes-z-ai-aliases-auth-order.test.ts | 3 +- ...-lastused-no-explicit-order-exists.test.ts | 2 +- ...s-stored-profiles-no-config-exists.test.ts | 2 +- src/agents/auth-profiles/order.ts | 6 +- src/agents/auth-profiles/profile-list.ts | 13 ++ src/agents/auth-profiles/profiles.ts | 13 +- src/agents/auth-profiles/repair.ts | 2 +- src/agents/auth-profiles/usage-state.ts | 138 ++++++++++++++++ src/agents/auth-profiles/usage.ts | 151 ++---------------- ...s-back-provider-default-per-dm-not.test.ts | 2 +- ...-undefined-sessionkey-is-undefined.test.ts | 2 +- ...ner.history-limit-from-session-key.test.ts | 2 +- ...-embedded-runner.limithistoryturns.test.ts | 2 +- .../reply/commands-subagents/action-info.ts | 96 +++++++++-- 15 files changed, 264 insertions(+), 172 deletions(-) create mode 100644 src/agents/auth-profiles/profile-list.ts create mode 100644 src/agents/auth-profiles/usage-state.ts diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts index 3e6437d7d27..5e50e6350bd 100644 --- a/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts +++ b/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { resolveAuthProfileOrder } from "./auth-profiles.js"; import { ANTHROPIC_CFG, ANTHROPIC_STORE, } from "./auth-profiles.resolve-auth-profile-order.fixtures.js"; +import { resolveAuthProfileOrder } from "./auth-profiles/order.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; describe("resolveAuthProfileOrder", () => { diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.normalizes-z-ai-aliases-auth-order.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.normalizes-z-ai-aliases-auth-order.test.ts index 9fe9b9dbb68..ce903f1bfb7 100644 --- a/src/agents/auth-profiles.resolve-auth-profile-order.normalizes-z-ai-aliases-auth-order.test.ts +++ b/src/agents/auth-profiles.resolve-auth-profile-order.normalizes-z-ai-aliases-auth-order.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; -import { type AuthProfileStore, resolveAuthProfileOrder } from "./auth-profiles.js"; +import { resolveAuthProfileOrder } from "./auth-profiles/order.js"; +import type { AuthProfileStore } from "./auth-profiles/types.js"; function makeApiKeyStore(provider: string, profileIds: string[]): AuthProfileStore { return { diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts index c9af0a6ac0a..d569d072b9f 100644 --- a/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts +++ b/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { resolveAuthProfileOrder } from "./auth-profiles.js"; +import { resolveAuthProfileOrder } from "./auth-profiles/order.js"; describe("resolveAuthProfileOrder", () => { it("orders by lastUsed when no explicit order exists", () => { diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.uses-stored-profiles-no-config-exists.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.uses-stored-profiles-no-config-exists.test.ts index ec6f0f6c3b9..5c8b44bf6d5 100644 --- a/src/agents/auth-profiles.resolve-auth-profile-order.uses-stored-profiles-no-config-exists.test.ts +++ b/src/agents/auth-profiles.resolve-auth-profile-order.uses-stored-profiles-no-config-exists.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { resolveAuthProfileOrder } from "./auth-profiles.js"; import { ANTHROPIC_CFG, ANTHROPIC_STORE, } from "./auth-profiles.resolve-auth-profile-order.fixtures.js"; +import { resolveAuthProfileOrder } from "./auth-profiles/order.js"; describe("resolveAuthProfileOrder", () => { const store = ANTHROPIC_STORE; diff --git a/src/agents/auth-profiles/order.ts b/src/agents/auth-profiles/order.ts index e6738bca740..7d8f4bb18e0 100644 --- a/src/agents/auth-profiles/order.ts +++ b/src/agents/auth-profiles/order.ts @@ -1,17 +1,17 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js"; import { resolveProviderIdForAuth } from "../provider-auth-aliases.js"; +import { findNormalizedProviderValue, normalizeProviderId } from "../provider-id.js"; import { evaluateStoredCredentialEligibility, type AuthCredentialReasonCode, } from "./credential-state.js"; -import { dedupeProfileIds, listProfilesForProvider } from "./profiles.js"; +import { dedupeProfileIds, listProfilesForProvider } from "./profile-list.js"; import type { AuthProfileStore } from "./types.js"; import { clearExpiredCooldowns, isProfileInCooldown, resolveProfileUnusableUntil, -} from "./usage.js"; +} from "./usage-state.js"; export type AuthProfileEligibilityReasonCode = | AuthCredentialReasonCode diff --git a/src/agents/auth-profiles/profile-list.ts b/src/agents/auth-profiles/profile-list.ts new file mode 100644 index 00000000000..3b1ce9aacf6 --- /dev/null +++ b/src/agents/auth-profiles/profile-list.ts @@ -0,0 +1,13 @@ +import { resolveProviderIdForAuth } from "../provider-auth-aliases.js"; +import type { AuthProfileStore } from "./types.js"; + +export function dedupeProfileIds(profileIds: string[]): string[] { + return [...new Set(profileIds)]; +} + +export function listProfilesForProvider(store: AuthProfileStore, provider: string): string[] { + const providerKey = resolveProviderIdForAuth(provider); + return Object.entries(store.profiles) + .filter(([, cred]) => resolveProviderIdForAuth(cred.provider) === providerKey) + .map(([id]) => id); +} diff --git a/src/agents/auth-profiles/profiles.ts b/src/agents/auth-profiles/profiles.ts index 9ad9e3ae8e9..ad06751566a 100644 --- a/src/agents/auth-profiles/profiles.ts +++ b/src/agents/auth-profiles/profiles.ts @@ -2,16 +2,14 @@ import { normalizeStringEntries } from "../../shared/string-normalization.js"; import { normalizeSecretInput } from "../../utils/normalize-secret-input.js"; import { resolveProviderIdForAuth } from "../provider-auth-aliases.js"; import { normalizeProviderId } from "../provider-id.js"; +import { dedupeProfileIds, listProfilesForProvider } from "./profile-list.js"; import { ensureAuthProfileStoreForLocalUpdate, saveAuthProfileStore, updateAuthProfileStoreWithLock, } from "./store.js"; import type { AuthProfileCredential, AuthProfileStore } from "./types.js"; - -export function dedupeProfileIds(profileIds: string[]): string[] { - return [...new Set(profileIds)]; -} +export { dedupeProfileIds, listProfilesForProvider } from "./profile-list.js"; export async function setAuthProfileOrder(params: { agentDir?: string; @@ -124,13 +122,6 @@ export async function removeProviderAuthProfilesWithLock(params: { }); } -export function listProfilesForProvider(store: AuthProfileStore, provider: string): string[] { - const providerKey = resolveProviderIdForAuth(provider); - return Object.entries(store.profiles) - .filter(([, cred]) => resolveProviderIdForAuth(cred.provider) === providerKey) - .map(([id]) => id); -} - export async function markAuthProfileGood(params: { store: AuthProfileStore; provider: string; diff --git a/src/agents/auth-profiles/repair.ts b/src/agents/auth-profiles/repair.ts index bd25d7096f8..7f66a76e76b 100644 --- a/src/agents/auth-profiles/repair.ts +++ b/src/agents/auth-profiles/repair.ts @@ -2,7 +2,7 @@ import type { AuthProfileConfig } from "../../config/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { findNormalizedProviderKey, normalizeProviderId } from "../provider-id.js"; import { resolveAuthProfileMetadata } from "./identity.js"; -import { dedupeProfileIds, listProfilesForProvider } from "./profiles.js"; +import { dedupeProfileIds, listProfilesForProvider } from "./profile-list.js"; import type { AuthProfileIdRepairResult, AuthProfileStore } from "./types.js"; function getProfileSuffix(profileId: string): string { diff --git a/src/agents/auth-profiles/usage-state.ts b/src/agents/auth-profiles/usage-state.ts new file mode 100644 index 00000000000..0bf0841d5cd --- /dev/null +++ b/src/agents/auth-profiles/usage-state.ts @@ -0,0 +1,138 @@ +import { normalizeProviderId } from "../provider-id.js"; +import type { AuthProfileStore, ProfileUsageStats } from "./types.js"; + +export function isAuthCooldownBypassedForProvider(provider: string | undefined): boolean { + const normalized = normalizeProviderId(provider ?? ""); + return normalized === "openrouter" || normalized === "kilocode"; +} + +export function resolveProfileUnusableUntil( + stats: Pick, +): number | null { + const values = [stats.cooldownUntil, stats.disabledUntil] + .filter((value): value is number => typeof value === "number") + .filter((value) => Number.isFinite(value) && value > 0); + if (values.length === 0) { + return null; + } + return Math.max(...values); +} + +export function isActiveUnusableWindow(until: number | undefined, now: number): boolean { + return typeof until === "number" && Number.isFinite(until) && until > 0 && now < until; +} + +export function shouldBypassModelScopedCooldown( + stats: Pick, + now: number, + forModel?: string, +): boolean { + return !!( + forModel && + stats.cooldownReason === "rate_limit" && + stats.cooldownModel && + stats.cooldownModel !== forModel && + !isActiveUnusableWindow(stats.disabledUntil, now) + ); +} + +/** + * Check if a profile is currently in cooldown (due to rate limits, overload, or other transient failures). + */ +export function isProfileInCooldown( + store: AuthProfileStore, + profileId: string, + now?: number, + forModel?: string, +): boolean { + if (isAuthCooldownBypassedForProvider(store.profiles[profileId]?.provider)) { + return false; + } + const stats = store.usageStats?.[profileId]; + if (!stats) { + return false; + } + const ts = now ?? Date.now(); + // Model-aware bypass: if the cooldown was caused by a rate_limit on a + // specific model and the caller is requesting a *different* model, allow it. + // We still honour any active billing/auth disable (`disabledUntil`) — those + // are profile-wide and must not be short-circuited by model scoping. + if (shouldBypassModelScopedCooldown(stats, ts, forModel)) { + return false; + } + const unusableUntil = resolveProfileUnusableUntil(stats); + return unusableUntil ? ts < unusableUntil : false; +} + +/** + * Clear expired cooldowns from all profiles in the store. + * + * When `cooldownUntil` or `disabledUntil` has passed, the corresponding fields + * are removed and error counters are reset so the profile gets a fresh start + * (circuit-breaker half-open -> closed). Without this, a stale `errorCount` + * causes the *next* transient failure to immediately escalate to a much longer + * cooldown -- the root cause of profiles appearing "stuck" after rate limits. + * + * `cooldownUntil` and `disabledUntil` are handled independently: if a profile + * has both and only one has expired, only that field is cleared. + * + * Mutates the in-memory store; disk persistence happens lazily on the next + * store write (e.g. `markAuthProfileUsed` / `markAuthProfileFailure`), which + * matches the existing save pattern throughout the auth-profiles module. + * + * @returns `true` if any profile was modified. + */ +export function clearExpiredCooldowns(store: AuthProfileStore, now?: number): boolean { + const usageStats = store.usageStats; + if (!usageStats) { + return false; + } + + const ts = now ?? Date.now(); + let mutated = false; + + for (const [profileId, stats] of Object.entries(usageStats)) { + if (!stats) { + continue; + } + + let profileMutated = false; + const cooldownExpired = + typeof stats.cooldownUntil === "number" && + Number.isFinite(stats.cooldownUntil) && + stats.cooldownUntil > 0 && + ts >= stats.cooldownUntil; + const disabledExpired = + typeof stats.disabledUntil === "number" && + Number.isFinite(stats.disabledUntil) && + stats.disabledUntil > 0 && + ts >= stats.disabledUntil; + + if (cooldownExpired) { + stats.cooldownUntil = undefined; + stats.cooldownReason = undefined; + stats.cooldownModel = undefined; + profileMutated = true; + } + if (disabledExpired) { + stats.disabledUntil = undefined; + stats.disabledReason = undefined; + profileMutated = true; + } + + // Reset error counters when ALL cooldowns have expired so the profile gets + // a fair retry window. Preserves lastFailureAt for the failureWindowMs + // decay check in computeNextProfileUsageStats. + if (profileMutated && !resolveProfileUnusableUntil(stats)) { + stats.errorCount = 0; + stats.failureCounts = undefined; + } + + if (profileMutated) { + usageStats[profileId] = stats; + mutated = true; + } + } + + return mutated; +} diff --git a/src/agents/auth-profiles/usage.ts b/src/agents/auth-profiles/usage.ts index 808951b203f..06d96fc70db 100644 --- a/src/agents/auth-profiles/usage.ts +++ b/src/agents/auth-profiles/usage.ts @@ -1,8 +1,21 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { normalizeProviderId } from "../model-selection.js"; +import { normalizeProviderId } from "../provider-id.js"; import { logAuthProfileFailureStateChange } from "./state-observation.js"; import { saveAuthProfileStore, updateAuthProfileStoreWithLock } from "./store.js"; import type { AuthProfileFailureReason, AuthProfileStore, ProfileUsageStats } from "./types.js"; +import { + clearExpiredCooldowns, + isActiveUnusableWindow, + isAuthCooldownBypassedForProvider, + isProfileInCooldown, + resolveProfileUnusableUntil, + shouldBypassModelScopedCooldown, +} from "./usage-state.js"; +export { + clearExpiredCooldowns, + isProfileInCooldown, + resolveProfileUnusableUntil, +} from "./usage-state.js"; const authProfileUsageDeps = { saveAuthProfileStore, @@ -70,11 +83,6 @@ type WhamCooldownProbeResult = { reason: string; }; -function isAuthCooldownBypassedForProvider(provider: string | undefined): boolean { - const normalized = normalizeProviderId(provider ?? ""); - return normalized === "openrouter" || normalized === "kilocode"; -} - function shouldProbeWhamForFailure( provider: string | undefined, reason: AuthProfileFailureReason, @@ -222,50 +230,6 @@ export async function probeWhamForCooldown( } } -export function resolveProfileUnusableUntil( - stats: Pick, -): number | null { - const values = [stats.cooldownUntil, stats.disabledUntil] - .filter((value): value is number => typeof value === "number") - .filter((value) => Number.isFinite(value) && value > 0); - if (values.length === 0) { - return null; - } - return Math.max(...values); -} - -/** - * Check if a profile is currently in cooldown (due to rate limits, overload, or other transient failures). - */ -export function isProfileInCooldown( - store: AuthProfileStore, - profileId: string, - now?: number, - forModel?: string, -): boolean { - if (isAuthCooldownBypassedForProvider(store.profiles[profileId]?.provider)) { - return false; - } - const stats = store.usageStats?.[profileId]; - if (!stats) { - return false; - } - const ts = now ?? Date.now(); - // Model-aware bypass: if the cooldown was caused by a rate_limit on a - // specific model and the caller is requesting a *different* model, allow it. - // We still honour any active billing/auth disable (`disabledUntil`) — those - // are profile-wide and must not be short-circuited by model scoping. - if (shouldBypassModelScopedCooldown(stats, ts, forModel)) { - return false; - } - const unusableUntil = resolveProfileUnusableUntil(stats); - return unusableUntil ? ts < unusableUntil : false; -} - -function isActiveUnusableWindow(until: number | undefined, now: number): boolean { - return typeof until === "number" && Number.isFinite(until) && until > 0 && now < until; -} - /** * Infer the most likely reason all candidate profiles are currently unavailable. * @@ -393,93 +357,6 @@ export function getSoonestCooldownExpiry( return Math.min(soonest, latestMatchingModelCooldown); } -function shouldBypassModelScopedCooldown( - stats: Pick, - now: number, - forModel?: string, -): boolean { - return !!( - forModel && - stats.cooldownReason === "rate_limit" && - stats.cooldownModel && - stats.cooldownModel !== forModel && - !isActiveUnusableWindow(stats.disabledUntil, now) - ); -} - -/** - * Clear expired cooldowns from all profiles in the store. - * - * When `cooldownUntil` or `disabledUntil` has passed, the corresponding fields - * are removed and error counters are reset so the profile gets a fresh start - * (circuit-breaker half-open → closed). Without this, a stale `errorCount` - * causes the *next* transient failure to immediately escalate to a much longer - * cooldown — the root cause of profiles appearing "stuck" after rate limits. - * - * `cooldownUntil` and `disabledUntil` are handled independently: if a profile - * has both and only one has expired, only that field is cleared. - * - * Mutates the in-memory store; disk persistence happens lazily on the next - * store write (e.g. `markAuthProfileUsed` / `markAuthProfileFailure`), which - * matches the existing save pattern throughout the auth-profiles module. - * - * @returns `true` if any profile was modified. - */ -export function clearExpiredCooldowns(store: AuthProfileStore, now?: number): boolean { - const usageStats = store.usageStats; - if (!usageStats) { - return false; - } - - const ts = now ?? Date.now(); - let mutated = false; - - for (const [profileId, stats] of Object.entries(usageStats)) { - if (!stats) { - continue; - } - - let profileMutated = false; - const cooldownExpired = - typeof stats.cooldownUntil === "number" && - Number.isFinite(stats.cooldownUntil) && - stats.cooldownUntil > 0 && - ts >= stats.cooldownUntil; - const disabledExpired = - typeof stats.disabledUntil === "number" && - Number.isFinite(stats.disabledUntil) && - stats.disabledUntil > 0 && - ts >= stats.disabledUntil; - - if (cooldownExpired) { - stats.cooldownUntil = undefined; - stats.cooldownReason = undefined; - stats.cooldownModel = undefined; - profileMutated = true; - } - if (disabledExpired) { - stats.disabledUntil = undefined; - stats.disabledReason = undefined; - profileMutated = true; - } - - // Reset error counters when ALL cooldowns have expired so the profile gets - // a fair retry window. Preserves lastFailureAt for the failureWindowMs - // decay check in computeNextProfileUsageStats. - if (profileMutated && !resolveProfileUnusableUntil(stats)) { - stats.errorCount = 0; - stats.failureCounts = undefined; - } - - if (profileMutated) { - usageStats[profileId] = stats; - mutated = true; - } - } - - return mutated; -} - /** * Mark a profile as successfully used. Resets error count and updates lastUsed. * Uses store lock to avoid overwriting concurrent usage updates. diff --git a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts index 9402a9d39a1..0cd8c8ebafb 100644 --- a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts +++ b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner.js"; +import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner/history.js"; describe("getDmHistoryLimitFromSessionKey", () => { it("falls back to provider default when per-DM not set", () => { diff --git a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts index b5b1017b540..86872e7286c 100644 --- a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts +++ b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner.js"; +import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner/history.js"; describe("getDmHistoryLimitFromSessionKey", () => { it("returns undefined when sessionKey is undefined", () => { diff --git a/src/agents/pi-embedded-runner.history-limit-from-session-key.test.ts b/src/agents/pi-embedded-runner.history-limit-from-session-key.test.ts index 776c54f1c6e..8ab852a684b 100644 --- a/src/agents/pi-embedded-runner.history-limit-from-session-key.test.ts +++ b/src/agents/pi-embedded-runner.history-limit-from-session-key.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner.js"; +import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner/history.js"; describe("getDmHistoryLimitFromSessionKey", () => { it("keeps backward compatibility for dm/direct session kinds", () => { diff --git a/src/agents/pi-embedded-runner.limithistoryturns.test.ts b/src/agents/pi-embedded-runner.limithistoryturns.test.ts index e9cbbc5e808..248e3db71a4 100644 --- a/src/agents/pi-embedded-runner.limithistoryturns.test.ts +++ b/src/agents/pi-embedded-runner.limithistoryturns.test.ts @@ -1,6 +1,6 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; import { describe, expect, it } from "vitest"; -import { limitHistoryTurns } from "./pi-embedded-runner.js"; +import { limitHistoryTurns } from "./pi-embedded-runner/history.js"; describe("limitHistoryTurns", () => { const mockUsage = { diff --git a/src/auto-reply/reply/commands-subagents/action-info.ts b/src/auto-reply/reply/commands-subagents/action-info.ts index 13cdc3e885d..b65c7b9fcd3 100644 --- a/src/auto-reply/reply/commands-subagents/action-info.ts +++ b/src/auto-reply/reply/commands-subagents/action-info.ts @@ -3,19 +3,94 @@ import { countPendingDescendantRunsFromRuns } from "../../../agents/subagent-reg import { getSubagentRunsSnapshotForRead } from "../../../agents/subagent-registry-state.js"; import { resolveStorePath } from "../../../config/sessions/paths.js"; import { loadSessionStore } from "../../../config/sessions/store-load.js"; +import { formatTimeAgo } from "../../../infra/format-time/format-relative.ts"; +import { parseAgentSessionKey } from "../../../routing/session-key.js"; import { formatDurationCompact } from "../../../shared/subagents-format.js"; import { findTaskByRunIdForOwner } from "../../../tasks/task-owner-access.js"; import { sanitizeTaskStatusText } from "../../../tasks/task-status.js"; import type { CommandHandlerResult } from "../commands-types.js"; -import { formatRunLabel } from "../subagents-utils.js"; import { - type SubagentsCommandContext, - formatTimestampWithAge, - loadSubagentSessionEntry, - resolveDisplayStatus, - resolveSubagentEntryForToken, - stopWithText, -} from "./shared.js"; + formatRunLabel, + formatRunStatus, + resolveSubagentTargetFromRuns, +} from "../subagents-utils.js"; +import { type SubagentsCommandContext } from "./shared.js"; + +const RECENT_WINDOW_MINUTES = 30; + +function stopWithText(text: string): CommandHandlerResult { + return { shouldContinue: false, reply: { text } }; +} + +function formatTimestamp(valueMs?: number) { + if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) { + return "n/a"; + } + return new Date(valueMs).toISOString(); +} + +function formatTimestampWithAge(valueMs?: number) { + if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) { + return "n/a"; + } + return `${formatTimestamp(valueMs)} (${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 resolveSubagentEntryForToken( + runs: SubagentsCommandContext["runs"], + token: string | undefined, +): { entry: SubagentsCommandContext["runs"][number] } | { reply: CommandHandlerResult } { + const resolved = resolveSubagentTargetFromRuns({ + runs, + token, + recentWindowMinutes: RECENT_WINDOW_MINUTES, + label: (entry) => formatRunLabel(entry), + isActive: (entry) => + !entry.endedAt || + Math.max( + 0, + countPendingDescendantRunsFromRuns( + getSubagentRunsSnapshotForRead(subagentRuns), + entry.childSessionKey, + ), + ) > 0, + errors: { + missingTarget: "Missing subagent id.", + invalidIndex: (value) => `Invalid subagent index: ${value}`, + unknownSession: (value) => `Unknown subagent session: ${value}`, + ambiguousLabel: (value) => `Ambiguous subagent label: ${value}`, + ambiguousLabelPrefix: (value) => `Ambiguous subagent label prefix: ${value}`, + ambiguousRunIdPrefix: (value) => `Ambiguous run id prefix: ${value}`, + unknownTarget: (value) => `Unknown subagent id: ${value}`, + }, + }); + if (!resolved.entry) { + return { reply: stopWithText(`⚠️ ${resolved.error ?? "Unknown subagent."}`) }; + } + return { entry: resolved.entry }; +} + +function loadSubagentSessionEntry(params: SubagentsCommandContext["params"], childKey: string) { + const parsed = parseAgentSessionKey(childKey); + const storePath = resolveStorePath(params.cfg.session?.store, { + agentId: parsed?.agentId, + }); + const store = loadSessionStore(storePath); + return { entry: store[childKey] }; +} export function handleSubagentsInfoAction(ctx: SubagentsCommandContext): CommandHandlerResult { const { params, requesterKey, runs, restTokens } = ctx; @@ -30,10 +105,7 @@ export function handleSubagentsInfoAction(ctx: SubagentsCommandContext): Command } const run = targetResolution.entry; - const { entry: sessionEntry } = loadSubagentSessionEntry(params, run.childSessionKey, { - loadSessionStore, - resolveStorePath, - }); + const { entry: sessionEntry } = loadSubagentSessionEntry(params, run.childSessionKey); const runtime = run.startedAt && Number.isFinite(run.startedAt) ? (formatDurationCompact((run.endedAt ?? Date.now()) - run.startedAt) ?? "n/a")