From 7ba28d6dba8bb0fe9c6df3babe39aa6554a157ad Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Mar 2026 09:13:23 +0000 Subject: [PATCH] fix: repair runtime seams after rebase --- src/agents/model-auth.ts | 47 ++++++++------- ...pi-agent.auth-profile-rotation.e2e.test.ts | 48 ++++++--------- src/agents/pi-embedded-runner/compact.ts | 2 +- src/agents/pi-embedded-runner/model.ts | 24 ++++++-- src/agents/pi-embedded-runner/run.ts | 2 +- src/agents/subagent-announce.ts | 18 +++--- ...eply.triggers.group-intro-prompts.cases.ts | 4 +- ...ets-active-session-native-stop.e2e.test.ts | 4 ++ .../reply/agent-runner-execution.ts | 10 +--- .../agent-runner.runreplyagent.e2e.test.ts | 59 +++++++++++++------ src/auto-reply/reply/agent-runner.ts | 44 ++------------ src/auto-reply/reply/get-reply-run.ts | 9 +-- src/auto-reply/reply/groups.ts | 5 +- src/plugins/provider-runtime.runtime.ts | 1 + 14 files changed, 134 insertions(+), 143 deletions(-) diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index fed2d2319d3..c811b9b2999 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -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 - | 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); diff --git a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts b/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts index cbea9e5f21b..1a3a79cd19c 100644 --- a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts +++ b/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts @@ -11,6 +11,7 @@ import type { EmbeddedRunAttemptResult } from "./pi-embedded-runner/run/types.js const runEmbeddedAttemptMock = vi.fn<(params: unknown) => Promise>(); 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(); - 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 }); } diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index df6466cf17c..ccbeb87fbf6 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -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"; diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index 997c9088dfd..0b1253fa14a 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -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, + }), + }; + } const model = modelRegistry.find(provider, modelId) as Model | 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, + model: fallbackInlineMatch as Model, }), }; } diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 1356234a59d..85e01ad0f72 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -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"; diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index ab2fbb1140e..a99a0837e6d 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -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; diff --git a/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts b/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts index cdaf43d6b51..e077bcda204 100644 --- a/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts +++ b/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts @@ -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.`, ], }, { diff --git a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts index 855a8eca249..416f636cd90 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts @@ -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"), diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 2022928ebb9..f164559a796 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -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 | 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(); const runId = params.opts?.runId ?? crypto.randomUUID(); - const { createReplyMediaPathNormalizer } = await loadReplyMediaPathsRuntime(); const normalizeReplyMediaPaths = createReplyMediaPathNormalizer({ cfg: params.followupRun.run.config, sessionKey: params.sessionKey, diff --git a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts index be7d611153a..830a6af8779 100644 --- a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts +++ b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts @@ -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 () => { diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index bbf37681af2..d7ce18d1cf8 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -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 | null = null; -let contextTokensRuntimePromise: Promise< - typeof import("../../agents/context-tokens.runtime.js") -> | null = null; -let replyMediaPathsRuntimePromise: Promise | 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; diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index 4f7633131a9..70c9fd5281c 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -156,7 +156,7 @@ type RunPreparedReplyParams = { sessionCfg: OpenClawConfig["session"]; commandAuthorized: boolean; command: ReturnType; - commandSource: string; + commandSource?: string; allowTextCommands: boolean; directives: InlineDirectives; defaultActivation: Parameters[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; diff --git a/src/auto-reply/reply/groups.ts b/src/auto-reply/reply/groups.ts index acdbbe67faf..cc9226f0cf4 100644 --- a/src/auto-reply/reply/groups.ts +++ b/src/auto-reply/reply/groups.ts @@ -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" diff --git a/src/plugins/provider-runtime.runtime.ts b/src/plugins/provider-runtime.runtime.ts index d4f036e1cf8..005e5f63ec3 100644 --- a/src/plugins/provider-runtime.runtime.ts +++ b/src/plugins/provider-runtime.runtime.ts @@ -3,5 +3,6 @@ export { buildProviderAuthDoctorHintWithPlugin, buildProviderMissingAuthMessageWithPlugin, formatProviderAuthProfileApiKeyWithPlugin, + prepareProviderRuntimeAuth, refreshProviderOAuthCredentialWithPlugin, } from "./provider-runtime.js";