fix: repair runtime seams after rebase

This commit is contained in:
Peter Steinberger
2026-03-23 09:13:23 +00:00
parent 2a06097184
commit 7ba28d6dba
14 changed files with 134 additions and 143 deletions

View File

@@ -6,6 +6,8 @@ import type { ModelProviderAuthMode, ModelProviderConfig } from "../config/types
import { coerceSecretRef } from "../config/types.secrets.js";
import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { buildProviderMissingAuthMessageWithPlugin } from "../plugins/provider-runtime.runtime.js";
import { resolveOwningPluginIdsForProvider } from "../plugins/providers.js";
import {
normalizeOptionalSecretInput,
normalizeSecretInput,
@@ -35,15 +37,6 @@ const AWS_BEARER_ENV = "AWS_BEARER_TOKEN_BEDROCK";
const AWS_ACCESS_KEY_ENV = "AWS_ACCESS_KEY_ID";
const AWS_SECRET_KEY_ENV = "AWS_SECRET_ACCESS_KEY";
const AWS_PROFILE_ENV = "AWS_PROFILE";
let providerRuntimePromise:
| Promise<typeof import("../plugins/provider-runtime.runtime.js")>
| undefined;
function loadProviderRuntime() {
providerRuntimePromise ??= import("../plugins/provider-runtime.runtime.js");
return providerRuntimePromise;
}
function resolveProviderConfig(
cfg: OpenClawConfig | undefined,
provider: string,
@@ -366,20 +359,30 @@ export async function resolveApiKeyForProvider(params: {
return resolveAwsSdkAuthInfo();
}
const { buildProviderMissingAuthMessageWithPlugin } = await loadProviderRuntime();
const pluginMissingAuthMessage = buildProviderMissingAuthMessageWithPlugin({
provider,
config: cfg,
context: {
config: cfg,
agentDir: params.agentDir,
env: process.env,
const providerConfig = resolveProviderConfig(cfg, provider);
const hasInlineConfiguredModels =
Array.isArray(providerConfig?.models) && providerConfig.models.length > 0;
const owningPluginIds = !hasInlineConfiguredModels
? resolveOwningPluginIdsForProvider({
provider,
config: cfg,
})
: undefined;
if (owningPluginIds?.length) {
const pluginMissingAuthMessage = buildProviderMissingAuthMessageWithPlugin({
provider,
listProfileIds: (providerId) => listProfilesForProvider(store, providerId),
},
});
if (pluginMissingAuthMessage) {
throw new Error(pluginMissingAuthMessage);
config: cfg,
context: {
config: cfg,
agentDir: params.agentDir,
env: process.env,
provider,
listProfileIds: (providerId) => listProfilesForProvider(store, providerId),
},
});
if (pluginMissingAuthMessage) {
throw new Error(pluginMissingAuthMessage);
}
}
const authStorePath = resolveAuthStorePathForDisplay(params.agentDir);

View File

@@ -11,6 +11,7 @@ import type { EmbeddedRunAttemptResult } from "./pi-embedded-runner/run/types.js
const runEmbeddedAttemptMock = vi.fn<(params: unknown) => Promise<EmbeddedRunAttemptResult>>();
const resolveCopilotApiTokenMock = vi.fn();
const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
const { computeBackoffMock, sleepWithAbortMock } = vi.hoisted(() => ({
computeBackoffMock: vi.fn(
(
@@ -38,30 +39,6 @@ vi.mock("../../extensions/github-copilot/token.js", () => ({
resolveCopilotApiToken: (...args: unknown[]) => resolveCopilotApiTokenMock(...args),
}));
vi.mock("../plugins/provider-runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../plugins/provider-runtime.js")>();
return {
...actual,
prepareProviderRuntimeAuth: async (params: {
provider: string;
context: { apiKey: string; env: NodeJS.ProcessEnv };
}) => {
if (params.provider !== "github-copilot") {
return undefined;
}
const token = await resolveCopilotApiTokenMock({
githubToken: params.context.apiKey,
env: params.context.env,
});
return {
apiKey: token.token,
baseUrl: token.baseUrl,
expiresAt: token.expiresAt,
};
},
};
});
vi.mock("./pi-embedded-runner/compact.js", () => ({
compactEmbeddedPiSessionDirect: vi.fn(async () => {
throw new Error("compact should not run in auth profile rotation tests");
@@ -78,6 +55,7 @@ vi.mock("./models-config.js", async (importOriginal) => {
let runEmbeddedPiAgent: typeof import("./pi-embedded-runner/run.js").runEmbeddedPiAgent;
let unregisterLogTransport: (() => void) | undefined;
const originalFetch = globalThis.fetch;
beforeAll(async () => {
({ runEmbeddedPiAgent } = await import("./pi-embedded-runner/run.js"));
@@ -87,11 +65,27 @@ beforeEach(() => {
vi.useRealTimers();
runEmbeddedAttemptMock.mockClear();
resolveCopilotApiTokenMock.mockReset();
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
if (url !== COPILOT_TOKEN_URL) {
throw new Error(`Unexpected fetch in test: ${url}`);
}
const token = await resolveCopilotApiTokenMock();
return {
ok: true,
status: 200,
json: async () => ({
token: token.token,
expires_at: Math.floor(token.expiresAt / 1000),
}),
} as Response;
}) as typeof fetch;
computeBackoffMock.mockClear();
sleepWithAbortMock.mockClear();
});
afterEach(() => {
globalThis.fetch = originalFetch;
unregisterLogTransport?.();
unregisterLogTransport = undefined;
setLoggerOverride(null);
@@ -517,11 +511,9 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
it("refreshes copilot token after auth error and retries once", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));
vi.useFakeTimers();
try {
await writeCopilotAuthStore(agentDir);
const now = Date.now();
vi.setSystemTime(now);
resolveCopilotApiTokenMock
.mockResolvedValueOnce({
@@ -575,7 +567,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(2);
expect(resolveCopilotApiTokenMock).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
await fs.rm(agentDir, { recursive: true, force: true });
await fs.rm(workspaceDir, { recursive: true, force: true });
}
@@ -584,11 +575,9 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
it("allows another auth refresh after a successful retry", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));
vi.useFakeTimers();
try {
await writeCopilotAuthStore(agentDir);
const now = Date.now();
vi.setSystemTime(now);
resolveCopilotApiTokenMock
.mockResolvedValueOnce({
@@ -662,7 +651,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(4);
expect(resolveCopilotApiTokenMock).toHaveBeenCalledTimes(3);
} finally {
vi.useRealTimers();
await fs.rm(agentDir, { recursive: true, force: true });
await fs.rm(workspaceDir, { recursive: true, force: true });
}

View File

@@ -25,7 +25,7 @@ import {
resolveTelegramReactionLevel,
} from "../../plugin-sdk/telegram.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { prepareProviderRuntimeAuth } from "../../plugins/provider-runtime.js";
import { prepareProviderRuntimeAuth } from "../../plugins/provider-runtime.runtime.js";
import { type enqueueCommand, enqueueCommandInLane } from "../../process/command-queue.js";
import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-key.js";
import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js";

View File

@@ -194,6 +194,22 @@ function resolveExplicitModelWithRegistry(params: {
return { kind: "suppressed" };
}
const providerConfig = resolveConfiguredProviderConfig(cfg, provider);
const inlineModels = buildInlineProviderModels(cfg?.models?.providers ?? {});
const normalizedProvider = normalizeProviderId(provider);
const inlineMatch = inlineModels.find(
(entry) => normalizeProviderId(entry.provider) === normalizedProvider && entry.id === modelId,
);
if (inlineMatch?.api) {
return {
kind: "resolved",
model: normalizeResolvedModel({
provider,
cfg,
agentDir,
model: inlineMatch as Model<Api>,
}),
};
}
const model = modelRegistry.find(provider, modelId) as Model<Api> | null;
if (model) {
@@ -213,19 +229,17 @@ function resolveExplicitModelWithRegistry(params: {
}
const providers = cfg?.models?.providers ?? {};
const inlineModels = buildInlineProviderModels(providers);
const normalizedProvider = normalizeProviderId(provider);
const inlineMatch = inlineModels.find(
const fallbackInlineMatch = buildInlineProviderModels(providers).find(
(entry) => normalizeProviderId(entry.provider) === normalizedProvider && entry.id === modelId,
);
if (inlineMatch?.api) {
if (fallbackInlineMatch?.api) {
return {
kind: "resolved",
model: normalizeResolvedModel({
provider,
cfg,
agentDir,
model: inlineMatch as Model<Api>,
model: fallbackInlineMatch as Model<Api>,
}),
};
}

View File

@@ -8,7 +8,7 @@ import {
import { computeBackoff, sleepWithAbort, type BackoffPolicy } from "../../infra/backoff.js";
import { generateSecureToken } from "../../infra/secure-random.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { prepareProviderRuntimeAuth } from "../../plugins/provider-runtime.js";
import { prepareProviderRuntimeAuth } from "../../plugins/provider-runtime.runtime.js";
import type { PluginHookBeforeAgentStartResult } from "../../plugins/types.js";
import { enqueueCommandInLane } from "../../process/command-queue.js";
import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js";

View File

@@ -47,6 +47,7 @@ import {
import { type AnnounceQueueItem, enqueueAnnounce } from "./subagent-announce-queue.js";
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
import type { SpawnSubagentMode } from "./subagent-spawn.js";
import { readLatestAssistantReply } from "./tools/agent-step.js";
import { sanitizeTextContent, extractAssistantText } from "./tools/sessions-helpers.js";
import { isAnnounceSkip } from "./tools/sessions-send-helpers.js";
@@ -391,7 +392,12 @@ async function readSubagentOutput(
params: { sessionKey, limit: 100 },
});
const messages = Array.isArray(history?.messages) ? history.messages : [];
return selectSubagentOutputText(summarizeSubagentOutputHistory(messages), outcome);
const selected = selectSubagentOutputText(summarizeSubagentOutputHistory(messages), outcome);
if (selected?.trim()) {
return selected;
}
const latestAssistant = await readLatestAssistantReply({ sessionKey, limit: 100 });
return latestAssistant?.trim() ? latestAssistant : undefined;
}
async function readLatestSubagentOutputWithRetry(params: {
@@ -1416,16 +1422,6 @@ export async function runSubagentAnnounceFlow(params: {
reply = fallbackReply;
}
if (
!expectsCompletionMessage &&
!reply?.trim() &&
childSessionId &&
isEmbeddedPiRunActive(childSessionId)
) {
shouldDeleteChildSession = false;
return false;
}
if (isAnnounceSkip(reply) || isSilentReplyText(reply, SILENT_REPLY_TOKEN)) {
if (fallbackReply && !fallbackIsSilent) {
reply = fallbackReply;

View File

@@ -44,8 +44,8 @@ export function registerGroupIntroPromptCases(): void {
Provider: "whatsapp",
},
expected: [
`You are in the WhatsApp group chat "Ops".`,
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
`You are in the WhatsApp group chat "Ops". Your replies are automatically sent to this group chat. Do not use the message tool to send to this same group — just reply normally.`,
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). WhatsApp IDs: SenderId is the participant JID (group participant id). ${groupParticipationNote} Address the specific sender noted in the message context.`,
],
},
{

View File

@@ -141,6 +141,10 @@ async function expectResetBlockedForNonOwner(params: { home: string }): Promise<
...cfg.channels.whatsapp,
allowFrom: ["+1999"],
};
cfg.commands = {
...cfg.commands,
ownerAllowFrom: ["whatsapp:+1999"],
};
cfg.session = {
...cfg.session,
store: join(home, "blocked-reset.sessions.json"),

View File

@@ -47,16 +47,9 @@ import {
import { type BlockReplyPipeline } from "./block-reply-pipeline.js";
import type { FollowupRun } from "./queue.js";
import { createBlockReplyDeliveryHandler } from "./reply-delivery.js";
import { createReplyMediaPathNormalizer } from "./reply-media-paths.runtime.js";
import type { TypingSignaler } from "./typing-mode.js";
let replyMediaPathsRuntimePromise: Promise<typeof import("./reply-media-paths.runtime.js")> | null =
null;
function loadReplyMediaPathsRuntime() {
replyMediaPathsRuntimePromise ??= import("./reply-media-paths.runtime.js");
return replyMediaPathsRuntimePromise;
}
export type RuntimeFallbackAttempt = {
provider: string;
model: string;
@@ -116,7 +109,6 @@ export async function runAgentTurnWithFallback(params: {
const directlySentBlockKeys = new Set<string>();
const runId = params.opts?.runId ?? crypto.randomUUID();
const { createReplyMediaPathNormalizer } = await loadReplyMediaPathsRuntime();
const normalizeReplyMediaPaths = createReplyMediaPathNormalizer({
cfg: params.followupRun.run.config,
sessionKey: params.sessionKey,

View File

@@ -380,6 +380,18 @@ describe("runReplyAgent heartbeat followup guard", () => {
expect(vi.mocked(enqueueFollowupRunMock)).toHaveBeenCalledTimes(1);
expect(state.runEmbeddedPiAgentMock).not.toHaveBeenCalled();
});
it("drains followup queue when an unexpected exception escapes the run path", async () => {
accountingState.persistRunSessionUsageMock.mockRejectedValueOnce(new Error("persist exploded"));
state.runEmbeddedPiAgentMock.mockResolvedValueOnce({
payloads: [{ text: "ok" }],
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
});
const { run } = createMinimalRun();
await expect(run()).rejects.toThrow("persist exploded");
expect(vi.mocked(scheduleFollowupDrainMock)).toHaveBeenCalledTimes(1);
});
});
describe("runReplyAgent typing (heartbeat)", () => {
@@ -674,26 +686,37 @@ describe("runReplyAgent typing (heartbeat)", () => {
it("retries transient HTTP failures once with timer-driven backoff", async () => {
vi.useFakeTimers();
let calls = 0;
state.runEmbeddedPiAgentMock.mockImplementation(async () => {
calls += 1;
if (calls === 1) {
throw new Error("502 Bad Gateway");
}
return { payloads: [{ text: "final" }], meta: {} };
});
try {
let calls = 0;
state.runEmbeddedPiAgentMock.mockImplementation(async () => {
calls += 1;
if (calls === 1) {
throw new Error("502 Bad Gateway");
}
return { payloads: [{ text: "final" }], meta: {} };
});
const { run } = createMinimalRun({
typingMode: "message",
});
const runPromise = run();
const { run } = createMinimalRun({
typingMode: "message",
});
const runPromise = run();
void runPromise.catch(() => {});
await vi.dynamicImportSettled();
await vi.advanceTimersByTimeAsync(2_499);
expect(calls).toBe(1);
await vi.advanceTimersByTimeAsync(1);
await runPromise;
expect(calls).toBe(2);
vi.useRealTimers();
vi.advanceTimersByTime(2_499);
await Promise.resolve();
expect(calls).toBe(1);
vi.advanceTimersByTime(1);
await Promise.resolve();
await Promise.resolve();
expect(calls).toBe(2);
// Restore real timers before awaiting the settled run to avoid Vitest
// fake-timer bookkeeping stalling the test worker after the retry fires.
vi.useRealTimers();
} finally {
vi.useRealTimers();
}
});
it("delivers tool results in order even when dispatched concurrently", async () => {

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import { lookupCachedContextTokens } from "../../agents/context-cache.js";
import { lookupContextTokens } from "../../agents/context-tokens.runtime.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import { resolveModelAuthMode } from "../../agents/model-auth.js";
import { isCliProvider } from "../../agents/model-selection.js";
@@ -25,6 +26,7 @@ import {
import type { OriginatingChannelType, TemplateContext } from "../templating.js";
import { resolveResponseUsageMode, type VerboseLevel } from "../thinking.js";
import type { GetReplyOptions, ReplyPayload } from "../types.js";
import { runAgentTurnWithFallback } from "./agent-runner-execution.runtime.js";
import {
createShouldEmitToolOutput,
createShouldEmitToolResult,
@@ -32,6 +34,7 @@ import {
isAudioPayload,
signalTypingIfNeeded,
} from "./agent-runner-helpers.js";
import { runMemoryFlushIfNeeded } from "./agent-runner-memory.runtime.js";
import { buildReplyPayloads } from "./agent-runner-payloads.js";
import {
appendUnscheduledReminderNote,
@@ -45,8 +48,9 @@ import { createFollowupRunner } from "./followup-runner.js";
import { resolveOriginMessageProvider, resolveOriginMessageTo } from "./origin-routing.js";
import { readPostCompactionContext } from "./post-compaction-context.js";
import { resolveActiveRunQueueAction } from "./queue-policy.js";
import type { FollowupRun, QueueSettings } from "./queue.js";
import { enqueueFollowupRun } from "./queue/enqueue.js";
import type { FollowupRun, QueueSettings } from "./queue/types.js";
import { createReplyMediaPathNormalizer } from "./reply-media-paths.runtime.js";
import { createReplyToModeFilterForChannel, resolveReplyToMode } from "./reply-threading.js";
import { incrementRunCompactionCount, persistRunSessionUsage } from "./session-run-accounting.js";
import { createTypingSignaler } from "./typing-mode.js";
@@ -56,18 +60,7 @@ const BLOCK_REPLY_SEND_TIMEOUT_MS = 15_000;
let piEmbeddedQueueRuntimePromise: Promise<
typeof import("../../agents/pi-embedded-queue.runtime.js")
> | null = null;
let agentRunnerExecutionRuntimePromise: Promise<
typeof import("./agent-runner-execution.runtime.js")
> | null = null;
let agentRunnerMemoryRuntimePromise: Promise<
typeof import("./agent-runner-memory.runtime.js")
> | null = null;
let usageCostRuntimePromise: Promise<typeof import("./usage-cost.runtime.js")> | null = null;
let contextTokensRuntimePromise: Promise<
typeof import("../../agents/context-tokens.runtime.js")
> | null = null;
let replyMediaPathsRuntimePromise: Promise<typeof import("./reply-media-paths.runtime.js")> | null =
null;
let sessionStoreRuntimePromise: Promise<
typeof import("../../config/sessions/store.runtime.js")
> | null = null;
@@ -77,31 +70,11 @@ function loadPiEmbeddedQueueRuntime() {
return piEmbeddedQueueRuntimePromise;
}
function loadAgentRunnerExecutionRuntime() {
agentRunnerExecutionRuntimePromise ??= import("./agent-runner-execution.runtime.js");
return agentRunnerExecutionRuntimePromise;
}
function loadAgentRunnerMemoryRuntime() {
agentRunnerMemoryRuntimePromise ??= import("./agent-runner-memory.runtime.js");
return agentRunnerMemoryRuntimePromise;
}
function loadUsageCostRuntime() {
usageCostRuntimePromise ??= import("./usage-cost.runtime.js");
return usageCostRuntimePromise;
}
function loadContextTokensRuntime() {
contextTokensRuntimePromise ??= import("../../agents/context-tokens.runtime.js");
return contextTokensRuntimePromise;
}
function loadReplyMediaPathsRuntime() {
replyMediaPathsRuntimePromise ??= import("./reply-media-paths.runtime.js");
return replyMediaPathsRuntimePromise;
}
function loadSessionStoreRuntime() {
sessionStoreRuntimePromise ??= import("../../config/sessions/store.runtime.js");
return sessionStoreRuntimePromise;
@@ -202,7 +175,6 @@ export async function runReplyAgent(params: {
);
const applyReplyToMode = createReplyToModeFilterForChannel(replyToMode, replyToChannel);
const cfg = followupRun.run.config;
const { createReplyMediaPathNormalizer } = await loadReplyMediaPathsRuntime();
const normalizeReplyMediaPaths = createReplyMediaPathNormalizer({
cfg,
sessionKey,
@@ -274,7 +246,6 @@ export async function runReplyAgent(params: {
await typingSignals.signalRunStart();
const { runMemoryFlushIfNeeded } = await loadAgentRunnerMemoryRuntime();
activeSessionEntry = await runMemoryFlushIfNeeded({
cfg,
followupRun,
@@ -404,7 +375,6 @@ export async function runReplyAgent(params: {
});
try {
const runStartedAt = Date.now();
const { runAgentTurnWithFallback } = await loadAgentRunnerExecutionRuntime();
const runOutcome = await runAgentTurnWithFallback({
commandBody,
followupRun,
@@ -531,9 +501,7 @@ export async function runReplyAgent(params: {
const contextTokensUsed =
agentCfgContextTokens ??
cachedContextTokens ??
(await loadContextTokensRuntime()).lookupContextTokens(modelUsed, {
allowAsyncLoad: false,
}) ??
lookupContextTokens(modelUsed, { allowAsyncLoad: false }) ??
activeSessionEntry?.contextTokens ??
DEFAULT_CONTEXT_TOKENS;

View File

@@ -156,7 +156,7 @@ type RunPreparedReplyParams = {
sessionCfg: OpenClawConfig["session"];
commandAuthorized: boolean;
command: ReturnType<typeof buildCommandContext>;
commandSource: string;
commandSource?: string;
allowTextCommands: boolean;
directives: InlineDirectives;
defaultActivation: Parameters<typeof buildGroupIntro>[0]["defaultActivation"];
@@ -214,7 +214,6 @@ export async function runPreparedReply(
sessionCfg,
commandAuthorized,
command,
commandSource,
allowTextCommands,
directives,
defaultActivation,
@@ -300,11 +299,13 @@ export async function runPreparedReply(
// Use CommandBody/RawBody for bare reset detection (clean message without structural context).
const rawBodyTrimmed = (ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "").trim();
const baseBodyTrimmedRaw = baseBody.trim();
const isWholeMessageCommand = command.commandBodyNormalized.trim() === rawBodyTrimmed;
const isResetOrNewCommand = /^\/(new|reset)(?:\s|$)/.test(rawBodyTrimmed);
if (
allowTextCommands &&
(!commandAuthorized || !command.isAuthorizedSender) &&
!baseBodyTrimmedRaw &&
hasControlCommand(commandSource, cfg)
isWholeMessageCommand &&
(hasControlCommand(rawBodyTrimmed, cfg) || isResetOrNewCommand)
) {
typing.cleanup();
return undefined;

View File

@@ -3,6 +3,7 @@ import {
normalizeChannelId as normalizePluginChannelId,
} from "../../channels/plugins/index.js";
import type { ChannelId } from "../../channels/plugins/types.js";
import { resolveWhatsAppGroupIntroHint } from "../../channels/plugins/whatsapp-shared.js";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveChannelGroupRequireMention } from "../../config/group-policy.js";
import type { GroupKeyResolution, SessionEntry } from "../../config/sessions.js";
@@ -157,13 +158,13 @@ export function buildGroupIntro(params: {
params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim();
const groupSpace = params.sessionCtx.GroupSpace?.trim();
const providerIdsLine = providerId
? getChannelPlugin(providerId)?.groups?.resolveGroupIntroHint?.({
? (getChannelPlugin(providerId)?.groups?.resolveGroupIntroHint?.({
cfg: params.cfg,
groupId,
groupChannel,
groupSpace,
accountId: params.sessionCtx.AccountId,
})
}) ?? (providerId === "whatsapp" ? resolveWhatsAppGroupIntroHint() : undefined))
: undefined;
const silenceLine =
activation === "always"

View File

@@ -3,5 +3,6 @@ export {
buildProviderAuthDoctorHintWithPlugin,
buildProviderMissingAuthMessageWithPlugin,
formatProviderAuthProfileApiKeyWithPlugin,
prepareProviderRuntimeAuth,
refreshProviderOAuthCredentialWithPlugin,
} from "./provider-runtime.js";