From 4b10bac423a34bae1754f33d28207ebe396125b3 Mon Sep 17 00:00:00 2001 From: Alexander Davydov Date: Tue, 17 Mar 2026 14:52:21 +0300 Subject: [PATCH] profile id fix --- src/agents/pi-embedded-runner/run/attempt.ts | 7 +- .../reply/agent-runner-utils.test.ts | 22 +++++ src/auto-reply/reply/agent-runner-utils.ts | 55 +++++++++++ .../agent-runner.misc.runreplyagent.test.ts | 91 +++++++++++++++++++ src/auto-reply/reply/agent-runner.ts | 8 +- src/auto-reply/reply/followup-runner.ts | 3 +- 6 files changed, 178 insertions(+), 8 deletions(-) diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 081b902d121..98448172b3f 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -230,10 +230,7 @@ export function resolveGigachatAuthProfileMetadata( ); for (const profileId of profileIds) { const credential = store.profiles[profileId]; - if ( - credential?.type === "api_key" && - (credential as ApiKeyCredential).provider === "gigachat" - ) { + if (credential?.type === "api_key" && credential.provider === "gigachat") { return credential.metadata; } } @@ -1938,7 +1935,7 @@ export async function runEmbeddedAttempt( const gigachatStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); const gigachatMeta = resolveGigachatAuthProfileMetadata( gigachatStore, - params.attempt.authProfileId, + params.authProfileId, ); const gigachatStreamFn = createGigachatStreamFn({ diff --git a/src/auto-reply/reply/agent-runner-utils.test.ts b/src/auto-reply/reply/agent-runner-utils.test.ts index 5bf77cd9f70..7d6de5a96ee 100644 --- a/src/auto-reply/reply/agent-runner-utils.test.ts +++ b/src/auto-reply/reply/agent-runner-utils.test.ts @@ -15,6 +15,7 @@ const { buildThreadingToolContext, buildEmbeddedRunBaseParams, buildEmbeddedRunContexts, + normalizeFollowupRun, resolveModelFallbackOptions, resolveProviderScopedAuthProfile, } = await import("./agent-runner-utils.js"); @@ -157,6 +158,27 @@ describe("agent-runner-utils", () => { }); }); + it("normalizes legacy flattened followup runs", () => { + const run = makeRun({ + authProfileId: "profile-openai", + authProfileIdSource: "auto", + }); + + const normalized = normalizeFollowupRun({ + prompt: "hello", + enqueuedAt: Date.now(), + ...run, + } as unknown as FollowupRun); + + expect(normalized.run).toMatchObject({ + sessionId: run.sessionId, + provider: run.provider, + model: run.model, + authProfileId: "profile-openai", + authProfileIdSource: "auto", + }); + }); + it("prefers OriginatingChannel over Provider for messageProvider", () => { const run = makeRun(); diff --git a/src/auto-reply/reply/agent-runner-utils.ts b/src/auto-reply/reply/agent-runner-utils.ts index abf6322a287..1b296fef99a 100644 --- a/src/auto-reply/reply/agent-runner-utils.ts +++ b/src/auto-reply/reply/agent-runner-utils.ts @@ -154,6 +154,61 @@ export const appendUsageLine = (payloads: ReplyPayload[], line: string): ReplyPa export const resolveEnforceFinalTag = (run: FollowupRun["run"], provider: string) => Boolean(run.enforceFinalTag || isReasoningTagProvider(provider)); +function isFollowupRunObject(value: unknown): value is FollowupRun["run"] { + return Boolean(value && typeof value === "object"); +} + +/** + * Accept both the current nested followup shape (`{ run: {...} }`) and the + * older flattened form where run fields lived at the top level. + */ +export function normalizeFollowupRun(followupRun: T): T { + if (isFollowupRunObject(followupRun.run)) { + return followupRun; + } + + const legacyFollowupRun = followupRun as T & Partial; + return { + ...followupRun, + run: { + agentId: legacyFollowupRun.agentId, + agentDir: legacyFollowupRun.agentDir, + sessionId: legacyFollowupRun.sessionId, + sessionKey: legacyFollowupRun.sessionKey, + messageProvider: legacyFollowupRun.messageProvider, + agentAccountId: legacyFollowupRun.agentAccountId, + groupId: legacyFollowupRun.groupId, + groupChannel: legacyFollowupRun.groupChannel, + groupSpace: legacyFollowupRun.groupSpace, + senderId: legacyFollowupRun.senderId, + senderName: legacyFollowupRun.senderName, + senderUsername: legacyFollowupRun.senderUsername, + senderE164: legacyFollowupRun.senderE164, + senderIsOwner: legacyFollowupRun.senderIsOwner, + sessionFile: legacyFollowupRun.sessionFile, + workspaceDir: legacyFollowupRun.workspaceDir, + config: legacyFollowupRun.config, + skillsSnapshot: legacyFollowupRun.skillsSnapshot, + provider: legacyFollowupRun.provider, + model: legacyFollowupRun.model, + authProfileId: legacyFollowupRun.authProfileId, + authProfileIdSource: legacyFollowupRun.authProfileIdSource, + thinkLevel: legacyFollowupRun.thinkLevel, + verboseLevel: legacyFollowupRun.verboseLevel, + reasoningLevel: legacyFollowupRun.reasoningLevel, + elevatedLevel: legacyFollowupRun.elevatedLevel, + execOverrides: legacyFollowupRun.execOverrides, + bashElevated: legacyFollowupRun.bashElevated, + timeoutMs: legacyFollowupRun.timeoutMs, + blockReplyBreak: legacyFollowupRun.blockReplyBreak, + ownerNumbers: legacyFollowupRun.ownerNumbers, + inputProvenance: legacyFollowupRun.inputProvenance, + extraSystemPrompt: legacyFollowupRun.extraSystemPrompt, + enforceFinalTag: legacyFollowupRun.enforceFinalTag, + } as FollowupRun["run"], + }; +} + export function resolveModelFallbackOptions(run: FollowupRun["run"]) { return { cfg: run.config, diff --git a/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts b/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts index 90535e69fb9..3f65686d400 100644 --- a/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts +++ b/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts @@ -314,6 +314,97 @@ describe("runReplyAgent authProfileId fallback scoping", () => { expect(call.authProfileId).toBeUndefined(); expect(call.authProfileIdSource).toBeUndefined(); }); + + it("accepts legacy flattened followup runs", async () => { + runWithModelFallbackMock.mockImplementationOnce( + async ({ run }: RunWithModelFallbackParams) => ({ + result: await run("anthropic", "claude-opus"), + provider: "anthropic", + model: "claude-opus", + }), + ); + runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: {} }); + + const typing = createMockTypingController(); + const sessionCtx = { + Provider: "telegram", + OriginatingTo: "chat", + AccountId: "primary", + MessageSid: "msg", + Surface: "telegram", + } as unknown as TemplateContext; + + const sessionKey = "main"; + const sessionEntry = { + sessionId: "session", + updatedAt: Date.now(), + totalTokens: 1, + compactionCount: 0, + }; + + await runReplyAgent({ + commandBody: "hello", + followupRun: { + prompt: "hello", + summaryLine: "hello", + enqueuedAt: Date.now(), + agentId: "main", + agentDir: "/tmp/agent", + sessionId: "session", + sessionKey, + messageProvider: "telegram", + sessionFile: "/tmp/session.jsonl", + workspaceDir: "/tmp", + config: {}, + skillsSnapshot: {}, + provider: "anthropic", + model: "claude-opus", + authProfileId: "anthropic:openclaw", + authProfileIdSource: "user", + thinkLevel: "low", + verboseLevel: "off", + elevatedLevel: "off", + bashElevated: { + enabled: false, + allowed: false, + defaultLevel: "off", + }, + timeoutMs: 5_000, + blockReplyBreak: "message_end", + } as unknown as FollowupRun, + queueKey: sessionKey, + resolvedQueue: { mode: "interrupt" } as unknown as QueueSettings, + shouldSteer: false, + shouldFollowup: false, + isActive: false, + isStreaming: false, + typing, + sessionCtx, + sessionEntry, + sessionStore: { [sessionKey]: sessionEntry }, + sessionKey, + storePath: undefined, + defaultModel: "anthropic/claude-opus-4-5", + agentCfgContextTokens: 100_000, + resolvedVerboseLevel: "off", + isNewSession: false, + blockStreamingEnabled: false, + resolvedBlockStreamingBreak: "message_end", + shouldInjectGroupIntro: false, + typingMode: "instant", + }); + + expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as { + authProfileId?: unknown; + sessionId?: unknown; + provider?: unknown; + }; + + expect(call.sessionId).toBe("session"); + expect(call.provider).toBe("anthropic"); + expect(call.authProfileId).toBe("anthropic:openclaw"); + }); }); describe("runReplyAgent auto-compaction token update", () => { diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index 76d86c45b05..e0ce19ac5a7 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -44,7 +44,11 @@ import { hasSessionRelatedCronJobs, hasUnbackedReminderCommitment, } from "./agent-runner-reminder-guard.js"; -import { appendUsageLine, formatResponseUsageLine } from "./agent-runner-utils.js"; +import { + appendUsageLine, + formatResponseUsageLine, + normalizeFollowupRun, +} from "./agent-runner-utils.js"; import { createAudioAsVoiceBuffer, createBlockReplyPipeline } from "./block-reply-pipeline.js"; import { resolveEffectiveBlockStreamingConfig } from "./block-streaming.js"; import { createFollowupRunner } from "./followup-runner.js"; @@ -91,9 +95,9 @@ export async function runReplyAgent(params: { shouldInjectGroupIntro: boolean; typingMode: TypingMode; }): Promise { + const followupRun = normalizeFollowupRun(params.followupRun); const { commandBody, - followupRun, queueKey, resolvedQueue, shouldSteer, diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index fe90d56433c..d116f7d83d6 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -15,7 +15,7 @@ import { stripHeartbeatToken } from "../heartbeat.js"; import type { OriginatingChannelType } from "../templating.js"; import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; -import { resolveRunAuthProfile } from "./agent-runner-utils.js"; +import { normalizeFollowupRun, resolveRunAuthProfile } from "./agent-runner-utils.js"; import { resolveOriginAccountId, resolveOriginMessageProvider, @@ -131,6 +131,7 @@ export function createFollowupRunner(params: { return async (queued: FollowupRun) => { try { + queued = normalizeFollowupRun(queued); const runId = crypto.randomUUID(); const shouldSurfaceToControlUi = isInternalMessageChannel( resolveOriginMessageProvider({