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: { async function runDelivery(params: {
opts: Record<string, unknown>; opts: Record<string, unknown>;
outboundSession?: { key?: string; agentId?: string };
sessionEntry?: SessionEntry; sessionEntry?: SessionEntry;
runtime?: RuntimeEnv; runtime?: RuntimeEnv;
resultText?: string; resultText?: string;
@@ -62,6 +63,7 @@ describe("deliverAgentCommandResult", () => {
deps, deps,
runtime, runtime,
opts: params.opts as never, opts: params.opts as never,
outboundSession: params.outboundSession,
sessionEntry: params.sessionEntry, sessionEntry: params.sessionEntry,
result, result,
payloads: result.payloads, 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 () => { it("prefixes nested agent outputs with context", async () => {
const runtime = createRuntime(); const runtime = createRuntime();
await runDelivery({ await runDelivery({

View File

@@ -14,6 +14,7 @@ import { setActivePluginRegistry } from "../plugins/runtime.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js"; import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
import { agentCommand } from "./agent.js"; import { agentCommand } from "./agent.js";
import * as agentDeliveryModule from "./agent/delivery.js";
vi.mock("../agents/auth-profiles.js", async (importOriginal) => { vi.mock("../agents/auth-profiles.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../agents/auth-profiles.js")>(); const actual = await importOriginal<typeof import("../agents/auth-profiles.js")>();
@@ -49,6 +50,7 @@ const runtime: RuntimeEnv = {
const configSpy = vi.spyOn(configModule, "loadConfig"); const configSpy = vi.spyOn(configModule, "loadConfig");
const runCliAgentSpy = vi.spyOn(cliRunnerModule, "runCliAgent"); const runCliAgentSpy = vi.spyOn(cliRunnerModule, "runCliAgent");
const deliverAgentCommandResultSpy = vi.spyOn(agentDeliveryModule, "deliverAgentCommandResult");
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
return withTempHomeBase(fn, { prefix: "openclaw-agent-" }); 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 () => { it("resolves resumed session transcript path from custom session store directory", async () => {
await withTempHome(async (home) => { await withTempHome(async (home) => {
const customStoreDir = path.join(home, "custom-state"); const customStoreDir = path.join(home, "custom-state");

View File

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

View File

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