fix: align claude-cli prompt hooks (#70625)

Merged via squash.

Prepared head SHA: 3de89da38f
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-04-23 16:34:16 +02:00
committed by GitHub
parent 74bb617889
commit 3e956a4982
13 changed files with 554 additions and 9 deletions

View File

@@ -2,6 +2,14 @@
Docs: https://docs.openclaw.ai
## Unreleased
### Changes
### Fixes
- CLI/Claude: run the same prompt-build hooks and trigger/channel context on `claude-cli` turns as on direct embedded runs, keeping Claude Code sessions aligned with OpenClaw workspace identity, routing, and hook-driven prompt mutations. (#70625) Thanks @mbelinky.
## 2026.4.22
### Changes

View File

@@ -71,6 +71,24 @@ For long-lived gateway hosts, an Anthropic API key is still the most predictable
setup. If you want to reuse an existing Claude login on the same host, use the
Anthropic Claude CLI path in onboarding/configure.
Recommended host setup for Claude CLI reuse:
```bash
# Run on the gateway host
claude auth login
claude auth status --text
openclaw models auth login --provider anthropic --method cli --set-default
```
This is a two-step setup:
1. Log Claude Code itself into Anthropic on the gateway host.
2. Tell OpenClaw to switch Anthropic model selection to the local `claude-cli`
backend and store the matching OpenClaw auth profile.
If `claude` is not on `PATH`, either install Claude Code first or set
`agents.defaults.cliBackends.claude-cli.command` to the real binary path.
Manual token entry (any provider; writes `auth-profiles.json` + updates config):
```bash

View File

@@ -169,6 +169,18 @@ resolver sees the same filtered set that OpenClaw would otherwise advertise in
the prompt. Skill env/API key overrides are still applied by OpenClaw to the
child process environment for the run.
Before OpenClaw can use the bundled `claude-cli` backend, Claude Code itself
must already be logged in on the same host:
```bash
claude auth login
claude auth status --text
openclaw models auth login --provider anthropic --method cli --set-default
```
Use `agents.defaults.cliBackends.claude-cli.command` only when the `claude`
binary is not already on `PATH`.
## Sessions
- If the CLI supports sessions, set `sessionArg` (e.g. `--session-id`) or

View File

@@ -431,6 +431,22 @@ describe("runCliAgent spawn path", () => {
expect(params.senderIsOwner).toBe(false);
});
it("forwards channel context through the compat wrapper", () => {
const params = buildRunClaudeCliAgentParams({
sessionId: "openclaw-session",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
prompt: "hi",
timeoutMs: 1_000,
runId: "run-claude-channel-wrapper",
messageChannel: "telegram",
messageProvider: "acp",
});
expect(params.messageChannel).toBe("telegram");
expect(params.messageProvider).toBe("acp");
});
it("forwards static extra system prompt through the compat wrapper", () => {
const params = buildRunClaudeCliAgentParams({
sessionId: "openclaw-session",

View File

@@ -140,6 +140,7 @@ export function buildRunClaudeCliAgentParams(params: RunClaudeCliAgentParams): R
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: params.agentId,
trigger: params.trigger,
sessionFile: params.sessionFile,
workspaceDir: params.workspaceDir,
config: params.config,
@@ -156,6 +157,8 @@ export function buildRunClaudeCliAgentParams(params: RunClaudeCliAgentParams): R
// Ignore it here so the compatibility wrapper does not accidentally resume
// an incompatible Claude session on the generic runner path.
images: params.images,
messageChannel: params.messageChannel,
messageProvider: params.messageProvider,
senderIsOwner: params.senderIsOwner,
};
}

View File

@@ -1,7 +1,114 @@
import { describe, expect, it } from "vitest";
import { shouldSkipLocalCliCredentialEpoch } from "./prepare.js";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { buildActiveMusicGenerationTaskPromptContextForSession } from "../music-generation-task-status.js";
import { buildActiveVideoGenerationTaskPromptContextForSession } from "../video-generation-task-status.js";
import {
prepareCliRunContext,
setCliRunnerPrepareTestDeps,
shouldSkipLocalCliCredentialEpoch,
} from "./prepare.js";
vi.mock("../../plugins/hook-runner-global.js", async () => {
const actual = await vi.importActual<typeof import("../../plugins/hook-runner-global.js")>(
"../../plugins/hook-runner-global.js",
);
return {
...actual,
getGlobalHookRunner: vi.fn(() => null),
};
});
vi.mock("../video-generation-task-status.js", async () => {
const actual = await vi.importActual<typeof import("../video-generation-task-status.js")>(
"../video-generation-task-status.js",
);
return {
...actual,
buildActiveVideoGenerationTaskPromptContextForSession: vi.fn(() => undefined),
};
});
vi.mock("../music-generation-task-status.js", async () => {
const actual = await vi.importActual<typeof import("../music-generation-task-status.js")>(
"../music-generation-task-status.js",
);
return {
...actual,
buildActiveMusicGenerationTaskPromptContextForSession: vi.fn(() => undefined),
};
});
const mockGetGlobalHookRunner = vi.mocked(getGlobalHookRunner);
const mockBuildActiveVideoGenerationTaskPromptContextForSession = vi.mocked(
buildActiveVideoGenerationTaskPromptContextForSession,
);
const mockBuildActiveMusicGenerationTaskPromptContextForSession = vi.mocked(
buildActiveMusicGenerationTaskPromptContextForSession,
);
function createCliBackendConfig(): OpenClawConfig {
return {
agents: {
defaults: {
cliBackends: {
"test-cli": {
command: "test-cli",
args: ["--print"],
systemPromptArg: "--system-prompt",
systemPromptWhen: "first",
sessionMode: "existing",
output: "text",
input: "arg",
},
},
},
},
} satisfies OpenClawConfig;
}
function createSessionFile() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cli-prepare-"));
const sessionFile = path.join(dir, "session.jsonl");
fs.writeFileSync(
sessionFile,
`${JSON.stringify({
type: "session",
version: CURRENT_SESSION_VERSION,
id: "session-test",
timestamp: new Date(0).toISOString(),
cwd: dir,
})}\n`,
"utf-8",
);
return { dir, sessionFile };
}
describe("shouldSkipLocalCliCredentialEpoch", () => {
beforeEach(() => {
setCliRunnerPrepareTestDeps({
makeBootstrapWarn: vi.fn(() => () => undefined),
resolveBootstrapContextForRun: vi.fn(async () => ({
bootstrapFiles: [],
contextFiles: [],
})),
resolveOpenClawDocsPath: vi.fn(async () => null),
});
mockGetGlobalHookRunner.mockReturnValue(null);
mockBuildActiveVideoGenerationTaskPromptContextForSession.mockReturnValue(undefined);
mockBuildActiveMusicGenerationTaskPromptContextForSession.mockReturnValue(undefined);
});
afterEach(() => {
mockGetGlobalHookRunner.mockReset();
mockBuildActiveVideoGenerationTaskPromptContextForSession.mockReset();
mockBuildActiveMusicGenerationTaskPromptContextForSession.mockReset();
});
it("skips local cli auth only when a profile-owned execution was prepared", () => {
expect(
shouldSkipLocalCliCredentialEpoch({
@@ -33,4 +140,217 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
}),
).toBe(false);
});
it("applies prompt-build hook context to Claude-style CLI preparation", async () => {
const { dir, sessionFile } = createSessionFile();
try {
const sessionManager = SessionManager.open(sessionFile);
sessionManager.appendMessage({ role: "user", content: "earlier context", timestamp: 1 });
sessionManager.appendMessage({
role: "assistant",
content: [{ type: "text", text: "earlier reply" }],
api: "responses",
provider: "test-cli",
model: "test-model",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: 2,
});
const hookRunner = {
hasHooks: vi.fn((hookName: string) => hookName === "before_prompt_build"),
runBeforePromptBuild: vi.fn(async ({ messages }: { messages: unknown[] }) => ({
prependContext: `history:${messages.length}`,
systemPrompt: "hook system",
prependSystemContext: "prepend system",
appendSystemContext: "append system",
})),
runBeforeAgentStart: vi.fn(),
};
mockGetGlobalHookRunner.mockReturnValue(hookRunner as never);
const context = await prepareCliRunContext({
sessionId: "session-test",
sessionKey: "agent:main:test",
agentId: "main",
trigger: "user",
sessionFile,
workspaceDir: dir,
prompt: "latest ask",
provider: "test-cli",
model: "test-model",
timeoutMs: 1_000,
runId: "run-test",
messageChannel: "telegram",
messageProvider: "acp",
config: {
...createCliBackendConfig(),
},
});
expect(context.params.prompt).toBe("history:2\n\nlatest ask");
expect(context.systemPrompt).toBe("prepend system\n\nhook system\n\nappend system");
expect(hookRunner.runBeforePromptBuild).toHaveBeenCalledWith(
{
prompt: "latest ask",
messages: [
{ role: "user", content: "earlier context", timestamp: 1 },
{
role: "assistant",
content: [{ type: "text", text: "earlier reply" }],
api: "responses",
provider: "test-cli",
model: "test-model",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: 2,
},
],
},
expect.objectContaining({
runId: "run-test",
agentId: "main",
sessionKey: "agent:main:test",
sessionId: "session-test",
workspaceDir: dir,
modelProviderId: "test-cli",
modelId: "test-model",
messageProvider: "acp",
trigger: "user",
channelId: "telegram",
}),
);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it("merges before_prompt_build and legacy before_agent_start hook context for CLI preparation", async () => {
const { dir, sessionFile } = createSessionFile();
try {
const hookRunner = {
hasHooks: vi.fn((_hookName: string) => true),
runBeforePromptBuild: vi.fn(async () => ({
prependContext: "prompt prepend",
systemPrompt: "prompt system",
prependSystemContext: "prompt prepend system",
appendSystemContext: "prompt append system",
})),
runBeforeAgentStart: vi.fn(async () => ({
prependContext: "legacy prepend",
systemPrompt: "legacy system",
prependSystemContext: "legacy prepend system",
appendSystemContext: "legacy append system",
})),
};
mockGetGlobalHookRunner.mockReturnValue(hookRunner as never);
const context = await prepareCliRunContext({
sessionId: "session-test",
sessionFile,
workspaceDir: dir,
prompt: "latest ask",
provider: "test-cli",
model: "test-model",
timeoutMs: 1_000,
runId: "run-test-legacy-merge",
config: createCliBackendConfig(),
});
expect(context.params.prompt).toBe("prompt prepend\n\nlegacy prepend\n\nlatest ask");
expect(context.systemPrompt).toBe(
"prompt prepend system\n\nlegacy prepend system\n\nprompt system\n\nprompt append system\n\nlegacy append system",
);
expect(hookRunner.runBeforePromptBuild).toHaveBeenCalledOnce();
expect(hookRunner.runBeforeAgentStart).toHaveBeenCalledOnce();
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it("preserves the base prompt when prompt-build hooks fail", async () => {
const { dir, sessionFile } = createSessionFile();
try {
const hookRunner = {
hasHooks: vi.fn((hookName: string) => hookName === "before_prompt_build"),
runBeforePromptBuild: vi.fn(async () => {
throw new Error("hook exploded");
}),
runBeforeAgentStart: vi.fn(),
};
mockGetGlobalHookRunner.mockReturnValue(hookRunner as never);
const context = await prepareCliRunContext({
sessionId: "session-test",
sessionFile,
workspaceDir: dir,
prompt: "latest ask",
provider: "test-cli",
model: "test-model",
timeoutMs: 1_000,
runId: "run-test-hook-failure",
extraSystemPrompt: "base extra system",
config: createCliBackendConfig(),
});
expect(context.params.prompt).toBe("latest ask");
expect(context.systemPrompt).toContain("base extra system");
expect(context.systemPrompt).not.toContain("hook exploded");
expect(hookRunner.runBeforePromptBuild).toHaveBeenCalledOnce();
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it("applies direct-run prepend system context helpers on the CLI path", async () => {
const { dir, sessionFile } = createSessionFile();
try {
mockBuildActiveVideoGenerationTaskPromptContextForSession.mockReturnValue(
"active video task",
);
const hookRunner = {
hasHooks: vi.fn((hookName: string) => hookName === "before_prompt_build"),
runBeforePromptBuild: vi.fn(async () => ({
systemPrompt: "hook system",
prependSystemContext: "hook prepend system",
})),
runBeforeAgentStart: vi.fn(),
};
mockGetGlobalHookRunner.mockReturnValue(hookRunner as never);
const context = await prepareCliRunContext({
sessionId: "session-test",
sessionKey: "agent:main:test",
trigger: "user",
sessionFile,
workspaceDir: dir,
prompt: "latest ask",
provider: "test-cli",
model: "test-model",
timeoutMs: 1_000,
runId: "run-test-prepend-helper",
config: createCliBackendConfig(),
});
expect(context.systemPrompt).toBe("active video task\n\nhook prepend system\n\nhook system");
expect(mockBuildActiveVideoGenerationTaskPromptContextForSession).toHaveBeenCalledWith(
"agent:main:test",
);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
});

View File

@@ -1,3 +1,4 @@
import { SessionManager } from "@mariozechner/pi-coding-agent";
import { ensureMcpLoopbackServer } from "../../gateway/mcp-http.js";
import {
createMcpLoopbackServerConfig,
@@ -7,6 +8,7 @@ import type {
CliBackendAuthEpochMode,
CliBackendPreparedExecution,
} from "../../plugins/cli-backend.types.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { resolveOpenClawAgentDir } from "../agent-paths.js";
import { resolveSessionAgentIds } from "../agent-scope.js";
import { loadAuthProfileStoreForRuntime } from "../auth-profiles/store.js";
@@ -30,6 +32,9 @@ import {
resolveBootstrapPromptTruncationWarningMode,
resolveBootstrapTotalMaxChars,
} from "../pi-embedded-helpers.js";
import { resolvePromptBuildHookResult } from "../pi-embedded-runner/run/attempt.prompt-helpers.js";
import { resolveAttemptPrependSystemContext } from "../pi-embedded-runner/run/attempt.prompt-helpers.js";
import { composeSystemPromptWithHookContext } from "../pi-embedded-runner/run/attempt.thread-helpers.js";
import { applyPluginTextReplacements } from "../plugin-text-transforms.js";
import { resolveSkillsPromptForRun } from "../skills.js";
import { resolveSystemPromptOverride } from "../system-prompt-override.js";
@@ -51,6 +56,11 @@ const prepareDeps = {
) => (await import("../docs-path.js")).resolveOpenClawDocsPath(params),
};
function loadCliPromptBuildMessages(sessionFile: string): unknown[] {
const entries = SessionManager.open(sessionFile).getEntries();
return entries.flatMap((entry) => (entry.type === "message" ? [entry.message as unknown] : []));
}
export function setCliRunnerPrepareTestDeps(overrides: Partial<typeof prepareDeps>): void {
Object.assign(prepareDeps, overrides);
}
@@ -181,7 +191,7 @@ export async function prepareCliRunContext(
OPENCLAW_MCP_AGENT_ID: sessionAgentId ?? "",
OPENCLAW_MCP_ACCOUNT_ID: params.agentAccountId ?? "",
OPENCLAW_MCP_SESSION_KEY: params.sessionKey ?? "",
OPENCLAW_MCP_MESSAGE_CHANNEL: params.messageProvider ?? "",
OPENCLAW_MCP_MESSAGE_CHANNEL: params.messageChannel ?? params.messageProvider ?? "",
}
: undefined,
warn: (message) => cliBackendLog.warn(message),
@@ -298,10 +308,50 @@ export async function prepareCliRunContext(
agentId: sessionAgentId,
systemPrompt: builtSystemPrompt,
}) ?? builtSystemPrompt;
const systemPrompt = applyPluginTextReplacements(
transformedSystemPrompt,
backendResolved.textTransforms?.input,
);
let systemPrompt = transformedSystemPrompt;
let preparedPrompt = params.prompt;
const hookRunner = getGlobalHookRunner();
if (hookRunner?.hasHooks("before_prompt_build") || hookRunner?.hasHooks("before_agent_start")) {
try {
const hookResult = await resolvePromptBuildHookResult({
prompt: params.prompt,
messages: loadCliPromptBuildMessages(params.sessionFile),
hookCtx: {
runId: params.runId,
agentId: sessionAgentId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
workspaceDir,
modelProviderId: params.provider,
modelId,
messageProvider: params.messageProvider,
trigger: params.trigger,
channelId: params.messageChannel ?? params.messageProvider,
},
hookRunner,
});
if (hookResult.prependContext) {
preparedPrompt = `${hookResult.prependContext}\n\n${preparedPrompt}`;
}
const hookSystemPrompt = hookResult.systemPrompt?.trim();
if (hookSystemPrompt) {
systemPrompt = hookSystemPrompt;
}
systemPrompt =
composeSystemPromptWithHookContext({
baseSystemPrompt: systemPrompt,
prependSystemContext: resolveAttemptPrependSystemContext({
sessionKey: params.sessionKey,
trigger: params.trigger,
hookPrependSystemContext: hookResult.prependSystemContext,
}),
appendSystemContext: hookResult.appendSystemContext,
}) ?? systemPrompt;
} catch (error) {
cliBackendLog.warn(`cli prompt-build hook preparation failed: ${String(error)}`);
}
}
systemPrompt = applyPluginTextReplacements(systemPrompt, backendResolved.textTransforms?.input);
const systemPromptReport = buildSystemPromptReport({
source: "run",
generatedAt: Date.now(),
@@ -326,7 +376,7 @@ export async function prepareCliRunContext(
});
return {
params,
params: preparedPrompt === params.prompt ? params : { ...params, prompt: preparedPrompt },
effectiveAuthProfileId,
started,
workspaceDir,

View File

@@ -7,12 +7,14 @@ import type { CliBackendConfig } from "../../config/types.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { PromptImageOrderEntry } from "../../media/prompt-image-order.js";
import type { ResolvedCliBackend } from "../cli-backends.js";
import type { EmbeddedRunTrigger } from "../pi-embedded-runner/run/params.js";
import type { SkillSnapshot } from "../skills.js";
export type RunCliAgentParams = {
sessionId: string;
sessionKey?: string;
agentId?: string;
trigger?: EmbeddedRunTrigger;
sessionFile: string;
workspaceDir: string;
config?: OpenClawConfig;
@@ -35,6 +37,7 @@ export type RunCliAgentParams = {
images?: ImageContent[];
imageOrder?: PromptImageOrderEntry[];
skillsSnapshot?: SkillSnapshot;
messageChannel?: string;
messageProvider?: string;
agentAccountId?: string;
senderIsOwner?: boolean;

View File

@@ -336,4 +336,53 @@ describe("CLI attempt execution", () => {
content: [{ type: "text", text: "hello from cli" }],
});
});
it("forwards user trigger and channel context to CLI runs", async () => {
const sessionKey = "agent:main:direct:claude-channel-context";
const sessionEntry: SessionEntry = {
sessionId: "openclaw-session-channel",
updatedAt: Date.now(),
};
const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
runCliAgentMock.mockResolvedValueOnce(makeCliResult("channel aware"));
await runAgentAttempt({
providerOverride: "claude-cli",
modelOverride: "opus",
cfg: {} as OpenClawConfig,
sessionEntry,
sessionId: sessionEntry.sessionId,
sessionKey,
sessionAgentId: "main",
sessionFile: path.join(tmpDir, "session.jsonl"),
workspaceDir: tmpDir,
body: "route this",
isFallbackRetry: false,
resolvedThinkLevel: "medium",
timeoutMs: 1_000,
runId: "run-cli-channel-context",
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
spawnedBy: undefined,
messageChannel: "telegram",
skillsSnapshot: undefined,
resolvedVerboseLevel: undefined,
agentDir: tmpDir,
onAgentEvent: vi.fn(),
authProfileProvider: "claude-cli",
sessionStore,
storePath,
sessionHasHistory: false,
});
expect(runCliAgentMock).toHaveBeenCalledTimes(1);
expect(runCliAgentMock).toHaveBeenCalledWith(
expect.objectContaining({
trigger: "user",
messageChannel: "telegram",
messageProvider: "telegram",
}),
);
});
});

View File

@@ -301,6 +301,7 @@ export function runAgentAttempt(params: {
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: params.sessionAgentId,
trigger: "user",
sessionFile: params.sessionFile,
workspaceDir: params.workspaceDir,
config: params.cfg,
@@ -322,6 +323,7 @@ export function runAgentAttempt(params: {
images: params.isFallbackRetry ? undefined : params.opts.images,
imageOrder: params.isFallbackRetry ? undefined : params.opts.imageOrder,
skillsSnapshot: params.skillsSnapshot,
messageChannel: params.messageChannel,
streamParams: params.opts.streamParams,
messageProvider: params.messageChannel,
agentAccountId: params.runContext.accountId,

View File

@@ -274,6 +274,7 @@ describe("runAgentTurnWithFallback", () => {
followupRun.run.model = "gpt-5.4";
followupRun.run.extraSystemPrompt = "dynamic inbound metadata\n\nstable group prompt";
followupRun.run.extraSystemPromptStatic = "stable group prompt";
followupRun.originatingChannel = "telegram";
const result = await runAgentTurnWithFallback({
commandBody: "hello",
@@ -304,6 +305,60 @@ describe("runAgentTurnWithFallback", () => {
expect.objectContaining({
extraSystemPrompt: "dynamic inbound metadata\n\nstable group prompt",
extraSystemPromptStatic: "stable group prompt",
trigger: "user",
messageChannel: "telegram",
messageProvider: "telegram",
}),
);
});
it("resolves CLI messageProvider from the live session surface when no origin channel is set", async () => {
state.isCliProviderMock.mockReturnValue(true);
state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
result: await params.run("codex-cli", "gpt-5.4"),
provider: "codex-cli",
model: "gpt-5.4",
attempts: [],
}));
state.runCliAgentMock.mockResolvedValueOnce({
payloads: [{ text: "final" }],
meta: {},
});
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
const followupRun = createFollowupRun();
followupRun.run.provider = "codex-cli";
followupRun.run.model = "gpt-5.4";
followupRun.run.messageProvider = "stale-provider";
await runAgentTurnWithFallback({
commandBody: "hello",
followupRun,
sessionCtx: {
Provider: "discord",
MessageSid: "msg",
} as unknown as TemplateContext,
opts: {},
typingSignals: createMockTypingSignaler(),
blockReplyPipeline: null,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
applyReplyToMode: (payload) => payload,
shouldEmitToolResult: () => true,
shouldEmitToolOutput: () => false,
pendingToolTasks: new Set(),
resetSessionAfterCompactionFailure: async () => false,
resetSessionAfterRoleOrderingConflict: async () => false,
isHeartbeat: false,
sessionKey: "main",
getActiveSessionEntry: () => undefined,
resolvedVerboseLevel: "off",
});
expect(state.runCliAgentMock).toHaveBeenCalledWith(
expect.objectContaining({
messageChannel: undefined,
messageProvider: "discord",
}),
);
});

View File

@@ -69,6 +69,7 @@ import {
resolveModelFallbackOptions,
} from "./agent-runner-utils.js";
import { type BlockReplyPipeline } from "./block-reply-pipeline.js";
import { resolveOriginMessageProvider } from "./origin-routing.js";
import type { FollowupRun } from "./queue.js";
import { createBlockReplyDeliveryHandler } from "./reply-delivery.js";
import type { ReplyMediaContext } from "./reply-media-paths.js";
@@ -896,6 +897,10 @@ export async function runAgentTurnWithFallback(params: {
provider === params.followupRun.run.provider
? params.followupRun.run.authProfileId
: undefined;
const hookMessageProvider = resolveOriginMessageProvider({
originatingChannel: params.followupRun.originatingChannel,
provider: params.sessionCtx.Provider,
});
return (async () => {
let lifecycleTerminalEmitted = false;
try {
@@ -903,6 +908,7 @@ export async function runAgentTurnWithFallback(params: {
sessionId: params.followupRun.run.sessionId,
sessionKey: params.sessionKey,
agentId: params.followupRun.run.agentId,
trigger: params.isHeartbeat ? "heartbeat" : "user",
sessionFile: params.followupRun.run.sessionFile,
workspaceDir: params.followupRun.run.workspaceDir,
config: runtimeConfig,
@@ -926,7 +932,8 @@ export async function runAgentTurnWithFallback(params: {
images: params.opts?.images,
imageOrder: params.opts?.imageOrder,
skillsSnapshot: params.followupRun.run.skillsSnapshot,
messageProvider: params.followupRun.run.messageProvider,
messageChannel: params.followupRun.originatingChannel ?? undefined,
messageProvider: hookMessageProvider,
agentAccountId: params.followupRun.run.agentAccountId,
senderIsOwner: params.followupRun.run.senderIsOwner,
abortSignal: params.replyOperation?.abortSignal ?? params.opts?.abortSignal,

View File

@@ -128,6 +128,7 @@ export function createCronPromptExecutor(params: {
sessionId: params.cronSession.sessionEntry.sessionId,
sessionKey: params.agentSessionKey,
agentId: params.agentId,
trigger: "cron",
sessionFile,
workspaceDir: params.workspaceDir,
config: params.cfgWithAgentDefaults,
@@ -139,6 +140,7 @@ export function createCronPromptExecutor(params: {
runId: params.cronSession.sessionEntry.sessionId,
cliSessionId,
skillsSnapshot: params.skillsSnapshot,
messageChannel: params.messageChannel,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature,
senderIsOwner: true,