fix(agent): forward resolved outbound session context for delivery

This commit is contained in:
Peter Steinberger
2026-02-26 22:14:11 +01:00
parent da9f24dd2e
commit 712e231725
4 changed files with 69 additions and 8 deletions

View File

@@ -48,6 +48,7 @@ describe("deliverAgentCommandResult", () => {
async function runDelivery(params: {
opts: Record<string, unknown>;
outboundSession?: { key?: string; agentId?: string };
sessionEntry?: SessionEntry;
runtime?: RuntimeEnv;
resultText?: string;
@@ -62,6 +63,7 @@ describe("deliverAgentCommandResult", () => {
deps,
runtime,
opts: params.opts as never,
outboundSession: params.outboundSession,
sessionEntry: params.sessionEntry,
result,
payloads: result.payloads,
@@ -234,6 +236,30 @@ describe("deliverAgentCommandResult", () => {
);
});
it("uses caller-provided outbound session context when opts.sessionKey is absent", async () => {
await runDelivery({
opts: {
message: "hello",
deliver: true,
channel: "whatsapp",
to: "+15551234567",
},
outboundSession: {
key: "agent:exec:hook:gmail:thread-1",
agentId: "exec",
},
});
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
expect.objectContaining({
session: expect.objectContaining({
key: "agent:exec:hook:gmail:thread-1",
agentId: "exec",
}),
}),
);
});
it("prefixes nested agent outputs with context", async () => {
const runtime = createRuntime();
await runDelivery({

View File

@@ -14,6 +14,7 @@ import { setActivePluginRegistry } from "../plugins/runtime.js";
import type { RuntimeEnv } from "../runtime.js";
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
import { agentCommand } from "./agent.js";
import * as agentDeliveryModule from "./agent/delivery.js";
vi.mock("../agents/auth-profiles.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../agents/auth-profiles.js")>();
@@ -49,6 +50,7 @@ const runtime: RuntimeEnv = {
const configSpy = vi.spyOn(configModule, "loadConfig");
const runCliAgentSpy = vi.spyOn(cliRunnerModule, "runCliAgent");
const deliverAgentCommandResultSpy = vi.spyOn(agentDeliveryModule, "deliverAgentCommandResult");
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
return withTempHomeBase(fn, { prefix: "openclaw-agent-" });
@@ -230,6 +232,35 @@ describe("agentCommand", () => {
});
});
it("forwards resolved outbound session context when resuming by sessionId", async () => {
await withTempHome(async (home) => {
const storePattern = path.join(home, "sessions", "{agentId}", "sessions.json");
const execStore = path.join(home, "sessions", "exec", "sessions.json");
writeSessionStoreSeed(execStore, {
"agent:exec:hook:gmail:thread-1": {
sessionId: "session-exec-hook",
updatedAt: Date.now(),
systemSent: true,
},
});
mockConfig(home, storePattern, undefined, undefined, [
{ id: "dev" },
{ id: "exec", default: true },
]);
await agentCommand({ message: "resume me", sessionId: "session-exec-hook" }, runtime);
const deliverCall = deliverAgentCommandResultSpy.mock.calls.at(-1)?.[0];
expect(deliverCall?.opts.sessionKey).toBeUndefined();
expect(deliverCall?.outboundSession).toEqual(
expect.objectContaining({
key: "agent:exec:hook:gmail:thread-1",
agentId: "exec",
}),
);
});
});
it("resolves resumed session transcript path from custom session store directory", async () => {
await withTempHome(async (home) => {
const customStoreDir = path.join(home, "custom-state");

View File

@@ -59,6 +59,7 @@ import {
emitAgentEvent,
registerAgentRunContext,
} from "../infra/agent-events.js";
import { buildOutboundSessionContext } from "../infra/outbound/session-context.js";
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
@@ -316,6 +317,11 @@ export async function agentCommand(
sessionKey: sessionKey ?? opts.sessionKey?.trim(),
config: cfg,
});
const outboundSession = buildOutboundSessionContext({
cfg,
agentId: sessionAgentId,
sessionKey,
});
const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, sessionAgentId);
const agentDir = resolveAgentDir(cfg, sessionAgentId);
const workspace = await ensureAgentWorkspace({
@@ -461,6 +467,7 @@ export async function agentCommand(
deps,
runtime,
opts,
outboundSession,
sessionEntry,
result,
payloads,
@@ -809,6 +816,7 @@ export async function agentCommand(
deps,
runtime,
opts,
outboundSession,
sessionEntry,
result,
payloads,

View File

@@ -16,7 +16,7 @@ import {
normalizeOutboundPayloads,
normalizeOutboundPayloadsForJson,
} from "../../infra/outbound/payloads.js";
import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js";
import type { OutboundSessionContext } from "../../infra/outbound/session-context.js";
import type { RuntimeEnv } from "../../runtime.js";
import { isInternalMessageChannel } from "../../utils/message-channel.js";
import type { AgentCommandOpts } from "./types.js";
@@ -64,11 +64,12 @@ export async function deliverAgentCommandResult(params: {
deps: CliDeps;
runtime: RuntimeEnv;
opts: AgentCommandOpts;
outboundSession: OutboundSessionContext | undefined;
sessionEntry: SessionEntry | undefined;
result: RunResult;
payloads: RunResult["payloads"];
}) {
const { cfg, deps, runtime, opts, sessionEntry, payloads, result } = params;
const { cfg, deps, runtime, opts, outboundSession, sessionEntry, payloads, result } = params;
const deliver = opts.deliver === true;
const bestEffortDeliver = opts.bestEffortDeliver === true;
const turnSourceChannel = opts.runContext?.messageChannel ?? opts.messageChannel;
@@ -212,18 +213,13 @@ export async function deliverAgentCommandResult(params: {
}
if (deliver && deliveryChannel && !isInternalMessageChannel(deliveryChannel)) {
if (deliveryTarget) {
const deliverySession = buildOutboundSessionContext({
cfg,
agentId: opts.agentId,
sessionKey: opts.sessionKey,
});
await deliverOutboundPayloads({
cfg,
channel: deliveryChannel,
to: deliveryTarget,
accountId: resolvedAccountId,
payloads: deliveryPayloads,
session: deliverySession,
session: outboundSession,
replyToId: resolvedReplyToId ?? null,
threadId: resolvedThreadTarget ?? null,
bestEffort: bestEffortDeliver,