Skip to content

Commit 115455e

Browse files
openclaw-clownfish[bot]vincentkoc
authored andcommitted
fix(agents): prefer sessionKey in sessions_send
1 parent d28500f commit 115455e

4 files changed

Lines changed: 93 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
1919
- Plugin SDK: add tracked Discord component-message helpers and a Telegram account-resolution compatibility facade, so existing plugins using those subpaths resolve while new plugins stay on generic channel SDK contracts. Thanks @vincentkoc.
2020
- Shared labels: preserve Unicode combining marks and NFC-equivalent accented text in group/channel slug normalization so non-Latin labels no longer lose meaningful characters. Fixes #58932; carries forward #58942 and #58995. Thanks @fengqing-git, @Starhappysh, and @koen666.
2121
- Docs/Hetzner: clarify that SSH tunnel access requires `AllowTcpForwarding local` before running `ssh -L`, so hardened VPS sshd configs do not block loopback Gateway access. Fixes #54557; carries forward #54564; refs #54954. Thanks @satishkc7, @blackstrype, and @Aftabbs.
22+
- Agents/sessions: make `sessions_send` prefer explicit `sessionKey` inputs, including session id values, over redundant `label`/`agentId` hints so sends no longer reject when callers include stale label metadata. Fixes #64699; refs #41199; carries forward #59324 and supersedes #56203. Thanks @Mintalix and @RevisitMoon.
2223
- Gateway/shutdown: report structured shutdown warnings and HTTP close timeout warnings through `ShutdownResult` while preserving lifecycle hook hardening. Carries forward #41296. Thanks @edenfunf.
2324
- Plugins/QA: prebuild the private QA channel runtime before plugin gauntlet source runs so wrapper CPU/RSS measurements are not polluted by private QA dist rebuild work. Thanks @vincentkoc.
2425
- Gateway/reload: bound default restart deferral and SIGUSR1 restart drain to five minutes while preserving explicit `deferralTimeoutMs: 0` indefinite waits, so stale active work accounting cannot block config reloads forever. Thanks @vincentkoc.

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 or label; sessionKey accepts a full session key or session id and takes precedence over label/agentId when both are present.",
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: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,6 @@ export function createSessionsSendTool(opts?: {
106106
const sessionKeyParam = readStringParam(params, "sessionKey");
107107
const labelParam = normalizeOptionalString(readStringParam(params, "label"));
108108
const labelAgentIdParam = normalizeOptionalString(readStringParam(params, "agentId"));
109-
if (sessionKeyParam && labelParam) {
110-
return jsonResult({
111-
runId: crypto.randomUUID(),
112-
status: "error",
113-
error: "Provide either sessionKey or label (not both).",
114-
});
115-
}
116109

117110
let sessionKey = sessionKeyParam;
118111
if (!sessionKey && labelParam) {

src/agents/tools/sessions.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,97 @@ describe("sessions_send gating", () => {
644644
expect(callGatewayMock.mock.calls[0]?.[0]).toMatchObject({ method: "sessions.resolve" });
645645
});
646646

647+
it("prefers sessionKey over a redundant label", async () => {
648+
const tool = createMainSessionsSendTool();
649+
650+
const result = await tool.execute("call-session-key-label", {
651+
sessionKey: MAIN_AGENT_SESSION_KEY,
652+
label: "stale-label",
653+
message: "hi",
654+
timeoutSeconds: 0,
655+
});
656+
657+
expect(result.details).toMatchObject({
658+
status: "accepted",
659+
sessionKey: MAIN_AGENT_SESSION_KEY,
660+
});
661+
expect(callGatewayMock).toHaveBeenCalledTimes(2);
662+
expect(callGatewayMock.mock.calls[0]?.[0]).toMatchObject({ method: "sessions.list" });
663+
expect(callGatewayMock.mock.calls[1]?.[0]).toMatchObject({
664+
method: "agent",
665+
params: {
666+
sessionKey: MAIN_AGENT_SESSION_KEY,
667+
},
668+
});
669+
expect(callGatewayMock.mock.calls).not.toContainEqual([
670+
expect.objectContaining({
671+
method: "sessions.resolve",
672+
params: expect.objectContaining({ label: "stale-label" }),
673+
}),
674+
]);
675+
});
676+
677+
it("prefers a sessionId-shaped sessionKey over redundant label and agentId hints", async () => {
678+
loadConfigMock.mockReturnValue({
679+
session: { scope: "per-sender", mainKey: "main" },
680+
tools: {
681+
agentToAgent: { enabled: false },
682+
sessions: { visibility: "all" },
683+
},
684+
});
685+
const sessionId = "11111111-1111-4111-8111-111111111111";
686+
const resolvedSessionKey = "agent:main:subagent:worker";
687+
callGatewayMock.mockImplementation(async (opts: unknown) => {
688+
const request = opts as { method?: string; params?: Record<string, unknown> };
689+
if (request.method === "sessions.resolve" && request.params?.sessionId === sessionId) {
690+
return { key: resolvedSessionKey };
691+
}
692+
if (request.method === "agent") {
693+
return { runId: "run-session-id-send" };
694+
}
695+
return {};
696+
});
697+
const tool = createMainSessionsSendTool();
698+
699+
const result = await tool.execute("call-session-id-label-agent", {
700+
sessionKey: sessionId,
701+
label: "stale-label",
702+
agentId: "other",
703+
message: "hi",
704+
timeoutSeconds: 0,
705+
});
706+
707+
expect(result.details).toMatchObject({
708+
status: "accepted",
709+
sessionKey: resolvedSessionKey,
710+
});
711+
expect(callGatewayMock).toHaveBeenCalledTimes(3);
712+
expect(callGatewayMock.mock.calls[0]?.[0]).toMatchObject({
713+
method: "sessions.resolve",
714+
params: { key: sessionId },
715+
});
716+
expect(callGatewayMock.mock.calls[1]?.[0]).toMatchObject({
717+
method: "sessions.resolve",
718+
params: {
719+
sessionId,
720+
includeGlobal: true,
721+
includeUnknown: true,
722+
},
723+
});
724+
expect(callGatewayMock.mock.calls[2]?.[0]).toMatchObject({
725+
method: "agent",
726+
params: {
727+
sessionKey: resolvedSessionKey,
728+
},
729+
});
730+
expect(callGatewayMock.mock.calls).not.toContainEqual([
731+
expect.objectContaining({
732+
method: "sessions.resolve",
733+
params: expect.objectContaining({ label: "stale-label" }),
734+
}),
735+
]);
736+
});
737+
647738
it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => {
648739
const tool = createMainSessionsSendTool();
649740

0 commit comments

Comments
 (0)