mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
perf: avoid sort-for-single selection
This commit is contained in:
@@ -36,9 +36,14 @@ export function selectPendingApprovalRequest(params: {
|
||||
}
|
||||
|
||||
if (normalizeLowercaseStringOrEmpty(params.requested) === "latest") {
|
||||
return {
|
||||
pending: [...params.pending].toSorted((a, b) => (b.ts ?? 0) - (a.ts ?? 0))[0],
|
||||
};
|
||||
let latest = params.pending[0];
|
||||
for (let index = 1; index < params.pending.length; index += 1) {
|
||||
const pending = params.pending[index];
|
||||
if ((pending.ts ?? 0) > (latest.ts ?? 0)) {
|
||||
latest = pending;
|
||||
}
|
||||
}
|
||||
return { pending: latest };
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -677,6 +677,22 @@ function isRoutingSummary(summary: SectionSummary): boolean {
|
||||
return summary.scores.routing > 0 || REM_ROUTING_SIGNAL_RE.test(summary.text);
|
||||
}
|
||||
|
||||
function findStrongestSummary(
|
||||
summaries: SectionSummary[],
|
||||
predicate: (summary: SectionSummary) => boolean,
|
||||
): SectionSummary | undefined {
|
||||
let strongest: SectionSummary | undefined;
|
||||
for (const summary of summaries) {
|
||||
if (!predicate(summary)) {
|
||||
continue;
|
||||
}
|
||||
if (!strongest || summary.scores.overall > strongest.scores.overall) {
|
||||
strongest = summary;
|
||||
}
|
||||
}
|
||||
return strongest;
|
||||
}
|
||||
|
||||
function previewGroundedRemForFile(params: {
|
||||
relPath: string;
|
||||
content: string;
|
||||
@@ -847,15 +863,15 @@ function previewGroundedRemForFile(params: {
|
||||
(sum, { section, snippets }) => sum + scoreSection(section, snippets).tasks,
|
||||
0,
|
||||
);
|
||||
const strongestRoutingSummary = summaries
|
||||
.filter((summary) => isRoutingSummary(summary))
|
||||
.toSorted((left, right) => right.scores.overall - left.scores.overall)[0];
|
||||
const strongestIncidentSummary = summaries
|
||||
.filter((summary) => summary.scores.incident > 0)
|
||||
.toSorted((left, right) => right.scores.overall - left.scores.overall)[0];
|
||||
const strongestExternalizationSummary = summaries
|
||||
.filter((summary) => summary.scores.externalization > 0)
|
||||
.toSorted((left, right) => right.scores.overall - left.scores.overall)[0];
|
||||
const strongestRoutingSummary = findStrongestSummary(summaries, isRoutingSummary);
|
||||
const strongestIncidentSummary = findStrongestSummary(
|
||||
summaries,
|
||||
(summary) => summary.scores.incident > 0,
|
||||
);
|
||||
const strongestExternalizationSummary = findStrongestSummary(
|
||||
summaries,
|
||||
(summary) => summary.scores.externalization > 0,
|
||||
);
|
||||
|
||||
if (facts.length === 0 && monitoringSignal >= 3) {
|
||||
addReflection(
|
||||
|
||||
@@ -207,12 +207,16 @@ async function mapWithConcurrency<T, U>(
|
||||
}
|
||||
|
||||
function extractTranscript(result: QaSuiteResult) {
|
||||
const details = result.scenarios.flatMap((scenario) =>
|
||||
scenario.steps
|
||||
.map((step) => step.details)
|
||||
.filter((detail): detail is string => Boolean(detail)),
|
||||
);
|
||||
return details.toSorted((left, right) => right.length - left.length)[0] ?? result.report;
|
||||
let longestDetail: string | undefined;
|
||||
for (const scenario of result.scenarios) {
|
||||
for (const step of scenario.steps) {
|
||||
const detail = step.details;
|
||||
if (detail && (!longestDetail || detail.length > longestDetail.length)) {
|
||||
longestDetail = detail;
|
||||
}
|
||||
}
|
||||
}
|
||||
return longestDetail ?? result.report;
|
||||
}
|
||||
|
||||
function collectTranscriptStats(transcript: string) {
|
||||
|
||||
@@ -208,12 +208,13 @@ export function getActiveSessionMaintenanceWarning(params: {
|
||||
const cutoffMs = now - params.pruneAfterMs;
|
||||
const wouldPrune = activeEntry.updatedAt != null ? activeEntry.updatedAt < cutoffMs : false;
|
||||
const keys = Object.keys(params.store);
|
||||
const wouldCap =
|
||||
keys.length > params.maxEntries &&
|
||||
keys
|
||||
.toSorted((a, b) => getEntryUpdatedAt(params.store[b]) - getEntryUpdatedAt(params.store[a]))
|
||||
.slice(params.maxEntries)
|
||||
.includes(activeSessionKey);
|
||||
const wouldCap = wouldCapActiveSession({
|
||||
store: params.store,
|
||||
keys,
|
||||
activeEntry,
|
||||
activeSessionKey,
|
||||
maxEntries: params.maxEntries,
|
||||
});
|
||||
|
||||
if (!wouldPrune && !wouldCap) {
|
||||
return null;
|
||||
@@ -230,6 +231,40 @@ export function getActiveSessionMaintenanceWarning(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function wouldCapActiveSession(params: {
|
||||
store: Record<string, SessionEntry>;
|
||||
keys: string[];
|
||||
activeEntry: SessionEntry;
|
||||
activeSessionKey: string;
|
||||
maxEntries: number;
|
||||
}): boolean {
|
||||
if (params.keys.length <= params.maxEntries) {
|
||||
return false;
|
||||
}
|
||||
if (params.maxEntries <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const activeUpdatedAt = getEntryUpdatedAt(params.activeEntry);
|
||||
let newerOrTieBeforeActive = 0;
|
||||
let seenActive = false;
|
||||
for (const key of params.keys) {
|
||||
if (key === params.activeSessionKey) {
|
||||
seenActive = true;
|
||||
continue;
|
||||
}
|
||||
const entryUpdatedAt = getEntryUpdatedAt(params.store[key]);
|
||||
if (entryUpdatedAt > activeUpdatedAt || (!seenActive && entryUpdatedAt === activeUpdatedAt)) {
|
||||
newerOrTieBeforeActive++;
|
||||
if (newerOrTieBeforeActive >= params.maxEntries) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cap the store to the N most recently updated entries.
|
||||
* Entries without `updatedAt` are sorted last (removed first when over limit).
|
||||
|
||||
@@ -3,7 +3,12 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createFixtureSuite } from "../../test-utils/fixture-suite.js";
|
||||
import { capEntryCount, pruneStaleEntries, rotateSessionFile } from "./store.js";
|
||||
import {
|
||||
capEntryCount,
|
||||
getActiveSessionMaintenanceWarning,
|
||||
pruneStaleEntries,
|
||||
rotateSessionFile,
|
||||
} from "./store.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
@@ -70,6 +75,48 @@ describe("capEntryCount", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getActiveSessionMaintenanceWarning", () => {
|
||||
it("warns when the active session is outside the retained recent entries", () => {
|
||||
const now = Date.now();
|
||||
const store = makeStore([
|
||||
["newest", makeEntry(now)],
|
||||
["recent", makeEntry(now - 1)],
|
||||
["active", makeEntry(now - 2)],
|
||||
["old", makeEntry(now - 3)],
|
||||
]);
|
||||
|
||||
const warning = getActiveSessionMaintenanceWarning({
|
||||
store,
|
||||
activeSessionKey: "active",
|
||||
pruneAfterMs: DAY_MS,
|
||||
maxEntries: 2,
|
||||
nowMs: now,
|
||||
});
|
||||
|
||||
expect(warning?.wouldCap).toBe(true);
|
||||
expect(warning?.wouldPrune).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves insertion order tie behavior from stable sorting", () => {
|
||||
const now = Date.now();
|
||||
const store = makeStore([
|
||||
["same-before", makeEntry(now)],
|
||||
["active", makeEntry(now)],
|
||||
["same-after", makeEntry(now)],
|
||||
]);
|
||||
|
||||
const warning = getActiveSessionMaintenanceWarning({
|
||||
store,
|
||||
activeSessionKey: "active",
|
||||
pruneAfterMs: DAY_MS,
|
||||
maxEntries: 1,
|
||||
nowMs: now,
|
||||
});
|
||||
|
||||
expect(warning?.wouldCap).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rotateSessionFile", () => {
|
||||
let testDir: string;
|
||||
let storePath: string;
|
||||
|
||||
@@ -199,10 +199,15 @@ function resolveDreamingNextCycle(
|
||||
if (!status?.phases) {
|
||||
return null;
|
||||
}
|
||||
const nextRunAtMs = Object.values(status.phases)
|
||||
.filter((phase) => phase.enabled && typeof phase.nextRunAtMs === "number")
|
||||
.map((phase) => phase.nextRunAtMs as number)
|
||||
.toSorted((a, b) => a - b)[0];
|
||||
let nextRunAtMs: number | undefined;
|
||||
for (const phase of Object.values(status.phases)) {
|
||||
if (!phase.enabled || typeof phase.nextRunAtMs !== "number") {
|
||||
continue;
|
||||
}
|
||||
if (nextRunAtMs === undefined || phase.nextRunAtMs < nextRunAtMs) {
|
||||
nextRunAtMs = phase.nextRunAtMs;
|
||||
}
|
||||
}
|
||||
return formatDreamNextCycle(nextRunAtMs);
|
||||
}
|
||||
|
||||
|
||||
@@ -579,15 +579,25 @@ const buildUsageInsightStats = (
|
||||
const errorRate = aggregates.messages.total
|
||||
? aggregates.messages.errors / aggregates.messages.total
|
||||
: 0;
|
||||
const peakErrorDay = aggregates.daily
|
||||
.filter((day) => day.messages > 0 && day.errors > 0)
|
||||
.map((day) => ({
|
||||
let peakErrorDay: UsageInsightStats["peakErrorDay"];
|
||||
for (const day of aggregates.daily) {
|
||||
if (day.messages <= 0 || day.errors <= 0) {
|
||||
continue;
|
||||
}
|
||||
const candidate = {
|
||||
date: day.date,
|
||||
errors: day.errors,
|
||||
messages: day.messages,
|
||||
rate: day.errors / day.messages,
|
||||
}))
|
||||
.toSorted((a, b) => b.rate - a.rate || b.errors - a.errors)[0];
|
||||
};
|
||||
if (
|
||||
!peakErrorDay ||
|
||||
candidate.rate > peakErrorDay.rate ||
|
||||
(candidate.rate === peakErrorDay.rate && candidate.errors > peakErrorDay.errors)
|
||||
) {
|
||||
peakErrorDay = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
durationSumMs,
|
||||
|
||||
Reference in New Issue
Block a user