mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-19 09:01:39 -06:00
fix(deps): keep package runtime dependencies single-owned (#126119)
* fix(deps): consolidate shared runtime helpers * test(concurrency): support current test lib target * fix(time): preserve year-scale plugin durations * fix(agents): preserve empty subagent completions
This commit is contained in:
committed by
GitHub
parent
0767902330
commit
baefd067bb
@@ -265,7 +265,6 @@ const bundledPluginIgnoredRuntimeDependencies = [
|
||||
|
||||
const rootBundledPluginRuntimeDependencies = [
|
||||
"@anthropic-ai/sdk",
|
||||
"@anthropic-ai/vertex-sdk",
|
||||
"@google/genai",
|
||||
"@grammyjs/runner",
|
||||
"@grammyjs/transformer-throttler",
|
||||
@@ -273,16 +272,11 @@ const rootBundledPluginRuntimeDependencies = [
|
||||
"@mozilla/readability",
|
||||
"@silvia-odwyer/photon-node",
|
||||
"@trycua/cua-driver",
|
||||
"@slack/bolt",
|
||||
"@slack/types",
|
||||
"@slack/web-api",
|
||||
"grammy",
|
||||
"linkedom",
|
||||
"minimatch",
|
||||
"node-edge-tts",
|
||||
"openshell",
|
||||
"clawpdf",
|
||||
"tokenjuice",
|
||||
] as const;
|
||||
|
||||
// Root installation and build workflows deliberately mirror these dependencies from their
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"discord-api-types": "0.38.52",
|
||||
"libopus-wasm": "0.2.0",
|
||||
"mdast-util-from-markdown": "2.0.3",
|
||||
"p-map": "7.0.6",
|
||||
"typebox": "1.3.6",
|
||||
"undici": "8.9.0",
|
||||
"ws": "8.21.1",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readAcpSessionEntry, type AcpSessionStoreEntry } from "openclaw/plugin-sdk/acp-runtime";
|
||||
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Discord plugin module implements thread bindings.lifecycle behavior.
|
||||
import {
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
normalizeOptionalString,
|
||||
uniqueStrings,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import pMap from "p-map";
|
||||
import { parseDiscordTarget } from "../targets.js";
|
||||
import { resolveChannelIdForBinding } from "./thread-bindings.discord-api.js";
|
||||
import { getThreadBindingManager } from "./thread-bindings.manager.js";
|
||||
@@ -266,9 +266,8 @@ export async function reconcileAcpThreadBindingsOnStartup(params: {
|
||||
}
|
||||
|
||||
if (params.healthProbe && probeTargets.length > 0) {
|
||||
const probeResults = await pMap(
|
||||
probeTargets,
|
||||
async ({ binding, sessionKey, session }) => {
|
||||
const { results: probeResults } = await runTasksWithConcurrency({
|
||||
tasks: probeTargets.map(({ binding, sessionKey, session }) => async () => {
|
||||
try {
|
||||
const result = await params.healthProbe?.({
|
||||
cfg: params.cfg,
|
||||
@@ -288,12 +287,11 @@ export async function reconcileAcpThreadBindingsOnStartup(params: {
|
||||
status: "uncertain" satisfies AcpThreadBindingHealthStatus,
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
concurrency: ACP_STARTUP_HEALTH_PROBE_CONCURRENCY_LIMIT,
|
||||
stopOnError: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
limit: ACP_STARTUP_HEALTH_PROBE_CONCURRENCY_LIMIT,
|
||||
errorMode: "stop",
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
for (const probeResult of probeResults) {
|
||||
if (probeResult.status === "stale") {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"jszip": "3.10.1",
|
||||
"pretty-ms": "9.3.0",
|
||||
"typebox": "1.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
parseStrictPositiveInteger,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { replaceFileAtomic } from "openclaw/plugin-sdk/security-runtime";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { formatDurationCompact } from "openclaw/plugin-sdk/time-runtime";
|
||||
import type { GoogleMeetCalendarLookupResult } from "./calendar.js";
|
||||
import type { GoogleMeetModeInput, GoogleMeetTransport } from "./config.js";
|
||||
import type { GoogleMeetRuntime } from "./runtime.js";
|
||||
@@ -355,9 +355,8 @@ export function formatDuration(value: number | undefined): string {
|
||||
if (value === undefined) {
|
||||
return "n/a";
|
||||
}
|
||||
return prettyMilliseconds(Math.max(0, Math.round(value / 1000) * 1000), {
|
||||
unitCount: 2,
|
||||
});
|
||||
const roundedMs = Math.max(0, Math.round(value / 1000) * 1000);
|
||||
return formatDurationCompact(roundedMs, { showYears: true, spaced: true }) ?? "0ms";
|
||||
}
|
||||
|
||||
export function writeDoctorStatus(status: Awaited<ReturnType<GoogleMeetRuntime["status"]>>): void {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"mdast-util-from-markdown": "2.0.3",
|
||||
"p-map": "7.0.6",
|
||||
"typebox": "1.3.6",
|
||||
"yaml": "2.9.0",
|
||||
"zod": "4.4.3"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
|
||||
import { resolveNonNegativeIntegerOption } from "openclaw/plugin-sdk/number-runtime";
|
||||
import type {
|
||||
OpenKeyedStoreOptions,
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
normalizeOptionalString,
|
||||
normalizeUniqueTrimmedStringList,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import pMap, { pMapSkip } from "p-map";
|
||||
import { walkMemoryWikiDirectory } from "./bounded-walk.js";
|
||||
|
||||
const LEGACY_IMPORT_RUN_READ_CONCURRENCY = 16;
|
||||
@@ -510,12 +510,16 @@ export async function readLegacyMemoryWikiImportRunRecords(
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return await pMap(
|
||||
entries.filter((entry) => entry.kind === "file"),
|
||||
async (entry) => {
|
||||
const raw = await fs.readFile(path.join(importRunsDir, entry.relativePath), "utf8");
|
||||
return normalizeMemoryWikiImportRunRecord(JSON.parse(raw) as unknown) ?? pMapSkip;
|
||||
},
|
||||
{ concurrency: LEGACY_IMPORT_RUN_READ_CONCURRENCY, stopOnError: true },
|
||||
);
|
||||
const { results } = await runTasksWithConcurrency({
|
||||
tasks: entries
|
||||
.filter((entry) => entry.kind === "file")
|
||||
.map((entry) => async () => {
|
||||
const raw = await fs.readFile(path.join(importRunsDir, entry.relativePath), "utf8");
|
||||
return normalizeMemoryWikiImportRunRecord(JSON.parse(raw) as unknown);
|
||||
}),
|
||||
limit: LEGACY_IMPORT_RUN_READ_CONCURRENCY,
|
||||
errorMode: "stop",
|
||||
throwOnError: true,
|
||||
});
|
||||
return results.filter((record): record is ChatGptImportRunRecord => record !== null);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { filterMemorySearchHitsBySessionVisibility } from "@openclaw/memory-core/api.js";
|
||||
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
|
||||
import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
|
||||
import { resolveDefaultAgentId, resolveSessionAgentId } from "openclaw/plugin-sdk/memory-host-core";
|
||||
import { getActiveMemorySearchManager } from "openclaw/plugin-sdk/memory-host-search";
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
uniqueStrings,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import pMap, { pMapSkip } from "p-map";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
import { walkMemoryWikiDirectory } from "./bounded-walk.js";
|
||||
import { assessClaimFreshness, isClaimContestedStatus } from "./claim-health.js";
|
||||
@@ -230,16 +230,18 @@ async function readQueryableWikiPagesByPaths(
|
||||
rootDir: string,
|
||||
files: string[],
|
||||
): Promise<QueryableWikiPage[]> {
|
||||
return await pMap(
|
||||
files,
|
||||
async (relativePath) => {
|
||||
const { results } = await runTasksWithConcurrency({
|
||||
tasks: files.map((relativePath) => async () => {
|
||||
const absolutePath = path.join(rootDir, relativePath);
|
||||
const raw = await fs.readFile(absolutePath, "utf8");
|
||||
const summary = toWikiPageSummary({ absolutePath, relativePath, raw });
|
||||
return summary ? { ...summary, raw } : pMapSkip;
|
||||
},
|
||||
{ concurrency: QUERY_PAGE_READ_CONCURRENCY, stopOnError: true },
|
||||
);
|
||||
return summary ? { ...summary, raw } : null;
|
||||
}),
|
||||
limit: QUERY_PAGE_READ_CONCURRENCY,
|
||||
errorMode: "stop",
|
||||
throwOnError: true,
|
||||
});
|
||||
return results.filter((page): page is QueryableWikiPage => page !== null);
|
||||
}
|
||||
|
||||
async function readQueryDigestBundle(
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import pMap from "p-map";
|
||||
import { walkMemoryWikiDirectory } from "./bounded-walk.js";
|
||||
import type { BridgeMemoryWikiResult } from "./bridge.js";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
@@ -253,9 +253,8 @@ export async function syncMemoryWikiUnsafeLocalSources(
|
||||
group: "unsafe-local",
|
||||
incomingCount: new Set([...artifacts.map((artifact) => artifact.syncKey), ...activeKeys]).size,
|
||||
});
|
||||
const results = await pMap(
|
||||
artifacts,
|
||||
async (artifact) => {
|
||||
const { results } = await runTasksWithConcurrency({
|
||||
tasks: artifacts.map((artifact) => async () => {
|
||||
const stats = await fs.stat(artifact.absolutePath);
|
||||
activeKeys.add(artifact.syncKey);
|
||||
return await writeUnsafeLocalSourcePage({
|
||||
@@ -265,9 +264,11 @@ export async function syncMemoryWikiUnsafeLocalSources(
|
||||
sourceSize: stats.size,
|
||||
state,
|
||||
});
|
||||
},
|
||||
{ concurrency: UNSAFE_LOCAL_SYNC_CONCURRENCY, stopOnError: true },
|
||||
);
|
||||
}),
|
||||
limit: UNSAFE_LOCAL_SYNC_CONCURRENCY,
|
||||
errorMode: "stop",
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
const removedCount = await pruneImportedSourceEntries({
|
||||
vaultRoot: config.vault.path,
|
||||
|
||||
@@ -8,10 +8,7 @@
|
||||
"@copilotkit/aimock": "1.37.4",
|
||||
"@modelcontextprotocol/sdk": "1.30.0",
|
||||
"@openclaw/crabline": "0.1.11",
|
||||
"p-limit": "7.3.1",
|
||||
"p-map": "7.0.6",
|
||||
"playwright-core": "1.62.1",
|
||||
"pretty-ms": "9.3.0",
|
||||
"semver": "7.8.5",
|
||||
"ws": "8.21.1",
|
||||
"yaml": "2.9.0",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Qa Lab plugin module implements character eval behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import pMap from "p-map";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { formatDurationCompact } from "openclaw/plugin-sdk/time-runtime";
|
||||
import { createQaArtifactRunId } from "./artifact-run-id.js";
|
||||
import { isQaFastModeModelRef, type QaProviderMode } from "./model-selection.js";
|
||||
import {
|
||||
@@ -242,9 +242,7 @@ function formatDuration(ms: number) {
|
||||
return "unknown";
|
||||
}
|
||||
const roundedMs = ms < 1000 ? Math.round(ms) : Math.round(ms / 1000) * 1000;
|
||||
return prettyMilliseconds(roundedMs, {
|
||||
unitCount: 2,
|
||||
});
|
||||
return formatDurationCompact(roundedMs, { showYears: true, spaced: true }) ?? "0ms";
|
||||
}
|
||||
|
||||
function logCharacterEvalProgress(
|
||||
@@ -518,9 +516,8 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
`start scenario=${scenarioId} candidates=${models.length} candidateConcurrency=${candidateConcurrency} output=${outputDir}`,
|
||||
);
|
||||
const candidatesStartedAt = Date.now();
|
||||
const runs = await pMap(
|
||||
models,
|
||||
async (model, index) => {
|
||||
const { results: runs } = await runTasksWithConcurrency({
|
||||
tasks: models.map((model, index) => async () => {
|
||||
const thinkingDefault = resolveCandidateThinkingDefault({
|
||||
model,
|
||||
candidateThinkingDefault: params.candidateThinkingDefault,
|
||||
@@ -599,9 +596,11 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
);
|
||||
return run;
|
||||
}
|
||||
},
|
||||
{ concurrency: candidateConcurrency, stopOnError: true },
|
||||
);
|
||||
}),
|
||||
limit: candidateConcurrency,
|
||||
errorMode: "stop",
|
||||
throwOnError: true,
|
||||
});
|
||||
const failedCandidateCount = runs.filter((run) => run.status === "fail").length;
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
@@ -626,9 +625,8 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
`judges start judges=${judgeModels.length} judgeConcurrency=${judgeConcurrency} timeout=${formatDuration(judgeTimeoutMs)} labels=${params.judgeBlindModels === true ? "blind" : "visible"}`,
|
||||
);
|
||||
const judgesStartedAt = Date.now();
|
||||
const judgments = await pMap(
|
||||
judgeModels,
|
||||
async (judgeModel, index) => {
|
||||
const { results: judgments } = await runTasksWithConcurrency({
|
||||
tasks: judgeModels.map((judgeModel, index) => async () => {
|
||||
const judgeOptions = resolveJudgeOptions({
|
||||
model: judgeModel,
|
||||
judgeThinkingDefault: params.judgeThinkingDefault,
|
||||
@@ -680,9 +678,11 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
`judge done ${formatEvalIndex(index, judgeModels.length)} model=${judgeModel} rankings=${rankings.length} duration=${formatDuration(judgment.durationMs)}${judgeError ? ` error="${judgeError}"` : ""}`,
|
||||
);
|
||||
return judgment;
|
||||
},
|
||||
{ concurrency: judgeConcurrency, stopOnError: true },
|
||||
);
|
||||
}),
|
||||
limit: judgeConcurrency,
|
||||
errorMode: "stop",
|
||||
throwOnError: true,
|
||||
});
|
||||
const failedJudgeCount = judgments.filter((judgment) => judgment.rankings.length === 0).length;
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
|
||||
@@ -4,12 +4,12 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
asNullableRecord as readRecord,
|
||||
readStringValue,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import pLimit from "p-limit";
|
||||
import type {
|
||||
QaEvidenceArtifactView,
|
||||
QaEvidenceGalleryEntryView,
|
||||
@@ -890,50 +890,59 @@ export async function buildQaEvidenceGalleryModel(params: {
|
||||
repoRoot,
|
||||
summaryEntries: summary.entries,
|
||||
});
|
||||
const limitArtifactView = pLimit(ARTIFACT_VIEW_CONCURRENCY);
|
||||
const entries = await Promise.all(
|
||||
summary.entries.map(async (entry, entryIndex): Promise<QaEvidenceGalleryEntryView> => {
|
||||
counts[entry.result.status] += 1;
|
||||
const sanitizeEntryText = (value: string) =>
|
||||
sanitizeGalleryText(value, {
|
||||
const artifactTasks = summary.entries.flatMap((entry, entryIndex) =>
|
||||
(entry.execution?.artifacts ?? []).map(
|
||||
(artifact, artifactIndex) => () =>
|
||||
buildArtifactView({
|
||||
allowedArtifactFiles,
|
||||
artifact,
|
||||
artifactIndex,
|
||||
evidenceDir,
|
||||
entryIndex,
|
||||
extraRoots: [requestedRepoRoot],
|
||||
hrefEvidencePath,
|
||||
repoRoot,
|
||||
});
|
||||
return {
|
||||
artifacts: await limitArtifactView.map(
|
||||
entry.execution?.artifacts ?? [],
|
||||
(artifact, artifactIndex) =>
|
||||
buildArtifactView({
|
||||
allowedArtifactFiles,
|
||||
artifact,
|
||||
artifactIndex,
|
||||
evidenceDir,
|
||||
entryIndex,
|
||||
extraRoots: [requestedRepoRoot],
|
||||
hrefEvidencePath,
|
||||
repoRoot,
|
||||
}),
|
||||
),
|
||||
coverage: entry.coverage.map((coverage) => ({
|
||||
id: sanitizeEntryText(coverage.id),
|
||||
role: sanitizeEntryText(coverage.role),
|
||||
})),
|
||||
failureReason: entry.result.failure?.reason
|
||||
? sanitizeEntryText(entry.result.failure.reason)
|
||||
: null,
|
||||
id: sanitizeEntryText(entry.test.id),
|
||||
kind: sanitizeEntryText(entry.test.kind),
|
||||
sourcePath: entry.test.source?.path
|
||||
? displayGalleryPath(entry.test.source.path, {
|
||||
extraRoots: [requestedRepoRoot],
|
||||
repoRoot,
|
||||
})
|
||||
: null,
|
||||
status: entry.result.status,
|
||||
title: sanitizeEntryText(entry.test.title),
|
||||
};
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const { results: artifactViews } = await runTasksWithConcurrency({
|
||||
tasks: artifactTasks,
|
||||
limit: ARTIFACT_VIEW_CONCURRENCY,
|
||||
errorMode: "continue",
|
||||
throwOnError: true,
|
||||
});
|
||||
let artifactOffset = 0;
|
||||
const entries = summary.entries.map((entry): QaEvidenceGalleryEntryView => {
|
||||
counts[entry.result.status] += 1;
|
||||
const artifactCount = entry.execution?.artifacts?.length ?? 0;
|
||||
const artifacts = artifactViews.slice(artifactOffset, artifactOffset + artifactCount);
|
||||
artifactOffset += artifactCount;
|
||||
const sanitizeEntryText = (value: string) =>
|
||||
sanitizeGalleryText(value, {
|
||||
extraRoots: [requestedRepoRoot],
|
||||
repoRoot,
|
||||
});
|
||||
return {
|
||||
artifacts,
|
||||
coverage: entry.coverage.map((coverage) => ({
|
||||
id: sanitizeEntryText(coverage.id),
|
||||
role: sanitizeEntryText(coverage.role),
|
||||
})),
|
||||
failureReason: entry.result.failure?.reason
|
||||
? sanitizeEntryText(entry.result.failure.reason)
|
||||
: null,
|
||||
id: sanitizeEntryText(entry.test.id),
|
||||
kind: sanitizeEntryText(entry.test.kind),
|
||||
sourcePath: entry.test.source?.path
|
||||
? displayGalleryPath(entry.test.source.path, {
|
||||
extraRoots: [requestedRepoRoot],
|
||||
repoRoot,
|
||||
})
|
||||
: null,
|
||||
status: entry.result.status,
|
||||
title: sanitizeEntryText(entry.test.title),
|
||||
};
|
||||
});
|
||||
return {
|
||||
counts,
|
||||
entries,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Qa Lab tests cover Slack live adapter message reconciliation.
|
||||
import type { FetchFunction } from "@slack/web-api";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createQaBusState } from "../../bus-state.js";
|
||||
import { testing } from "./adapter.runtime.js";
|
||||
import type { SlackQaFetchFunction as FetchFunction } from "./slack-live.contracts.js";
|
||||
|
||||
describe("Slack live adapter reconciliation", () => {
|
||||
it("aborts an in-flight observer fetch when the adapter stops", async () => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
createSlackWriteClient,
|
||||
resolveSlackWebClientOptions,
|
||||
} from "@openclaw/slack/api.js";
|
||||
import type { FetchFunction } from "@slack/web-api";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { acquireDebugProxyCaptureStore } from "openclaw/plugin-sdk/proxy-capture";
|
||||
@@ -22,7 +21,11 @@ import {
|
||||
parseSlackQaCredentialPayload,
|
||||
resolveSlackQaRuntimeEnv,
|
||||
} from "./slack-live.config.js";
|
||||
import type { SlackMessage, SlackQaRuntimeEnv } from "./slack-live.contracts.js";
|
||||
import type {
|
||||
SlackMessage,
|
||||
SlackQaFetchFunction,
|
||||
SlackQaRuntimeEnv,
|
||||
} from "./slack-live.contracts.js";
|
||||
import { waitForSlackChannelStable } from "./slack-live.message-observations.js";
|
||||
import {
|
||||
getSlackIdentity,
|
||||
@@ -33,6 +36,7 @@ import {
|
||||
|
||||
type AdapterFactory = NonNullable<QaRunnerCliRegistration["adapterFactory"]>;
|
||||
type FactoryContext = Parameters<AdapterFactory["create"]>[0];
|
||||
type FetchFunction = SlackQaFetchFunction;
|
||||
type AdapterDefinition = Awaited<ReturnType<AdapterFactory["create"]>>;
|
||||
|
||||
const SLACK_POLL_INTERVAL_MS = 500;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import path from "node:path";
|
||||
import type { WebClient } from "@slack/web-api";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import type { QaRunnerCliRegistration } from "openclaw/plugin-sdk/qa-runner-runtime";
|
||||
import {
|
||||
@@ -14,6 +13,7 @@ import type {
|
||||
SlackQaScenarioContext,
|
||||
SlackQaScenarioMetadata,
|
||||
SlackQaScenarioRun,
|
||||
SlackQaWebClient as WebClient,
|
||||
} from "./slack-live.contracts.js";
|
||||
import { assertSlackCodexApprovalModelSupported } from "./slack-live.contracts.js";
|
||||
import { waitForSlackChannelStable } from "./slack-live.message-observations.js";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// QA Lab Slack native approval observation and resolution.
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { WebClient } from "@slack/web-api";
|
||||
import type { ChannelApprovalKind } from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import { assertApprovalDecisionResult } from "../shared/live-approval-result.js";
|
||||
import {
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
type SlackObservedMessage,
|
||||
type SlackApprovalArtifact,
|
||||
type SlackMessage,
|
||||
type SlackQaWebClient as WebClient,
|
||||
} from "./slack-live.contracts.js";
|
||||
import {
|
||||
listSlackMessages,
|
||||
|
||||
@@ -3,7 +3,6 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { listSlackReactions } from "@openclaw/slack/api.js";
|
||||
import type { WebClient } from "@slack/web-api";
|
||||
import { extractGatewayMessageText } from "../../gateway-log-sentinel.js";
|
||||
import { formatApprovalResultValue } from "../shared/live-approval-result.js";
|
||||
import { asPlainRecord } from "./slack-live.config.js";
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
type SlackQaCodexApprovalScenarioRun,
|
||||
type SlackQaScenarioContext,
|
||||
type SlackQaScenarioMetadata,
|
||||
type SlackQaWebClient as WebClient,
|
||||
} from "./slack-live.contracts.js";
|
||||
|
||||
export function resolveCodexFileApprovalTargetPath(token: string) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// QA Lab Slack credentials, instrumentation, and channel config.
|
||||
import type { WebClient } from "@slack/web-api";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { asNonArrayRecord, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
type SlackQaConfigOverrides,
|
||||
SLACK_QA_ENV_KEYS,
|
||||
slackQaCredentialPayloadSchema,
|
||||
type SlackQaWebClient as WebClient,
|
||||
} from "./slack-live.contracts.js";
|
||||
|
||||
function resolveEnvValue(env: NodeJS.ProcessEnv, key: (typeof SLACK_QA_ENV_KEYS)[number]) {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
// QA Lab Slack live domain contracts and wire schemas.
|
||||
import type { WebClient } from "@slack/web-api";
|
||||
import type { createSlackWebClient } from "@openclaw/slack/api.js";
|
||||
import type { ChannelApprovalKind } from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { z } from "zod";
|
||||
import type { startQaGatewayChild } from "../../gateway-child.js";
|
||||
import { splitQaModelRef } from "../../model-selection.js";
|
||||
|
||||
export type SlackQaWebClient = ReturnType<typeof createSlackWebClient>;
|
||||
export type SlackQaFetchFunction = NonNullable<
|
||||
NonNullable<Parameters<typeof createSlackWebClient>[1]>["fetch"]
|
||||
>;
|
||||
type WebClient = SlackQaWebClient;
|
||||
|
||||
export type SlackQaRuntimeEnv = {
|
||||
channelId: string;
|
||||
driverBotToken: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// QA Lab Slack scenario reply observation and channel readiness.
|
||||
import type { WebClient } from "@slack/web-api";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import type { startQaGatewayChild } from "../../gateway-child.js";
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
type SlackAuthIdentity,
|
||||
type SlackObservedMessage,
|
||||
type SlackMessage,
|
||||
type SlackQaWebClient as WebClient,
|
||||
} from "./slack-live.contracts.js";
|
||||
import {
|
||||
listSlackMessages,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// QA Lab Slack Web API and stored-message observations.
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import { createSlackWebClient, sendSlackMessage } from "@openclaw/slack/api.js";
|
||||
import type { WebClient } from "@slack/web-api";
|
||||
import {
|
||||
asPlainRecord,
|
||||
countSlackNativeDataBlocks,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
type SlackMessage,
|
||||
slackHistorySchema,
|
||||
slackRepliesSchema,
|
||||
type SlackQaWebClient as WebClient,
|
||||
} from "./slack-live.contracts.js";
|
||||
import { buildSlackInvalidBlocksTableProbe } from "./slack-live.invalid-blocks.js";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Qa Lab plugin module implements suite planning behavior.
|
||||
import path from "node:path";
|
||||
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
|
||||
import { parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import pMap from "p-map";
|
||||
import { createQaArtifactRunId } from "./artifact-run-id.js";
|
||||
import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "./cli-paths.js";
|
||||
import type { QaCliBackendAuthMode } from "./gateway-child.js";
|
||||
@@ -430,9 +430,8 @@ async function mapQaSuiteWithConcurrency<T, U>(
|
||||
}
|
||||
})();
|
||||
}
|
||||
const results = await pMap(
|
||||
items,
|
||||
async (item, index) => {
|
||||
const { results } = await runTasksWithConcurrency({
|
||||
tasks: items.map((item, index) => async () => {
|
||||
if (stopped) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -445,12 +444,11 @@ async function mapQaSuiteWithConcurrency<T, U>(
|
||||
stopped = true;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{
|
||||
concurrency: Math.max(1, Math.floor(concurrency)),
|
||||
stopOnError: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
limit: Math.max(1, Math.floor(concurrency)),
|
||||
errorMode: "stop",
|
||||
throwOnError: true,
|
||||
});
|
||||
const completed: U[] = [];
|
||||
for (const result of results) {
|
||||
if (result !== undefined) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { formatDurationCompact } from "openclaw/plugin-sdk/time-runtime";
|
||||
|
||||
export function formatTime(timestamp: number) {
|
||||
return new Date(timestamp).toLocaleTimeString([], {
|
||||
@@ -23,9 +23,7 @@ export function formatIso(iso?: string) {
|
||||
|
||||
export function formatDuration(ms: number): string {
|
||||
const roundedMs = ms < 1000 ? Math.round(ms) : Math.round(ms / 1000) * 1000;
|
||||
return prettyMilliseconds(Math.max(0, roundedMs), {
|
||||
unitCount: 2,
|
||||
});
|
||||
return formatDurationCompact(Math.max(0, roundedMs), { showYears: true, spaced: true }) ?? "0ms";
|
||||
}
|
||||
|
||||
export function esc(text: string) {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"@slack/types": "3.0.0",
|
||||
"@slack/web-api": "8.0.0",
|
||||
"get-east-asian-width": "1.6.0",
|
||||
"p-map": "7.0.6",
|
||||
"typebox": "1.3.6",
|
||||
"undici": "7.29.0",
|
||||
"ws": "8.21.1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Slack plugin module implements media behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import type { WebClient as SlackWebClient } from "@slack/web-api";
|
||||
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { normalizeHostname } from "openclaw/plugin-sdk/host-runtime";
|
||||
import { resolveRequestUrl } from "openclaw/plugin-sdk/request-url";
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import pMap, { pMapSkip } from "p-map";
|
||||
import { formatSlackFileReference } from "../file-reference.js";
|
||||
import type { SlackAttachment, SlackFile } from "../types.js";
|
||||
import { MAX_SLACK_MEDIA_FILES, type SlackMediaResult } from "./media-types.js";
|
||||
@@ -332,9 +332,8 @@ export async function resolveSlackMedia(params: {
|
||||
const limitedFiles =
|
||||
files.length > MAX_SLACK_MEDIA_FILES ? files.slice(0, MAX_SLACK_MEDIA_FILES) : files;
|
||||
|
||||
const resolved = await pMap(
|
||||
limitedFiles,
|
||||
async (file): Promise<SlackMediaResult | typeof pMapSkip> => {
|
||||
const { results } = await runTasksWithConcurrency({
|
||||
tasks: limitedFiles.map((file) => async (): Promise<SlackMediaResult | null> => {
|
||||
// Audio preflight keys the original event file object so admission can
|
||||
// reuse that exact download without turning this into a persistent cache.
|
||||
const preloaded = params.preloadedMedia?.get(file);
|
||||
@@ -344,7 +343,7 @@ export async function resolveSlackMedia(params: {
|
||||
const eventUrl = file.url_private_download ?? file.url_private;
|
||||
const url = eventUrl ?? (await fetchFreshSlackFileUrl({ file, client: params.client }));
|
||||
if (!url) {
|
||||
return pMapSkip;
|
||||
return null;
|
||||
}
|
||||
const result = await downloadSlackMediaFile({
|
||||
file,
|
||||
@@ -357,12 +356,12 @@ export async function resolveSlackMedia(params: {
|
||||
govSlack,
|
||||
}).catch(() => null);
|
||||
if (result || !eventUrl) {
|
||||
return result ?? pMapSkip;
|
||||
return result;
|
||||
}
|
||||
|
||||
const freshUrl = await fetchFreshSlackFileUrl({ file, client: params.client });
|
||||
if (!freshUrl) {
|
||||
return pMapSkip;
|
||||
return null;
|
||||
}
|
||||
const retryResult = await downloadSlackMediaFile({
|
||||
file,
|
||||
@@ -374,10 +373,13 @@ export async function resolveSlackMedia(params: {
|
||||
abortSignal: params.abortSignal,
|
||||
govSlack,
|
||||
}).catch(() => null);
|
||||
return retryResult ?? pMapSkip;
|
||||
},
|
||||
{ concurrency: MAX_SLACK_MEDIA_CONCURRENCY, stopOnError: true },
|
||||
);
|
||||
return retryResult;
|
||||
}),
|
||||
limit: MAX_SLACK_MEDIA_CONCURRENCY,
|
||||
errorMode: "stop",
|
||||
throwOnError: true,
|
||||
});
|
||||
const resolved = results.filter((result): result is SlackMediaResult => result !== null);
|
||||
|
||||
return resolved.length > 0 ? resolved : null;
|
||||
}
|
||||
|
||||
Generated
-21
@@ -832,9 +832,6 @@ importers:
|
||||
mdast-util-from-markdown:
|
||||
specifier: 2.0.3
|
||||
version: 2.0.3(supports-color@10.2.2)
|
||||
p-map:
|
||||
specifier: 7.0.6
|
||||
version: 7.0.6
|
||||
typebox:
|
||||
specifier: 1.3.6
|
||||
version: 1.3.6
|
||||
@@ -994,9 +991,6 @@ importers:
|
||||
jszip:
|
||||
specifier: 3.10.1
|
||||
version: 3.10.1
|
||||
pretty-ms:
|
||||
specifier: 9.3.0
|
||||
version: 9.3.0
|
||||
typebox:
|
||||
specifier: 1.3.6
|
||||
version: 1.3.6
|
||||
@@ -1286,9 +1280,6 @@ importers:
|
||||
mdast-util-from-markdown:
|
||||
specifier: 2.0.3
|
||||
version: 2.0.3(supports-color@10.2.2)
|
||||
p-map:
|
||||
specifier: 7.0.6
|
||||
version: 7.0.6
|
||||
typebox:
|
||||
specifier: 1.3.6
|
||||
version: 1.3.6
|
||||
@@ -1621,18 +1612,9 @@ importers:
|
||||
'@openclaw/crabline':
|
||||
specifier: 0.1.11
|
||||
version: 0.1.11
|
||||
p-limit:
|
||||
specifier: 7.3.1
|
||||
version: 7.3.1
|
||||
p-map:
|
||||
specifier: 7.0.6
|
||||
version: 7.0.6
|
||||
playwright-core:
|
||||
specifier: 1.62.1
|
||||
version: 1.62.1
|
||||
pretty-ms:
|
||||
specifier: 9.3.0
|
||||
version: 9.3.0
|
||||
semver:
|
||||
specifier: 7.8.5
|
||||
version: 7.8.5
|
||||
@@ -1769,9 +1751,6 @@ importers:
|
||||
get-east-asian-width:
|
||||
specifier: 1.6.0
|
||||
version: 1.6.0
|
||||
p-map:
|
||||
specifier: 7.0.6
|
||||
version: 7.0.6
|
||||
typebox:
|
||||
specifier: 1.3.6
|
||||
version: 1.3.6
|
||||
|
||||
@@ -2960,6 +2960,8 @@ describe("CLI attempt execution", () => {
|
||||
});
|
||||
|
||||
expect(embeddedArg.suppressLiveStreamOutput).toBe(false);
|
||||
expect(embeddedArg.terminalReplyExpectation).toBe("optional");
|
||||
expect(embeddedArg.allowEmptyAssistantReplyAsSilent).toBe(true);
|
||||
});
|
||||
|
||||
it("forwards exact cron creator authority into embedded execution", async () => {
|
||||
|
||||
@@ -83,6 +83,7 @@ import type { ContextEngineLogicalTurnLease } from "../harness/context-engine-lo
|
||||
import type { ContextEngineTurnAttemptFacts } from "../harness/context-engine-turn-attempt.js";
|
||||
import { runAgentHarnessBeforeMessageWriteHook } from "../harness/hook-helpers.js";
|
||||
import { resolveAvailableAgentHarnessPolicy } from "../harness/selection.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
|
||||
import { resolveCliRuntimeExecutionProvider } from "../model-runtime-aliases.js";
|
||||
import { isCliProvider } from "../model-selection.js";
|
||||
import { resolveOpenAIRuntimeProvider } from "../openai-routing.js";
|
||||
@@ -1138,6 +1139,8 @@ export function runAgentAttempt(params: {
|
||||
sandboxSessionKey: params.sessionKey,
|
||||
agentId: params.sessionAgentId,
|
||||
trigger: "user",
|
||||
// Subagent lifecycle owns the stricter explicit visible/silent/empty evidence check.
|
||||
terminalReplyExpectation: params.opts.lane === AGENT_LANE_SUBAGENT ? "optional" : undefined,
|
||||
messageChannel: params.messageChannel,
|
||||
messageProvider: params.opts.messageProvider ?? params.messageChannel,
|
||||
agentAccountId: params.runContext.accountId,
|
||||
@@ -1227,7 +1230,8 @@ export function runAgentAttempt(params: {
|
||||
modelRun: params.opts.modelRun,
|
||||
promptMode: params.opts.promptMode,
|
||||
disableTools,
|
||||
allowEmptyAssistantReplyAsSilent: isSubagentAnnounceHandoff,
|
||||
allowEmptyAssistantReplyAsSilent:
|
||||
params.opts.lane === AGENT_LANE_SUBAGENT || isSubagentAnnounceHandoff,
|
||||
onAgentEvent: params.onAgentEvent,
|
||||
deferTerminalLifecycle: params.deferTerminalLifecycle,
|
||||
suppressNextUserMessagePersistence: params.suppressPromptPersistenceOnRetry === true,
|
||||
|
||||
@@ -9,6 +9,8 @@ export type FormatDurationSecondsOptions = {
|
||||
};
|
||||
|
||||
export type FormatDurationCompactOptions = {
|
||||
/** Show year units instead of folding them into days. Default: false */
|
||||
showYears?: boolean;
|
||||
/** Add space between units: "2m 5s" instead of "2m5s". Default: false */
|
||||
spaced?: boolean;
|
||||
};
|
||||
@@ -64,7 +66,7 @@ export function formatDurationCompact(
|
||||
return prettyMilliseconds(roundedMs);
|
||||
}
|
||||
const formatted = prettyMilliseconds(Math.round(ms / 1000) * 1000, {
|
||||
hideYear: true,
|
||||
hideYear: options?.showYears !== true,
|
||||
unitCount: 2,
|
||||
});
|
||||
return options?.spaced ? formatted : formatted.replaceAll(" ", "");
|
||||
|
||||
@@ -61,6 +61,11 @@ describe("format-duration", () => {
|
||||
{ input: 65000, options: { spaced: true }, expected: "1m 5s" },
|
||||
{ input: 3660000, options: { spaced: true }, expected: "1h 1m" },
|
||||
{ input: 90000000, options: { spaced: true }, expected: "1d 1h" },
|
||||
{
|
||||
input: 366 * 86400000,
|
||||
options: { showYears: true, spaced: true },
|
||||
expected: "1y 1d",
|
||||
},
|
||||
{ input: 59500, expected: "1m" },
|
||||
{ input: 59400, expected: "59s" },
|
||||
])("formats compact duration for %j", ({ input, options, expected }) => {
|
||||
|
||||
@@ -6,3 +6,4 @@ export {
|
||||
formatZonedTimestamp,
|
||||
resolveTimezone,
|
||||
} from "../infra/format-time/format-datetime.js";
|
||||
export { formatDurationCompact } from "../infra/format-time/format-duration.js";
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runTasksWithConcurrency } from "./run-with-concurrency.js";
|
||||
|
||||
function createDeferred() {
|
||||
let resolve = () => {};
|
||||
const promise = new Promise<void>((resolvePromise) => {
|
||||
resolve = resolvePromise;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
describe("runTasksWithConcurrency", () => {
|
||||
it("preserves task order with bounded worker count", async () => {
|
||||
let running = 0;
|
||||
@@ -107,4 +115,66 @@ describe("runTasksWithConcurrency", () => {
|
||||
expect(onTaskError).toHaveBeenNthCalledWith(1, firstErr, 0);
|
||||
expect(onTaskError).toHaveBeenNthCalledWith(2, secondErr, 2);
|
||||
});
|
||||
|
||||
it("rejects early and stops scheduling new work in stop mode", async () => {
|
||||
const err = new Error("boom");
|
||||
const releaseInFlight = createDeferred();
|
||||
const inFlightSettled = createDeferred();
|
||||
const started: number[] = [];
|
||||
const run = runTasksWithConcurrency({
|
||||
tasks: [
|
||||
async () => {
|
||||
started.push(0);
|
||||
await releaseInFlight.promise;
|
||||
inFlightSettled.resolve();
|
||||
return 10;
|
||||
},
|
||||
async () => {
|
||||
started.push(1);
|
||||
throw err;
|
||||
},
|
||||
async () => {
|
||||
started.push(2);
|
||||
return 30;
|
||||
},
|
||||
],
|
||||
limit: 2,
|
||||
errorMode: "stop",
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
await expect(run).rejects.toBe(err);
|
||||
releaseInFlight.resolve();
|
||||
await inFlightSettled.promise;
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
expect(started).toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it("keeps scheduling after an early rejection in continue mode", async () => {
|
||||
const err = new Error("boom");
|
||||
const completed = createDeferred();
|
||||
const started: number[] = [];
|
||||
const run = runTasksWithConcurrency({
|
||||
tasks: [
|
||||
async () => {
|
||||
started.push(0);
|
||||
throw err;
|
||||
},
|
||||
async () => {
|
||||
started.push(1);
|
||||
completed.resolve();
|
||||
return 20;
|
||||
},
|
||||
],
|
||||
limit: 1,
|
||||
errorMode: "continue",
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
await expect(run).rejects.toBe(err);
|
||||
await completed.promise;
|
||||
expect(started).toEqual([0, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@ export type RunTasksWithConcurrencyOptions<T> = {
|
||||
limit: number;
|
||||
/** `stop` prevents new work after the first failure; in-flight workers still settle. */
|
||||
errorMode?: ConcurrencyErrorMode;
|
||||
/** Reject immediately on a task failure instead of returning aggregate error state. */
|
||||
throwOnError?: boolean;
|
||||
/** Called once per failed task with the original task index. */
|
||||
onTaskError?: (error: unknown, index: number) => void;
|
||||
};
|
||||
@@ -29,7 +31,7 @@ export type RunTasksWithConcurrencyResult<T> = {
|
||||
export async function runTasksWithConcurrency<T>(
|
||||
params: RunTasksWithConcurrencyOptions<T>,
|
||||
): Promise<RunTasksWithConcurrencyResult<T>> {
|
||||
const { tasks, limit, onTaskError } = params;
|
||||
const { tasks, limit, onTaskError, throwOnError = false } = params;
|
||||
const errorMode = params.errorMode ?? "continue";
|
||||
if (tasks.length === 0) {
|
||||
return { results: [], firstError: undefined, hasError: false };
|
||||
@@ -56,10 +58,17 @@ export async function runTasksWithConcurrency<T>(
|
||||
hasError = true;
|
||||
}
|
||||
onTaskError?.(error, index);
|
||||
if (throwOnError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.allSettled(runs);
|
||||
if (throwOnError) {
|
||||
await Promise.all(runs);
|
||||
} else {
|
||||
await Promise.allSettled(runs);
|
||||
}
|
||||
return { results, firstError, hasError };
|
||||
}
|
||||
|
||||
+1
-1
@@ -514,7 +514,7 @@ function shouldExternalizeGatewayProtocolDependency(id: string): boolean {
|
||||
}
|
||||
|
||||
function shouldExternalizeGatewayClientDependency(id: string): boolean {
|
||||
return ["ws", "@openclaw/gateway-protocol"].some(
|
||||
return ["ws", "@openclaw/gateway-protocol", "ipaddr.js"].some(
|
||||
(dependency) => id === dependency || id.startsWith(`${dependency}/`),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user