Skip to content

Commit 4a08e3c

Browse files
committed
fix: prefer sessionKey in sessions_send target resolution
1 parent 316e0f2 commit 4a08e3c

4 files changed

Lines changed: 76 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Docs: https://docs.openclaw.ai
1414

1515
- NVIDIA/NIM: persist the `NVIDIA_API_KEY` provider marker and mark bundled NVIDIA Chat Completions models as string-content compatible, so NIM models load from `models.json` and OpenAI-compatible subagent calls send plain text content. Fixes #73013 and #50107; refs #73014. Thanks @bautrey, @iot2edge, @ifearghal, and @futhgar.
1616
- CLI/plugins: use plugin metadata snapshots for install slot selection and add opt-in plugin lifecycle timing traces, so plugin install avoids runtime-loading the plugin registry for metadata-only decisions. Thanks @shakkernerd.
17-
- Agents/sessions_send: prefer explicit `sessionKey` targets over redundant `label` and `agentId` fields so exact-key sends no longer fail or resolve through the label path. (#39551) Thanks @1034378361.
17+
- Agents/sessions_send: prefer explicit `sessionKey`/`sessionId` targets over redundant `label` and `agentId` fields so exact-key sends no longer fail or resolve through the label path. (#39551) Thanks @1034378361.
1818
- fix(plugins): restrict bundled plugin dir resolution to trusted package roots. (#73275) Thanks @pgondhi987.
1919
- fix(security): prevent workspace PATH injection via service env and trash helpers. (#73264) Thanks @pgondhi987.
2020
- Active Memory: allow `allowedChatTypes` to include explicit portal/webchat sessions and classify `agent:...:explicit:...` session keys before opaque session ids can shadow the chat type. Fixes #65775. (#66285) Thanks @Lidang-Jiang.

src/agents/tool-description-presets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function describeSessionsHistoryTool(): string {
2727

2828
export function describeSessionsSendTool(): string {
2929
return [
30-
"Send a message into another visible session by sessionKey or label.",
30+
"Send a message into another visible session by sessionKey, sessionId, or label.",
3131
"Use this to delegate follow-up work to an existing session; waits for the target run and returns the updated assistant reply when available.",
3232
].join(" ");
3333
}

src/agents/tools/sessions-send-tool.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { runSessionsSendA2AFlow } from "./sessions-send-tool.a2a.js";
3636

3737
const SessionsSendToolSchema = Type.Object({
3838
sessionKey: Type.Optional(Type.String()),
39+
sessionId: Type.Optional(Type.String()),
3940
label: Type.Optional(Type.String({ minLength: 1, maxLength: SESSION_LABEL_MAX_LENGTH })),
4041
agentId: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
4142
message: Type.String(),
@@ -102,7 +103,8 @@ export function createSessionsSendTool(opts?: {
102103
sandboxed: opts?.sandboxed === true,
103104
});
104105

105-
const sessionKeyParam = readStringParam(params, "sessionKey");
106+
const sessionKeyParam =
107+
readStringParam(params, "sessionKey") ?? readStringParam(params, "sessionId");
106108
const labelParam = sessionKeyParam
107109
? undefined
108110
: normalizeOptionalString(readStringParam(params, "label"));
@@ -193,7 +195,7 @@ export function createSessionsSendTool(opts?: {
193195
return jsonResult({
194196
runId: crypto.randomUUID(),
195197
status: "error",
196-
error: "Either sessionKey or label is required",
198+
error: "Either sessionKey/sessionId or label is required",
197199
});
198200
}
199201
const resolvedSession = await resolveSessionReference({

src/agents/tools/sessions.test.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ describe("sessions_send gating", () => {
609609
callGatewayMock.mockClear();
610610
});
611611

612-
it("returns an error when neither sessionKey nor label is provided", async () => {
612+
it("returns an error when no sessionKey, sessionId, or label is provided", async () => {
613613
const tool = createMainSessionsSendTool();
614614

615615
const result = await tool.execute("call-missing-target", {
@@ -619,7 +619,7 @@ describe("sessions_send gating", () => {
619619

620620
expect(result.details).toMatchObject({
621621
status: "error",
622-
error: "Either sessionKey or label is required",
622+
error: "Either sessionKey/sessionId or label is required",
623623
});
624624
expect(callGatewayMock).not.toHaveBeenCalled();
625625
});
@@ -705,6 +705,74 @@ describe("sessions_send gating", () => {
705705
);
706706
});
707707

708+
it("prefers sessionId when sessionId, label, and agentId are all provided", async () => {
709+
loadConfigMock.mockReturnValue({
710+
session: { scope: "per-sender", mainKey: "main" },
711+
tools: {
712+
agentToAgent: { enabled: true },
713+
sessions: { visibility: "all" },
714+
},
715+
});
716+
callGatewayMock.mockImplementation(async (opts: unknown) => {
717+
const request = opts as { method?: string; params?: Record<string, unknown> };
718+
if (request.method === "sessions.resolve") {
719+
if (request.params?.sessionId === "sess-worker") {
720+
return { key: "agent:worker:main" };
721+
}
722+
return {};
723+
}
724+
if (request.method === "sessions.list") {
725+
return {
726+
path: "/tmp/sessions.json",
727+
sessions: [
728+
{
729+
key: "agent:worker:main",
730+
kind: "direct",
731+
sessionId: "sess-worker",
732+
},
733+
],
734+
};
735+
}
736+
if (request.method === "agent") {
737+
return { runId: "run-mixed-session-id", acceptedAt: 123 };
738+
}
739+
return {};
740+
});
741+
742+
const tool = createMainSessionsSendTool();
743+
const result = await tool.execute("call-mixed-session-id-target", {
744+
sessionId: "sess-worker",
745+
label: "wrong-label",
746+
agentId: "wrong-agent",
747+
message: "hello from session id",
748+
timeoutSeconds: 0,
749+
});
750+
751+
expect(result.details).toMatchObject({
752+
status: "accepted",
753+
sessionKey: "agent:worker:main",
754+
});
755+
expect(
756+
callGatewayMock.mock.calls.some((call) => {
757+
const params = (call[0] as { params?: Record<string, unknown> }).params;
758+
return params?.label === "wrong-label" || params?.agentId === "wrong-agent";
759+
}),
760+
).toBe(false);
761+
expect(callGatewayMock.mock.calls).toEqual(
762+
expect.arrayContaining([
763+
[
764+
expect.objectContaining({
765+
method: "agent",
766+
params: expect.objectContaining({
767+
sessionKey: "agent:worker:main",
768+
message: "hello from session id",
769+
}),
770+
}),
771+
],
772+
]),
773+
);
774+
});
775+
708776
it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => {
709777
const tool = createMainSessionsSendTool();
710778

0 commit comments

Comments
 (0)