refactor(lint): enable map spread rule

This commit is contained in:
Peter Steinberger
2026-04-18 19:53:02 +01:00
parent 0c245c35c5
commit 4fa961d4f1
73 changed files with 352 additions and 344 deletions

View File

@@ -17,7 +17,7 @@
"eslint-plugin-unicorn/prefer-set-size": "error",
"oxc/no-accumulating-spread": "error",
"oxc/no-async-endpoint-handlers": "error",
"oxc/no-map-spread": "off",
"oxc/no-map-spread": "error",
"typescript/consistent-return": "error",
"typescript/no-explicit-any": "error",
"typescript/no-extraneous-class": "error",
@@ -55,7 +55,8 @@
{
"files": ["src/security/**"],
"rules": {
"eslint/no-warning-comments": "off"
"eslint/no-warning-comments": "off",
"oxc/no-map-spread": "off"
}
},
{

View File

@@ -36,10 +36,9 @@ export function buildArceeCatalogModels(): NonNullable<ModelProviderConfig["mode
}
export function buildArceeOpenRouterCatalogModels(): NonNullable<ModelProviderConfig["models"]> {
return buildArceeCatalogModels().map((model) => ({
...model,
id: toArceeOpenRouterModelId(model.id),
}));
return buildArceeCatalogModels().map((model) =>
Object.assign({}, model, { id: toArceeOpenRouterModelId(model.id) }),
);
}
export function buildArceeProvider(): ModelProviderConfig {

View File

@@ -25,10 +25,11 @@ export const listDiscordDirectoryPeersFromConfig = createResolvedDirectoryEntrie
resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId),
resolveSources: (account) => {
const allowFrom = account.config.allowFrom ?? account.config.dm?.allowFrom ?? [];
const guildUsers = Object.values(account.config.guilds ?? {}).flatMap((guild) => [
...(guild.users ?? []),
...Object.values(guild.channels ?? {}).flatMap((channel) => channel.users ?? []),
]);
const guildUsers = Object.values(account.config.guilds ?? {}).flatMap((guild) =>
(guild.users ?? []).concat(
Object.values(guild.channels ?? {}).flatMap((channel) => channel.users ?? []),
),
);
return [allowFrom, Object.keys(account.config.dms ?? {}), guildUsers];
},
normalizeId: (raw) => {

View File

@@ -110,7 +110,7 @@ function cleanBlocksForInsert(blocks: FeishuDocxBlock[]): {
.map((block) => {
if (block.block_type === 31 && block.table?.merge_info) {
const { merge_info: _merge_info, ...tableRest } = block.table;
return { ...block, table: tableRest };
return Object.assign({}, block, { table: tableRest });
}
return block;
});

View File

@@ -141,7 +141,7 @@ function normalizeProviderModels(
return model;
}
mutated = true;
return { ...model, id: nextId };
return Object.assign({}, model, { id: nextId });
});
return mutated ? { ...provider, models: nextModels } : provider;

View File

@@ -120,10 +120,9 @@ async function runGeminiSearch(params: {
for (let index = 0; index < rawCitations.length; index += 10) {
const batch = rawCitations.slice(index, index + 10);
const resolved = await Promise.all(
batch.map(async (citation) => ({
...citation,
url: await resolveCitationRedirectUrl(citation.url),
})),
batch.map(async (citation) =>
Object.assign({}, citation, { url: await resolveCitationRedirectUrl(citation.url) }),
),
);
citations.push(...resolved);
}

View File

@@ -279,7 +279,7 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = createChat
listPeers: async (params) => listIrcDirectoryPeersFromConfig(params),
listGroups: async (params) => {
const entries = await listIrcDirectoryGroupsFromConfig(params);
return entries.map((entry) => ({ ...entry, name: entry.id }));
return entries.map((entry) => Object.assign({}, entry, { name: entry.id }));
},
}),
status: createComputedAccountStatusAdapter<ResolvedIrcAccount, IrcProbe>({

View File

@@ -817,10 +817,11 @@ export async function prepareLmstudioDynamicModels(
headers,
quiet: true,
});
return discoveredModels.map((model) => ({
...model,
provider: PROVIDER_ID,
api: ctx.providerConfig?.api ?? "openai-completions",
baseUrl,
}));
return discoveredModels.map((model) =>
Object.assign({}, model, {
provider: PROVIDER_ID,
api: ctx.providerConfig?.api ?? `openai-completions`,
baseUrl,
}),
);
}

View File

@@ -364,7 +364,9 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
return entries.map((entry) => {
const raw = entry.id.startsWith("user:") ? entry.id.slice("user:".length) : entry.id;
const incomplete = !raw.startsWith("@") || !raw.includes(":");
return incomplete ? { ...entry, name: "incomplete id; expected @user:server" } : entry;
return incomplete
? Object.assign({}, entry, { name: `incomplete id; expected @user:server` })
: entry;
});
},
listGroups: async (params) => await listMatrixDirectoryGroupsFromConfig(params),

View File

@@ -840,35 +840,45 @@ function buildAgentDigest(params: {
.toSorted((left, right) => left.relativePath.localeCompare(right.relativePath))
.map((page) => {
const pageFreshness = assessPageFreshness(page);
return {
...(page.id ? { id: page.id } : {}),
title: page.title,
kind: page.kind,
path: page.relativePath,
sourceIds: [...page.sourceIds],
questions: [...page.questions],
contradictions: [...page.contradictions],
...(typeof page.confidence === "number" ? { confidence: page.confidence } : {}),
freshnessLevel: pageFreshness.level,
...(pageFreshness.lastTouchedAt ? { lastTouchedAt: pageFreshness.lastTouchedAt } : {}),
claimCount: page.claims.length,
topClaims: sortClaims(page)
.slice(0, 5)
.map((claim) => {
const freshness = assessClaimFreshness({ page, claim });
return {
...(claim.id ? { id: claim.id } : {}),
text: claim.text,
status: normalizeClaimStatus(claim.status),
...(typeof claim.confidence === "number" ? { confidence: claim.confidence } : {}),
evidenceCount: claim.evidence.length,
missingEvidence: claim.evidence.length === 0,
evidence: [...claim.evidence],
freshnessLevel: freshness.level,
...(freshness.lastTouchedAt ? { lastTouchedAt: freshness.lastTouchedAt } : {}),
};
}),
};
return Object.assign(
{},
page.id ? { id: page.id } : {},
{
title: page.title,
kind: page.kind,
path: page.relativePath,
sourceIds: [...page.sourceIds],
questions: [...page.questions],
contradictions: [...page.contradictions],
},
typeof page.confidence === "number" ? { confidence: page.confidence } : {},
{ freshnessLevel: pageFreshness.level },
pageFreshness.lastTouchedAt ? { lastTouchedAt: pageFreshness.lastTouchedAt } : {},
{
claimCount: page.claims.length,
topClaims: sortClaims(page)
.slice(0, 5)
.map((claim) => {
const freshness = assessClaimFreshness({ page, claim });
return Object.assign(
{},
claim.id ? { id: claim.id } : {},
{
text: claim.text,
status: normalizeClaimStatus(claim.status),
},
typeof claim.confidence === "number" ? { confidence: claim.confidence } : {},
{
evidenceCount: claim.evidence.length,
missingEvidence: claim.evidence.length === 0,
evidence: [...claim.evidence],
freshnessLevel: freshness.level,
},
freshness.lastTouchedAt ? { lastTouchedAt: freshness.lastTouchedAt } : {},
);
}),
},
);
});
return {
pageCounts: params.pageCounts,

View File

@@ -93,5 +93,7 @@ export function buildMistralModelDefinition(): ModelDefinitionConfig {
}
export function buildMistralCatalogModels(): ModelDefinitionConfig[] {
return MISTRAL_MODEL_CATALOG.map((model) => ({ ...model, input: [...model.input] }));
return MISTRAL_MODEL_CATALOG.map((model) =>
Object.assign({}, model, { input: [...model.input] }),
);
}

View File

@@ -75,6 +75,8 @@ export function buildMoonshotProvider(): ModelProviderConfig {
return {
baseUrl: MOONSHOT_BASE_URL,
api: "openai-completions",
models: MOONSHOT_MODEL_CATALOG.map((model) => ({ ...model, input: [...model.input] })),
models: MOONSHOT_MODEL_CATALOG.map((model) =>
Object.assign({}, model, { input: [...model.input] }),
),
};
}

View File

@@ -194,11 +194,10 @@ export async function enrichOllamaModelsWithContext(
const batchResults = await Promise.all(
batch.map(async (model) => {
const showInfo = await queryOllamaModelShowInfoCached(apiBase, model);
return {
...model,
return Object.assign({}, model, {
contextWindow: showInfo.contextWindow,
capabilities: showInfo.capabilities,
};
});
}),
);
enriched.push(...batchResults);

View File

@@ -85,10 +85,10 @@ export function buildQaBusSnapshot(params: {
}): QaBusStateSnapshot {
return {
cursor: params.cursor,
conversations: Array.from(params.conversations.values()).map((conversation) => ({
...conversation,
})),
threads: Array.from(params.threads.values()).map((thread) => ({ ...thread })),
conversations: Array.from(params.conversations.values()).map((conversation) =>
Object.assign({}, conversation),
),
threads: Array.from(params.threads.values()).map((thread) => Object.assign({}, thread)),
messages: Array.from(params.messages.values()).map((message) => cloneMessage(message)),
events: params.events.map((event) => cloneEvent(event)),
};

View File

@@ -651,10 +651,10 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
timeoutMs: judgeTimeoutMs,
});
rankings = parseJudgeReply(rawReply, new Set(judgePrompt.labelToModel.keys())).map(
(ranking) => ({
...ranking,
model: judgePrompt.labelToModel.get(ranking.model) ?? ranking.model,
}),
(ranking) =>
Object.assign({}, ranking, {
model: judgePrompt.labelToModel.get(ranking.model) ?? ranking.model,
}),
);
} catch (error) {
judgeError = formatErrorMessage(error);

View File

@@ -6,7 +6,7 @@ export function buildQwenProvider(params?: { baseUrl?: string }): ModelProviderC
return {
baseUrl,
api: "openai-completions",
models: buildQwenModelCatalogForBaseUrl(baseUrl).map((model) => ({ ...model })),
models: buildQwenModelCatalogForBaseUrl(baseUrl).map((model) => Object.assign({}, model)),
};
}

View File

@@ -446,7 +446,7 @@ export async function handleSlackAction(
(pin.message as { ts?: unknown }).ts,
)
: pin.message;
return message ? { ...pin, message } : pin;
return message ? Object.assign({}, pin, { message }) : pin;
});
return jsonResult({ ok: true, pins: normalizedPins });
}

View File

@@ -70,7 +70,7 @@ function fitTelegramCommandsWithinTextBudget(
const description = truncateTelegramCommandText(command.description, descriptionCap);
if (description !== command.description) {
descriptionTrimmed = true;
return { ...command, description };
return Object.assign({}, command, { description });
}
return command;
});

View File

@@ -86,7 +86,7 @@ export function buildVydraSpeechProvider(): SpeechProviderPlugin {
models: [DEFAULT_VYDRA_SPEECH_MODEL],
voices: VYDRA_SPEECH_VOICES.map((voice) => voice.id),
resolveConfig: ({ rawConfig }) => normalizeVydraSpeechConfig(rawConfig),
listVoices: async () => VYDRA_SPEECH_VOICES.map((voice) => ({ ...voice })),
listVoices: async () => VYDRA_SPEECH_VOICES.map((voice) => Object.assign({}, voice)),
isConfigured: ({ providerConfig }) =>
Boolean(readVydraSpeechConfig(providerConfig).apiKey || process.env.VYDRA_API_KEY),
synthesize: async (req) => {

View File

@@ -169,10 +169,9 @@ describe("web auto-reply", () => {
const sharedRaw = crypto.randomBytes(width * height * 3);
const renderedFormats = await Promise.all(
formats.map(async (fmt) => ({
...fmt,
image: await fmt.make(sharedRaw, { width, height }),
})),
formats.map(async (fmt) =>
Object.assign({}, fmt, { image: await fmt.make(sharedRaw, { width, height }) }),
),
);
await withMediaCap(SMALL_MEDIA_CAP_MB, async () => {

View File

@@ -215,10 +215,11 @@ export async function main() {
}
const rows = Object.values(totalsByJob)
.map((r) => ({
...r,
models: Object.values(r.models).toSorted((a, b) => b.total_tokens - a.total_tokens),
}))
.map((r) =>
Object.assign({}, r, {
models: Object.values(r.models).toSorted((a, b) => b.total_tokens - a.total_tokens),
}),
)
.toSorted((a, b) => b.total_tokens - a.total_tokens);
if (asJson) {

View File

@@ -197,11 +197,12 @@ function mergeTestPlans(plans) {
}
const planGroups = [...groupsByConfig.values()]
.map((group) => ({
...group,
extensionIds: group.extensionIds.toSorted((left, right) => left.localeCompare(right)),
roots: [...new Set(group.roots)],
}))
.map((group) =>
Object.assign({}, group, {
extensionIds: group.extensionIds.toSorted((left, right) => left.localeCompare(right)),
roots: [...new Set(group.roots)],
}),
)
.toSorted((left, right) => left.config.localeCompare(right.config));
return {
@@ -279,6 +280,7 @@ export function createExtensionTestShards(params = {}) {
return shards
.map((shard, index) =>
Object.assign(
{},
{ index, checkName: `checks-node-extensions-shard-${index + 1}` },
mergeTestPlans(shard.plans),
),

View File

@@ -352,17 +352,15 @@ export async function collectPluginClawHubReleasePlan(params?: {
});
const all = await Promise.all(
selectedPublishable.map(async (plugin) => ({
...plugin,
alreadyPublished: await isPluginVersionPublishedOnClawHub(
plugin.packageName,
plugin.version,
{
registryBaseUrl: params?.registryBaseUrl,
fetchImpl: params?.fetchImpl,
},
),
})),
selectedPublishable.map(async (plugin) =>
Object.assign({}, plugin, {
alreadyPublished: await isPluginVersionPublishedOnClawHub(
plugin.packageName,
plugin.version,
{ registryBaseUrl: params?.registryBaseUrl, fetchImpl: params?.fetchImpl },
),
}),
),
);
return {

View File

@@ -462,10 +462,11 @@ export function collectPluginReleasePlan(params?: {
})
: allPublishable;
const all = selectedPublishable.map((plugin) => ({
...plugin,
alreadyPublished: isPluginVersionPublished(plugin.packageName, plugin.version),
}));
const all = selectedPublishable.map((plugin) =>
Object.assign({}, plugin, {
alreadyPublished: isPluginVersionPublished(plugin.packageName, plugin.version),
}),
);
return {
all,

View File

@@ -167,12 +167,13 @@ export function resolveRunnerMatrix(params) {
];
return {
include: runners.flatMap((runner) =>
suites.map((suite) => ({
...runner,
suite,
suite_label: formatSuiteLabel(suite),
lane: suite.includes("upgrade") || suite === "dev-update" ? "upgrade" : "fresh",
})),
suites.map((suite) =>
Object.assign({}, runner, {
suite,
suite_label: formatSuiteLabel(suite),
lane: suite.includes(`upgrade`) || suite === `dev-update` ? `upgrade` : `fresh`,
}),
),
),
};
}

View File

@@ -449,10 +449,11 @@ export function discoverBundledPluginRuntimeDeps(params = {}) {
}
return [...deps.values()]
.map((dep) => ({
...dep,
pluginIds: [...dep.pluginIds].toSorted((a, b) => a.localeCompare(b)),
}))
.map((dep) =>
Object.assign({}, dep, {
pluginIds: [...dep.pluginIds].toSorted((a, b) => a.localeCompare(b)),
}),
)
.toSorted((a, b) => a.name.localeCompare(b.name));
}

View File

@@ -97,14 +97,17 @@ export function mergeProviderModels(
implicitValue: implicitModel.maxTokens,
});
return {
...explicitModel,
input: implicitModel.input,
reasoning: "reasoning" in explicitModel ? explicitModel.reasoning : implicitModel.reasoning,
...(contextWindow === undefined ? {} : { contextWindow }),
...(contextTokens === undefined ? {} : { contextTokens }),
...(maxTokens === undefined ? {} : { maxTokens }),
};
return Object.assign(
{},
explicitModel,
{
input: implicitModel.input,
reasoning: `reasoning` in explicitModel ? explicitModel.reasoning : implicitModel.reasoning,
},
contextWindow === undefined ? {} : { contextWindow },
contextTokens === undefined ? {} : { contextTokens },
maxTokens === undefined ? {} : { maxTokens },
);
});
for (const implicitModel of implicitModels) {

View File

@@ -126,7 +126,7 @@ vi.mock("@mariozechner/pi-ai", async () => {
context: { messages?: Array<{ role?: string; content?: unknown }> },
) => {
streamCallCount += 1;
const messages = (context.messages ?? []).map((message) => ({ ...message }));
const messages = (context.messages ?? []).map((message) => Object.assign({}, message));
observedContexts.push(messages);
const stream = actual.createAssistantMessageEventStream();
queueMicrotask(() => {

View File

@@ -168,7 +168,7 @@ export function applyFinalEffectiveToolPolicy(
}),
{ policy: params.sandboxToolPolicy, label: "sandbox tools.allow" },
{ policy: subagentPolicy, label: "subagent tools.allow" },
].map((step) => ({ ...step, suppressUnavailableCoreToolWarning: true }));
].map((step) => Object.assign({}, step, { suppressUnavailableCoreToolWarning: true }));
return applyToolPolicyPipeline({
tools: ownerFiltered,
toolMeta: (tool) => getPluginToolMeta(tool),

View File

@@ -288,13 +288,12 @@ export function truncateToolResultMessage(
1,
Math.min(maxChars, Math.max(minKeepChars + defaultSuffix.length, proportionalBudget)),
);
return {
...textBlock,
return Object.assign({}, textBlock, {
text: truncateToolResultText(textBlock.text, blockBudget, {
suffix: suffixFactory,
minKeepChars,
}),
};
});
});
return { ...msg, content: newContent } as AgentMessage;

View File

@@ -105,14 +105,14 @@ export function sanitizeToolResult(result: unknown): unknown {
const entry = item as Record<string, unknown>;
const type = readStringValue(entry.type);
if (type === "text" && typeof entry.text === "string") {
return { ...entry, text: truncateToolText(entry.text) };
return Object.assign({}, entry, { text: truncateToolText(entry.text) });
}
if (type === "image") {
const data = readStringValue(entry.data);
const bytes = data ? data.length : undefined;
const cleaned = { ...entry };
delete cleaned.data;
return { ...cleaned, bytes, omitted: true };
return Object.assign({}, cleaned, { bytes, omitted: true });
}
return entry;
});

View File

@@ -126,10 +126,7 @@ function withToolResultText(
(block as { type?: unknown }).type === "text"
) {
replaced = true;
return {
...(block as TextContentBlock),
text,
};
return Object.assign({}, block as TextContentBlock, { text });
}
return block;
});
@@ -332,7 +329,7 @@ async function normalizeReadImageResult(
const nextContent = content.map((block) => {
if (block && typeof block === "object" && (block as { type?: unknown }).type === "image") {
const b = block as ImageContentBlock & { mimeType: string };
return { ...b, mimeType: sniffed } satisfies ImageContentBlock;
return Object.assign({}, b, { mimeType: sniffed }) satisfies ImageContentBlock;
}
if (
block &&
@@ -341,10 +338,9 @@ async function normalizeReadImageResult(
typeof (block as { text?: unknown }).text === "string"
) {
const b = block as TextContentBlock & { text: string };
return {
...b,
return Object.assign({}, b, {
text: rewriteReadImageHeader(b.text, sniffed),
} satisfies TextContentBlock;
}) satisfies TextContentBlock;
}
return block;
});

View File

@@ -39,7 +39,7 @@ const coreTools = [
];
vi.mock("../openclaw-tools.js", () => ({
createOpenClawTools: () => coreTools.map((tool) => ({ ...tool })),
createOpenClawTools: () => coreTools.map((tool) => Object.assign({}, tool)),
__testing: {
setDepsForTest: () => {},
},

View File

@@ -446,7 +446,7 @@ function rewriteAssistantToolCallIds(params: {
return block;
}
changed = true;
return { ...(block as unknown as Record<string, unknown>), id: nextId };
return Object.assign({}, block as unknown as Record<string, unknown>, { id: nextId });
});
if (!changed) {

View File

@@ -364,15 +364,18 @@ export function parseAvailableTags(raw: unknown): AvailableTag[] | undefined {
(t): t is Record<string, unknown> =>
typeof t === "object" && t !== null && typeof t.name === "string",
)
.map((t) => ({
...(t.id !== undefined && typeof t.id === "string" ? { id: t.id } : {}),
name: t.name as string,
...(typeof t.moderated === "boolean" ? { moderated: t.moderated } : {}),
...(t.emoji_id === null || typeof t.emoji_id === "string" ? { emoji_id: t.emoji_id } : {}),
...(t.emoji_name === null || typeof t.emoji_name === "string"
? { emoji_name: t.emoji_name }
: {}),
}));
.map((t) =>
Object.assign(
{},
t.id !== undefined && typeof t.id === `string` ? { id: t.id } : {},
{ name: t.name as string },
typeof t.moderated === `boolean` ? { moderated: t.moderated } : {},
t.emoji_id === null || typeof t.emoji_id === `string` ? { emoji_id: t.emoji_id } : {},
t.emoji_name === null || typeof t.emoji_name === `string`
? { emoji_name: t.emoji_name }
: {},
),
);
// Return undefined instead of empty array to avoid accidentally clearing all tags
return result.length ? result : undefined;
}

View File

@@ -33,7 +33,9 @@ export async function resolveAcpAttachments(params: {
const mediaAttachments = runtime
.normalizeAttachments(params.ctx)
.map((attachment) =>
normalizeOptionalString(attachment.path) ? { ...attachment, url: undefined } : attachment,
normalizeOptionalString(attachment.path)
? Object.assign({}, attachment, { url: undefined })
: attachment,
);
const cache = new runtime.MediaAttachmentCache(mediaAttachments, {
localPathRoots: runtime.resolveMediaAttachmentLocalRoots({

View File

@@ -57,11 +57,10 @@ export function filterMessagingToolMediaDuplicates(params: {
if (!stripSingle && (!mediaUrls || filteredUrls?.length === mediaUrls.length)) {
return payload;
}
return {
...payload,
return Object.assign({}, payload, {
mediaUrl: stripSingle ? undefined : mediaUrl,
mediaUrls: filteredUrls?.length ? filteredUrls : undefined,
};
});
});
}

View File

@@ -33,6 +33,7 @@ function resolveProviderChoiceOptions(params?: {
scope: "text-inference",
}).map((contribution) =>
Object.assign(
{},
{ value: contribution.option.value as AuthChoice, label: contribution.option.label },
contribution.option.hint ? { hint: contribution.option.hint } : {},
contribution.option.assistantPriority !== undefined
@@ -140,10 +141,9 @@ export function buildAuthChoiceGroups(params: {
});
}
const groups = Array.from(groupsById.values())
.map((group) => ({
...group,
options: [...group.options].toSorted(compareAssistantOptions),
}))
.map((group) =>
Object.assign({}, group, { options: [...group.options].toSorted(compareAssistantOptions) }),
)
.toSorted(compareGroupLabels);
const skipOption = params.includeSkip

View File

@@ -160,13 +160,12 @@ export async function resolveBackupPlanFromPaths(params: {
const candidates: BackupAssetCandidate[] = await Promise.all(
rawCandidates.map(async (candidate) => {
const exists = await pathExists(candidate.sourcePath);
return {
...candidate,
return Object.assign({}, candidate, {
exists,
canonicalPath: exists
? await canonicalizeExistingPath(candidate.sourcePath)
: path.resolve(candidate.sourcePath),
};
});
}),
);

View File

@@ -97,13 +97,11 @@ export function resolveChannelSetupEntries(params: {
manifestInstalledIds.has(entry.id as ChannelChoice) &&
shouldShowChannelInSetup(entry.meta),
)
.map((entry) => ({
...entry,
meta: normalizeChannelMeta({
id: entry.id as ChannelChoice,
meta: entry.meta,
.map((entry) =>
Object.assign({}, entry, {
meta: normalizeChannelMeta({ id: entry.id as ChannelChoice, meta: entry.meta }),
}),
}));
);
const installableCatalogEntries = installableCatalogEntriesSource
.filter(
(entry) =>
@@ -111,13 +109,11 @@ export function resolveChannelSetupEntries(params: {
!manifestInstalledIds.has(entry.id as ChannelChoice) &&
shouldShowChannelInSetup(entry.meta),
)
.map((entry) => ({
...entry,
meta: normalizeChannelMeta({
id: entry.id as ChannelChoice,
meta: entry.meta,
.map((entry) =>
Object.assign({}, entry, {
meta: normalizeChannelMeta({ id: entry.id as ChannelChoice, meta: entry.meta }),
}),
}));
);
const metaById = new Map<string, ChannelMeta>();
for (const meta of listChatChannels()) {

View File

@@ -185,13 +185,14 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: {
const { missing, conflicts } = scanBundledPluginRuntimeDeps({ packageRoot });
if (conflicts.length > 0) {
const conflictLines = conflicts.flatMap((conflict) => [
`- ${conflict.name}: ${conflict.versions.join(", ")}`,
...conflict.versions.flatMap((version) => {
const pluginIds = conflict.pluginIdsByVersion.get(version) ?? [];
return pluginIds.length > 0 ? [` - ${version}: ${pluginIds.join(", ")}`] : [];
}),
]);
const conflictLines = conflicts.flatMap((conflict) =>
[`- ${conflict.name}: ${conflict.versions.join(", ")}`].concat(
conflict.versions.flatMap((version) => {
const pluginIds = conflict.pluginIdsByVersion.get(version) ?? [];
return pluginIds.length > 0 ? [` - ${version}: ${pluginIds.join(", ")}`] : [];
}),
),
);
note(
[
"Bundled plugin runtime deps use conflicting versions.",

View File

@@ -531,10 +531,7 @@ export function normalizeLegacyMistralModelMaxTokens(
changes.push(
`Normalized models.providers.${providerId}.models[${index}].maxTokens (${maxTokens}${normalizedMaxTokens}) to avoid Mistral context-window rejects.`,
);
return {
...model,
maxTokens: normalizedMaxTokens,
};
return Object.assign({}, model, { maxTokens: normalizedMaxTokens });
});
if (!modelsChanged) {

View File

@@ -123,16 +123,17 @@ function buildActionRows(params: {
cappedKeys: Set<string>;
budgetEvictedKeys: Set<string>;
}): SessionCleanupActionRow[] {
return toSessionDisplayRows(params.beforeStore).map((row) => ({
...row,
action: resolveSessionCleanupAction({
key: row.key,
missingKeys: params.missingKeys,
staleKeys: params.staleKeys,
cappedKeys: params.cappedKeys,
budgetEvictedKeys: params.budgetEvictedKeys,
return toSessionDisplayRows(params.beforeStore).map((row) =>
Object.assign({}, row, {
action: resolveSessionCleanupAction({
key: row.key,
missingKeys: params.missingKeys,
staleKeys: params.staleKeys,
cappedKeys: params.cappedKeys,
budgetEvictedKeys: params.budgetEvictedKeys,
}),
}),
}));
);
}
function pruneMissingTranscriptEntries(params: {

View File

@@ -146,11 +146,12 @@ export async function sessionsCommand(
const rows = targets
.flatMap((target) => {
const store = loadSessionStore(target.storePath);
return toSessionDisplayRows(store).map((row) => ({
...row,
agentId: parseAgentSessionKey(row.key)?.agentId ?? target.agentId,
kind: classifySessionKey(row.key, store[row.key]),
}));
return toSessionDisplayRows(store).map((row) =>
Object.assign({}, row, {
agentId: parseAgentSessionKey(row.key)?.agentId ?? target.agentId,
kind: classifySessionKey(row.key, store[row.key]),
}),
);
})
.filter((row) => {
if (activeMinutes === undefined) {

View File

@@ -51,7 +51,7 @@ export function buildStatusChannelsSection(params: {
width: params.width,
renderTable: params.renderTable,
columns: statusChannelsTableColumns.map((column) =>
column.key === "Detail" ? { ...column, minWidth: 28 } : column,
column.key === "Detail" ? Object.assign({}, column, { minWidth: 28 }) : column,
),
rows: buildStatusChannelsTableRows({
rows: params.rows,

View File

@@ -264,11 +264,11 @@ async function createMockStatusScanResult(params: { includePluginCompatibility?:
...mocks.listGatewayAgentsBasic(),
bootstrapPendingCount: 0,
totalSessions: 1,
agents: mocks.listGatewayAgentsBasic().agents.map((agent: { id: string; name?: string }) => ({
...agent,
bootstrapPending: false,
activeSessions: 1,
})),
agents: mocks
.listGatewayAgentsBasic()
.agents.map((agent: { id: string; name?: string }) =>
Object.assign({}, agent, { bootstrapPending: false, activeSessions: 1 }),
),
};
const sessions = createSessionStatusRows();
const channelIssues = gatewayReachable

View File

@@ -211,15 +211,14 @@ export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig {
return model;
}
providerMutated = true;
return {
...raw,
return Object.assign({}, raw, {
reasoning,
input,
cost,
contextWindow,
maxTokens,
api,
} as ModelDefinitionConfig;
}) as ModelDefinitionConfig;
});
if (!providerMutated) {

View File

@@ -248,7 +248,9 @@ export async function resolveAllAgentSessionStoreTargets(
agentsRoot,
realAgentsRoot,
});
return validatedStorePath ? { ...target, storePath: validatedStorePath } : undefined;
return validatedStorePath
? Object.assign({}, target, { storePath: validatedStorePath })
: undefined;
}),
)
).filter((target): target is SessionStoreTarget => Boolean(target));

View File

@@ -514,10 +514,9 @@ export async function dispatchCronDelivery(
return p;
}
const normalized = normalizeSilentReplyText(p.text);
return {
...p,
return Object.assign({}, p, {
text: normalized.strippedTrailingSilentToken ? undefined : normalized.text,
};
});
})
.filter((p) => hasReplyPayloadContent(p, { trimText: true }));
if (payloadsForDelivery.length === 0) {

View File

@@ -118,41 +118,47 @@ function listReloadRules(): ReloadRule[] {
return cachedReloadRules;
}
// Channel docking: plugins contribute hot reload/no-op prefixes here.
const channelReloadRules: ReloadRule[] = listChannelPlugins().flatMap((plugin) => [
...(plugin.reload?.configPrefixes ?? []).map(
(prefix): ReloadRule => ({
prefix,
kind: "hot",
actions: [`restart-channel:${plugin.id}` as ReloadAction],
}),
),
...(plugin.reload?.noopPrefixes ?? []).map(
(prefix): ReloadRule => ({
prefix,
kind: "none",
}),
),
]);
const pluginReloadRules: ReloadRule[] = (registry?.reloads ?? []).flatMap((entry) => [
...(entry.registration.restartPrefixes ?? []).map(
(prefix): ReloadRule => ({
prefix,
kind: "restart",
}),
),
...(entry.registration.hotPrefixes ?? []).map(
(prefix): ReloadRule => ({
prefix,
kind: "hot",
}),
),
...(entry.registration.noopPrefixes ?? []).map(
(prefix): ReloadRule => ({
prefix,
kind: "none",
}),
),
]);
const channelReloadRules: ReloadRule[] = listChannelPlugins().flatMap((plugin) =>
(plugin.reload?.configPrefixes ?? [])
.map(
(prefix): ReloadRule => ({
prefix,
kind: "hot",
actions: [`restart-channel:${plugin.id}` as ReloadAction],
}),
)
.concat(
(plugin.reload?.noopPrefixes ?? []).map(
(prefix): ReloadRule => ({
prefix,
kind: "none",
}),
),
),
);
const pluginReloadRules: ReloadRule[] = (registry?.reloads ?? []).flatMap((entry) =>
(entry.registration.restartPrefixes ?? [])
.map(
(prefix): ReloadRule => ({
prefix,
kind: "restart",
}),
)
.concat(
(entry.registration.hotPrefixes ?? []).map(
(prefix): ReloadRule => ({
prefix,
kind: "hot",
}),
),
(entry.registration.noopPrefixes ?? []).map(
(prefix): ReloadRule => ({
prefix,
kind: "none",
}),
),
),
);
const rules = [
...BASE_RELOAD_RULES,
...pluginReloadRules,

View File

@@ -1213,10 +1213,9 @@ function buildLiveGatewayConfig(params: {
...params.cfg,
agents: {
...params.cfg.agents,
list: (params.cfg.agents?.list ?? []).map((entry) => ({
...entry,
sandbox: { mode: "off" },
})),
list: (params.cfg.agents?.list ?? []).map((entry) =>
Object.assign({}, entry, { sandbox: { mode: `off` } }),
),
defaults: {
...params.cfg.agents?.defaults,
// Live tests should avoid Docker sandboxing so tool probes can

View File

@@ -221,7 +221,9 @@ type MockConfig = {
};
function getAgentList(cfg: unknown): MockAgentEntry[] {
return ((cfg as MockConfig | undefined)?.agents?.list ?? []).map((entry) => ({ ...entry }));
return ((cfg as MockConfig | undefined)?.agents?.list ?? []).map((entry) =>
Object.assign({}, entry),
);
}
function mergeAgentConfig(cfg: unknown, opts: unknown): MockConfig {

View File

@@ -120,10 +120,9 @@ function buildPluginGroups(params: {
groups.set(groupId, existing);
}
return [...groups.values()]
.map((group) => ({
...group,
tools: group.tools.toSorted((a, b) => a.id.localeCompare(b.id)),
}))
.map((group) =>
Object.assign({}, group, { tools: group.tools.toSorted((a, b) => a.id.localeCompare(b.id)) }),
)
.toSorted((a, b) => a.label.localeCompare(b.label));
}

View File

@@ -293,7 +293,7 @@ async function discoverAllSessionsForUsage(params: {
startMs: params.startMs,
endMs: params.endMs,
});
return sessions.map((session) => ({ ...session, agentId: agent.id }));
return sessions.map((session) => Object.assign({}, session, { agentId: agent.id }));
}),
);
return results.flat().toSorted((a, b) => b.mtime - a.mtime);

View File

@@ -574,10 +574,7 @@ async function discoverViaAvahi(
args.push("-d", domain.replace(/\.$/, ""));
}
const browse = await run(args, { timeoutMs });
return parseAvahiBrowse(browse.stdout).map((beacon) => ({
...beacon,
domain,
}));
return parseAvahiBrowse(browse.stdout).map((beacon) => Object.assign({}, beacon, { domain }));
}
export async function discoverGatewayBeacons(

View File

@@ -854,13 +854,12 @@ export function recordAllowlistUse(
const nextAllowlist = allowlist.map((item) =>
item.pattern === entry.pattern &&
(item.argPattern ?? undefined) === (entry.argPattern ?? undefined)
? {
...item,
? Object.assign({}, item, {
id: item.id ?? crypto.randomUUID(),
lastUsedAt: Date.now(),
lastUsedCommand: command,
lastResolvedPath: resolvedPath,
}
})
: item,
);
agents[target] = { ...existing, allowlist: nextAllowlist };

View File

@@ -967,10 +967,11 @@ export function markdownToIRWithMeta(
links: clampLinkSpans(state.links, finalLength),
},
hasTables: state.hasTables,
tables: state.collectedTables.map((table) => ({
...table,
placeholderOffset: Math.min(table.placeholderOffset, finalLength),
})),
tables: state.collectedTables.map((table) =>
Object.assign({}, table, {
placeholderOffset: Math.min(table.placeholderOffset, finalLength),
}),
),
};
}

View File

@@ -341,11 +341,9 @@ describe("pairing store", () => {
requests?: Array<Record<string, unknown>>;
};
const expiredAt = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
const requests = (parsed.requests ?? []).map((entry) => ({
...entry,
createdAt: expiredAt,
lastSeenAt: expiredAt,
}));
const requests = (parsed.requests ?? []).map((entry) =>
Object.assign({}, entry, { createdAt: expiredAt, lastSeenAt: expiredAt }),
);
await writeJsonFixture(filePath, { version: 1, requests });
expect(await listChannelPairingRequests("demo-pairing-b")).toHaveLength(0);
const next = await upsertChannelPairingRequest({

View File

@@ -28,8 +28,7 @@ function loadPluginRuntime(): PluginRuntimeModule | null {
}
export function resolveRuntimeCliBackends(): PluginCliBackendEntry[] {
return (loadPluginRuntime()?.getActivePluginRegistry()?.cliBackends ?? []).map((entry) => ({
...entry.backend,
pluginId: entry.pluginId,
}));
return (loadPluginRuntime()?.getActivePluginRegistry()?.cliBackends ?? []).map((entry) =>
Object.assign({}, entry.backend, { pluginId: entry.pluginId }),
);
}

View File

@@ -2822,12 +2822,13 @@ module.exports = { id: "throws-after-import", register() {} };`,
] as const;
runSinglePluginRegistryScenarios(
scenarios.map((scenario) => ({
...scenario,
body: `module.exports = { id: "${scenario.pluginId}", register(api) {
scenarios.map((scenario) =>
Object.assign({}, scenario, {
body: `module.exports = { id: "${scenario.pluginId}", register(api) {
api.registerHttpRoute(${scenario.routeOptions});
} };`,
})),
}),
),
);
});

View File

@@ -57,10 +57,9 @@ function resolveProviderDiscoveryEntryPlugins(params: {
try {
const moduleExport = loadSource(manifest.providerDiscoverySource!) as ProviderDiscoveryModule;
providers.push(
...normalizeDiscoveryModule(moduleExport).map((provider) => ({
...provider,
pluginId: manifest.id,
})),
...normalizeDiscoveryModule(moduleExport).map((provider) =>
Object.assign({}, provider, { pluginId: manifest.id }),
),
);
} catch {
// Discovery fast path is optional. Fall back to the full plugin loader

View File

@@ -275,10 +275,9 @@ export function resolvePluginProviders(params: {
return [];
}
const registry = loadOpenClawPlugins(loadState.loadOptions);
return registry.providers.map((entry) => ({
...entry.provider,
pluginId: entry.pluginId,
}));
return registry.providers.map((entry) =>
Object.assign({}, entry.provider, { pluginId: entry.pluginId }),
);
}
const loadState = resolveRuntimeProviderPluginLoadState(params, base);
const registry = resolveRuntimePluginRegistry(loadState.loadOptions);
@@ -286,8 +285,7 @@ export function resolvePluginProviders(params: {
return [];
}
return registry.providers.map((entry) => ({
...entry.provider,
pluginId: entry.pluginId,
}));
return registry.providers.map((entry) =>
Object.assign({}, entry.provider, { pluginId: entry.pluginId }),
);
}

View File

@@ -19,29 +19,38 @@ const LIVE_RUNTIME_STATE_GUARDS: Record<
},
};
function guardAssertions() {
return Object.entries(LIVE_RUNTIME_STATE_GUARDS).flatMap(([relativePath, guard]) => [
...guard.required.map((needle) => ({
relativePath,
type: "required" as const,
needle,
message: `${relativePath} missing ${needle}`,
})),
...guard.forbidden.map((needle) => ({
relativePath,
type: "forbidden" as const,
needle,
message: `${relativePath} must not contain ${needle}`,
})),
]);
}
function expectGuardState(params: {
source: string;
type GuardAssertion = {
relativePath: string;
type: "required" | "forbidden";
needle: string;
message: string;
}) {
};
function guardAssertions(): GuardAssertion[] {
return Object.entries(LIVE_RUNTIME_STATE_GUARDS).flatMap(([relativePath, guard]) =>
guard.required
.map<GuardAssertion>((needle) => ({
relativePath,
type: "required",
needle,
message: `${relativePath} missing ${needle}`,
}))
.concat(
guard.forbidden.map<GuardAssertion>((needle) => ({
relativePath,
type: "forbidden",
needle,
message: `${relativePath} must not contain ${needle}`,
})),
),
);
}
function expectGuardState(
params: {
source: string;
} & Pick<GuardAssertion, "message" | "needle" | "type">,
) {
if (params.type === "required") {
expect(params.source, params.message).toContain(params.needle);
return;

View File

@@ -208,11 +208,12 @@ function buildPluginReport(
return {
workspaceDir,
...registry,
plugins: registry.plugins.map((plugin) => ({
...plugin,
imported: plugin.format !== "bundle" && importedPluginIds.has(plugin.id),
version: resolveReportedPluginVersion(plugin, params?.env),
})),
plugins: registry.plugins.map((plugin) =>
Object.assign({}, plugin, {
imported: plugin.format !== `bundle` && importedPluginIds.has(plugin.id),
version: resolveReportedPluginVersion(plugin, params?.env),
}),
),
};
}

View File

@@ -126,7 +126,7 @@ export function loadBundledWebSearchProviderEntriesFromDir(params: {
if (providers.length === 0) {
return null;
}
return providers.map((provider) => ({ ...provider, pluginId: params.pluginId }));
return providers.map((provider) => Object.assign({}, provider, { pluginId: params.pluginId }));
}
export function loadBundledRuntimeWebSearchProviderEntriesFromDir(params: {
@@ -148,7 +148,7 @@ export function loadBundledRuntimeWebSearchProviderEntriesFromDir(params: {
if (providers.length === 0) {
return null;
}
return providers.map((provider) => ({ ...provider, pluginId: params.pluginId }));
return providers.map((provider) => Object.assign({}, provider, { pluginId: params.pluginId }));
}
export function loadBundledWebFetchProviderEntriesFromDir(params: {
@@ -170,7 +170,7 @@ export function loadBundledWebFetchProviderEntriesFromDir(params: {
if (providers.length === 0) {
return null;
}
return providers.map((provider) => ({ ...provider, pluginId: params.pluginId }));
return providers.map((provider) => Object.assign({}, provider, { pluginId: params.pluginId }));
}
export function resolveBundledExplicitWebSearchProvidersFromPublicArtifacts(params: {

View File

@@ -175,9 +175,6 @@ export function mapRegistryProviders<TProvider extends { id: string }>(params: {
return params.sortProviders(
params.entries
.filter((entry) => !onlyPluginIdSet || onlyPluginIdSet.has(entry.pluginId))
.map((entry) => ({
...entry.provider,
pluginId: entry.pluginId,
})),
.map((entry) => Object.assign({}, entry.provider, { pluginId: entry.pluginId })),
);
}

View File

@@ -75,7 +75,7 @@ export function createRunRegistry(options?: { maxExitedRecords?: number }): RunR
};
const list: RunRegistry["list"] = () => {
return Array.from(records.values()).map((record) => ({ ...record }));
return Array.from(records.values()).map((record) => Object.assign({}, record));
};
const listByScope: RunRegistry["listByScope"] = (scopeKey) => {
@@ -84,7 +84,7 @@ export function createRunRegistry(options?: { maxExitedRecords?: number }): RunR
}
return Array.from(records.values())
.filter((record) => record.scopeKey === scopeKey)
.map((record) => ({ ...record }));
.map((record) => Object.assign({}, record));
};
const updateState: RunRegistry["updateState"] = (runId, state, patch) => {

View File

@@ -1814,7 +1814,7 @@ export async function cancelTaskById(params: {
export function listTaskRecords(): TaskRecord[] {
ensureTaskRegistryReady();
return [...tasks.values()]
.map((task, insertionIndex) => ({ ...cloneTaskRecord(task), insertionIndex }))
.map((task, insertionIndex) => Object.assign({}, cloneTaskRecord(task), { insertionIndex }))
.toSorted(compareTasksNewestFirst)
.map(({ insertionIndex: _, ...task }) => task);
}
@@ -1851,7 +1851,7 @@ function listTasksFromIndex(index: Map<string, Set<string>>, key: string): TaskR
return [...ids]
.map((taskId, insertionIndex) => {
const task = tasks.get(taskId);
return task ? { ...cloneTaskRecord(task), insertionIndex } : null;
return task ? Object.assign({}, cloneTaskRecord(task), { insertionIndex }) : null;
})
.filter(
(

View File

@@ -155,7 +155,7 @@ function buildToolRichSystemPrompt(params: {
"web_search",
"x_search",
"web_fetch",
].map((name) => ({ ...createStubTool(name), description: `${name} tool` }));
].map((name) => Object.assign({}, createStubTool(name), { description: `${name} tool` }));
return buildEmbeddedSystemPrompt({
workspaceDir: params.workspaceDir,
reasoningTagHint: false,

View File

@@ -211,13 +211,7 @@ function expandTextContent(text: string): {
const content = mergeAdjacentTextItems(
parts.map((item) => {
if (item.type === "attachment" && item.attachment.kind === "audio" && audioAsVoice) {
return {
...item,
attachment: {
...item.attachment,
isVoiceNote: true,
},
};
return Object.assign({}, item, { attachment: { ...item.attachment, isVoiceNote: true } });
}
return item;
}),

View File

@@ -276,12 +276,7 @@ function patchSessionThinkingLevel(
state.sessionsResult = {
...current,
sessions: current.sessions.map((row) =>
row.key === sessionKey
? {
...row,
thinkingLevel,
}
: row,
row.key === sessionKey ? Object.assign({}, row, { thinkingLevel }) : row,
),
};
}

View File

@@ -724,12 +724,13 @@ export function renderConfig(props: ConfigProps) {
const schemaProps = analysis.schema?.properties ?? {};
const VIRTUAL_SECTIONS = new Set(["__appearance__"]);
const visibleCategories = SECTION_CATEGORIES.map((cat) => ({
...cat,
sections: cat.sections.filter(
(s) => (includeVirtualSections && VIRTUAL_SECTIONS.has(s.key)) || s.key in schemaProps,
),
})).filter((cat) => cat.sections.length > 0);
const visibleCategories = SECTION_CATEGORIES.map((cat) =>
Object.assign({}, cat, {
sections: cat.sections.filter(
(s) => (includeVirtualSections && VIRTUAL_SECTIONS.has(s.key)) || s.key in schemaProps,
),
}),
).filter((cat) => cat.sections.length > 0);
// Catch any schema keys not in our categories
const extraSections = Object.keys(schemaProps)

View File

@@ -80,10 +80,7 @@ function formatDiaryChipLabel(date: string): string {
function buildDiaryNavigation(entries: DiaryEntry[]): DiaryEntryNav[] {
const reversed = [...entries].toReversed();
return reversed.map((entry, page) => ({
...entry,
page,
}));
return reversed.map((entry, page) => Object.assign({}, entry, { page }));
}
type DreamingPhaseInfo = {