mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:00:54 +00:00
perf: slim hot test imports
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { resolveAuthProfileOrder } from "./auth-profiles.js";
|
|
||||||
import {
|
import {
|
||||||
ANTHROPIC_CFG,
|
ANTHROPIC_CFG,
|
||||||
ANTHROPIC_STORE,
|
ANTHROPIC_STORE,
|
||||||
} from "./auth-profiles.resolve-auth-profile-order.fixtures.js";
|
} from "./auth-profiles.resolve-auth-profile-order.fixtures.js";
|
||||||
|
import { resolveAuthProfileOrder } from "./auth-profiles/order.js";
|
||||||
import type { AuthProfileStore } from "./auth-profiles/types.js";
|
import type { AuthProfileStore } from "./auth-profiles/types.js";
|
||||||
|
|
||||||
describe("resolveAuthProfileOrder", () => {
|
describe("resolveAuthProfileOrder", () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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 {
|
function makeApiKeyStore(provider: string, profileIds: string[]): AuthProfileStore {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { resolveAuthProfileOrder } from "./auth-profiles.js";
|
import { resolveAuthProfileOrder } from "./auth-profiles/order.js";
|
||||||
|
|
||||||
describe("resolveAuthProfileOrder", () => {
|
describe("resolveAuthProfileOrder", () => {
|
||||||
it("orders by lastUsed when no explicit order exists", () => {
|
it("orders by lastUsed when no explicit order exists", () => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { resolveAuthProfileOrder } from "./auth-profiles.js";
|
|
||||||
import {
|
import {
|
||||||
ANTHROPIC_CFG,
|
ANTHROPIC_CFG,
|
||||||
ANTHROPIC_STORE,
|
ANTHROPIC_STORE,
|
||||||
} from "./auth-profiles.resolve-auth-profile-order.fixtures.js";
|
} from "./auth-profiles.resolve-auth-profile-order.fixtures.js";
|
||||||
|
import { resolveAuthProfileOrder } from "./auth-profiles/order.js";
|
||||||
|
|
||||||
describe("resolveAuthProfileOrder", () => {
|
describe("resolveAuthProfileOrder", () => {
|
||||||
const store = ANTHROPIC_STORE;
|
const store = ANTHROPIC_STORE;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js";
|
|
||||||
import { resolveProviderIdForAuth } from "../provider-auth-aliases.js";
|
import { resolveProviderIdForAuth } from "../provider-auth-aliases.js";
|
||||||
|
import { findNormalizedProviderValue, normalizeProviderId } from "../provider-id.js";
|
||||||
import {
|
import {
|
||||||
evaluateStoredCredentialEligibility,
|
evaluateStoredCredentialEligibility,
|
||||||
type AuthCredentialReasonCode,
|
type AuthCredentialReasonCode,
|
||||||
} from "./credential-state.js";
|
} from "./credential-state.js";
|
||||||
import { dedupeProfileIds, listProfilesForProvider } from "./profiles.js";
|
import { dedupeProfileIds, listProfilesForProvider } from "./profile-list.js";
|
||||||
import type { AuthProfileStore } from "./types.js";
|
import type { AuthProfileStore } from "./types.js";
|
||||||
import {
|
import {
|
||||||
clearExpiredCooldowns,
|
clearExpiredCooldowns,
|
||||||
isProfileInCooldown,
|
isProfileInCooldown,
|
||||||
resolveProfileUnusableUntil,
|
resolveProfileUnusableUntil,
|
||||||
} from "./usage.js";
|
} from "./usage-state.js";
|
||||||
|
|
||||||
export type AuthProfileEligibilityReasonCode =
|
export type AuthProfileEligibilityReasonCode =
|
||||||
| AuthCredentialReasonCode
|
| AuthCredentialReasonCode
|
||||||
|
|||||||
13
src/agents/auth-profiles/profile-list.ts
Normal file
13
src/agents/auth-profiles/profile-list.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -2,16 +2,14 @@ import { normalizeStringEntries } from "../../shared/string-normalization.js";
|
|||||||
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
||||||
import { resolveProviderIdForAuth } from "../provider-auth-aliases.js";
|
import { resolveProviderIdForAuth } from "../provider-auth-aliases.js";
|
||||||
import { normalizeProviderId } from "../provider-id.js";
|
import { normalizeProviderId } from "../provider-id.js";
|
||||||
|
import { dedupeProfileIds, listProfilesForProvider } from "./profile-list.js";
|
||||||
import {
|
import {
|
||||||
ensureAuthProfileStoreForLocalUpdate,
|
ensureAuthProfileStoreForLocalUpdate,
|
||||||
saveAuthProfileStore,
|
saveAuthProfileStore,
|
||||||
updateAuthProfileStoreWithLock,
|
updateAuthProfileStoreWithLock,
|
||||||
} from "./store.js";
|
} from "./store.js";
|
||||||
import type { AuthProfileCredential, AuthProfileStore } from "./types.js";
|
import type { AuthProfileCredential, AuthProfileStore } from "./types.js";
|
||||||
|
export { dedupeProfileIds, listProfilesForProvider } from "./profile-list.js";
|
||||||
export function dedupeProfileIds(profileIds: string[]): string[] {
|
|
||||||
return [...new Set(profileIds)];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setAuthProfileOrder(params: {
|
export async function setAuthProfileOrder(params: {
|
||||||
agentDir?: string;
|
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: {
|
export async function markAuthProfileGood(params: {
|
||||||
store: AuthProfileStore;
|
store: AuthProfileStore;
|
||||||
provider: string;
|
provider: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { AuthProfileConfig } from "../../config/types.js";
|
|||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
import { findNormalizedProviderKey, normalizeProviderId } from "../provider-id.js";
|
import { findNormalizedProviderKey, normalizeProviderId } from "../provider-id.js";
|
||||||
import { resolveAuthProfileMetadata } from "./identity.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";
|
import type { AuthProfileIdRepairResult, AuthProfileStore } from "./types.js";
|
||||||
|
|
||||||
function getProfileSuffix(profileId: string): string {
|
function getProfileSuffix(profileId: string): string {
|
||||||
|
|||||||
138
src/agents/auth-profiles/usage-state.ts
Normal file
138
src/agents/auth-profiles/usage-state.ts
Normal file
@@ -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<ProfileUsageStats, "cooldownUntil" | "disabledUntil">,
|
||||||
|
): 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<ProfileUsageStats, "cooldownReason" | "cooldownModel" | "disabledUntil">,
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,8 +1,21 @@
|
|||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
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 { logAuthProfileFailureStateChange } from "./state-observation.js";
|
||||||
import { saveAuthProfileStore, updateAuthProfileStoreWithLock } from "./store.js";
|
import { saveAuthProfileStore, updateAuthProfileStoreWithLock } from "./store.js";
|
||||||
import type { AuthProfileFailureReason, AuthProfileStore, ProfileUsageStats } from "./types.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 = {
|
const authProfileUsageDeps = {
|
||||||
saveAuthProfileStore,
|
saveAuthProfileStore,
|
||||||
@@ -70,11 +83,6 @@ type WhamCooldownProbeResult = {
|
|||||||
reason: string;
|
reason: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isAuthCooldownBypassedForProvider(provider: string | undefined): boolean {
|
|
||||||
const normalized = normalizeProviderId(provider ?? "");
|
|
||||||
return normalized === "openrouter" || normalized === "kilocode";
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldProbeWhamForFailure(
|
function shouldProbeWhamForFailure(
|
||||||
provider: string | undefined,
|
provider: string | undefined,
|
||||||
reason: AuthProfileFailureReason,
|
reason: AuthProfileFailureReason,
|
||||||
@@ -222,50 +230,6 @@ export async function probeWhamForCooldown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveProfileUnusableUntil(
|
|
||||||
stats: Pick<ProfileUsageStats, "cooldownUntil" | "disabledUntil">,
|
|
||||||
): 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.
|
* Infer the most likely reason all candidate profiles are currently unavailable.
|
||||||
*
|
*
|
||||||
@@ -393,93 +357,6 @@ export function getSoonestCooldownExpiry(
|
|||||||
return Math.min(soonest, latestMatchingModelCooldown);
|
return Math.min(soonest, latestMatchingModelCooldown);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldBypassModelScopedCooldown(
|
|
||||||
stats: Pick<ProfileUsageStats, "cooldownReason" | "cooldownModel" | "disabledUntil">,
|
|
||||||
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.
|
* Mark a profile as successfully used. Resets error count and updates lastUsed.
|
||||||
* Uses store lock to avoid overwriting concurrent usage updates.
|
* Uses store lock to avoid overwriting concurrent usage updates.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner.js";
|
import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner/history.js";
|
||||||
|
|
||||||
describe("getDmHistoryLimitFromSessionKey", () => {
|
describe("getDmHistoryLimitFromSessionKey", () => {
|
||||||
it("falls back to provider default when per-DM not set", () => {
|
it("falls back to provider default when per-DM not set", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner.js";
|
import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner/history.js";
|
||||||
|
|
||||||
describe("getDmHistoryLimitFromSessionKey", () => {
|
describe("getDmHistoryLimitFromSessionKey", () => {
|
||||||
it("returns undefined when sessionKey is undefined", () => {
|
it("returns undefined when sessionKey is undefined", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner.js";
|
import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner/history.js";
|
||||||
|
|
||||||
describe("getDmHistoryLimitFromSessionKey", () => {
|
describe("getDmHistoryLimitFromSessionKey", () => {
|
||||||
it("keeps backward compatibility for dm/direct session kinds", () => {
|
it("keeps backward compatibility for dm/direct session kinds", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { limitHistoryTurns } from "./pi-embedded-runner.js";
|
import { limitHistoryTurns } from "./pi-embedded-runner/history.js";
|
||||||
|
|
||||||
describe("limitHistoryTurns", () => {
|
describe("limitHistoryTurns", () => {
|
||||||
const mockUsage = {
|
const mockUsage = {
|
||||||
|
|||||||
@@ -3,19 +3,94 @@ import { countPendingDescendantRunsFromRuns } from "../../../agents/subagent-reg
|
|||||||
import { getSubagentRunsSnapshotForRead } from "../../../agents/subagent-registry-state.js";
|
import { getSubagentRunsSnapshotForRead } from "../../../agents/subagent-registry-state.js";
|
||||||
import { resolveStorePath } from "../../../config/sessions/paths.js";
|
import { resolveStorePath } from "../../../config/sessions/paths.js";
|
||||||
import { loadSessionStore } from "../../../config/sessions/store-load.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 { formatDurationCompact } from "../../../shared/subagents-format.js";
|
||||||
import { findTaskByRunIdForOwner } from "../../../tasks/task-owner-access.js";
|
import { findTaskByRunIdForOwner } from "../../../tasks/task-owner-access.js";
|
||||||
import { sanitizeTaskStatusText } from "../../../tasks/task-status.js";
|
import { sanitizeTaskStatusText } from "../../../tasks/task-status.js";
|
||||||
import type { CommandHandlerResult } from "../commands-types.js";
|
import type { CommandHandlerResult } from "../commands-types.js";
|
||||||
import { formatRunLabel } from "../subagents-utils.js";
|
|
||||||
import {
|
import {
|
||||||
type SubagentsCommandContext,
|
formatRunLabel,
|
||||||
formatTimestampWithAge,
|
formatRunStatus,
|
||||||
loadSubagentSessionEntry,
|
resolveSubagentTargetFromRuns,
|
||||||
resolveDisplayStatus,
|
} from "../subagents-utils.js";
|
||||||
resolveSubagentEntryForToken,
|
import { type SubagentsCommandContext } from "./shared.js";
|
||||||
stopWithText,
|
|
||||||
} 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 {
|
export function handleSubagentsInfoAction(ctx: SubagentsCommandContext): CommandHandlerResult {
|
||||||
const { params, requesterKey, runs, restTokens } = ctx;
|
const { params, requesterKey, runs, restTokens } = ctx;
|
||||||
@@ -30,10 +105,7 @@ export function handleSubagentsInfoAction(ctx: SubagentsCommandContext): Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
const run = targetResolution.entry;
|
const run = targetResolution.entry;
|
||||||
const { entry: sessionEntry } = loadSubagentSessionEntry(params, run.childSessionKey, {
|
const { entry: sessionEntry } = loadSubagentSessionEntry(params, run.childSessionKey);
|
||||||
loadSessionStore,
|
|
||||||
resolveStorePath,
|
|
||||||
});
|
|
||||||
const runtime =
|
const runtime =
|
||||||
run.startedAt && Number.isFinite(run.startedAt)
|
run.startedAt && Number.isFinite(run.startedAt)
|
||||||
? (formatDurationCompact((run.endedAt ?? Date.now()) - run.startedAt) ?? "n/a")
|
? (formatDurationCompact((run.endedAt ?? Date.now()) - run.startedAt) ?? "n/a")
|
||||||
|
|||||||
Reference in New Issue
Block a user