mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(sessions): reject thread send targets
This commit is contained in:
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram: inherit the process DNS result order for Bot API transport and downgrade recovered sticky IPv4 fallback promotions to debug logs, while keeping pinned-IP escalation warnings visible. Fixes #75904. Thanks @highfly-hi and @neeravmakwana.
|
||||
- Web search/MiniMax: allow `MINIMAX_OAUTH_TOKEN` to satisfy MiniMax Search credentials, so OAuth-authorized MiniMax Token Plan setups do not need a separate web-search key. Fixes #65768. Thanks @kikibrian and @zhouhe-xydt.
|
||||
- Providers/MiniMax: derive Coding Plan usage polling from the configured MiniMax base URL, so global setups no longer query the CN usage host. Fixes #65054. Thanks @sixone74 and @Yanhu007.
|
||||
- Sessions: reject `sessions_send` targets that resolve to thread-scoped chat sessions, so inter-agent coordination cannot be injected into active human-facing Slack or Discord threads. Fixes #52496. Thanks @barry-p5cc.
|
||||
- Subagents: honor `sessions_spawn` with `expectsCompletionMessage: false` by skipping parent completion handoff delivery while still running child cleanup. Fixes #75848. Thanks @alfredjbclaw.
|
||||
- Gateway/logging: keep deferred channel startup logs on the subsystem logger, so Slack, Discord, Telegram, and voice-call startup messages keep timestamped prefixes. Thanks @vincentkoc.
|
||||
- Replies/typing: keep typing alive for queued follow-up messages that are genuinely waiting behind an active run, instead of making chat surfaces look idle while work is queued. Fixes #65685. Thanks @papag00se.
|
||||
|
||||
@@ -93,6 +93,11 @@ the response:
|
||||
immediately.
|
||||
- **Wait for reply:** set a timeout and get the response inline.
|
||||
|
||||
Thread-scoped chat sessions, such as Slack or Discord keys ending in
|
||||
`:thread:<id>`, are not valid `sessions_send` targets. Use the parent channel
|
||||
session key for inter-agent coordination so tool-routed messages do not appear
|
||||
inside an active human-facing thread.
|
||||
|
||||
Messages and A2A follow-up replies are marked as inter-session data in the
|
||||
receiving prompt (`[Inter-session message ... isUser=false]`) and in transcript
|
||||
provenance. The receiving agent should treat them as tool-routed data, not as a
|
||||
|
||||
@@ -28,6 +28,7 @@ export function describeSessionsHistoryTool(): string {
|
||||
export function describeSessionsSendTool(): string {
|
||||
return [
|
||||
"Send a message into another visible session by sessionKey or label.",
|
||||
"Thread-scoped chat sessions are rejected; target the parent channel session for inter-agent coordination.",
|
||||
"Use this to delegate follow-up work to an existing session; waits for the target run and returns the updated assistant reply when available.",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import { Type } from "typebox";
|
||||
import { isRequesterParentOfBackgroundAcpSession } from "../../acp/session-interaction-mode.js";
|
||||
import { parseSessionThreadInfoFast } from "../../config/sessions/thread-info.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
@@ -254,6 +255,15 @@ export function createSessionsSendTool(opts?: {
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
if (parseSessionThreadInfoFast(resolvedKey).threadId) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "error",
|
||||
error:
|
||||
"sessions_send cannot target a thread session for inter-agent coordination. Use the parent channel session key instead.",
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
|
||||
// Capture the pre-run assistant snapshot before starting the nested run.
|
||||
// Fast in-process test doubles and short-circuit agent paths can finish
|
||||
|
||||
@@ -658,6 +658,62 @@ describe("sessions_send gating", () => {
|
||||
expect(result.details).toMatchObject({ status: "forbidden" });
|
||||
});
|
||||
|
||||
it("rejects direct thread session targets before dispatching an agent run", async () => {
|
||||
loadConfigMock.mockReturnValue({
|
||||
session: { scope: "per-sender", mainKey: "main" },
|
||||
tools: {
|
||||
agentToAgent: { enabled: false },
|
||||
sessions: { visibility: "all" },
|
||||
},
|
||||
});
|
||||
const threadSessionKey = "agent:main:slack:channel:C123:thread:1710000000.000100";
|
||||
const tool = createMainSessionsSendTool();
|
||||
|
||||
const result = await tool.execute("call-thread-target", {
|
||||
sessionKey: threadSessionKey,
|
||||
message: "hi",
|
||||
timeoutSeconds: 0,
|
||||
});
|
||||
|
||||
expect(result.details).toMatchObject({
|
||||
status: "error",
|
||||
sessionKey: threadSessionKey,
|
||||
});
|
||||
expect((result.details as { error?: string } | undefined)?.error ?? "").toContain(
|
||||
"cannot target a thread session",
|
||||
);
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects label targets that resolve to canonical thread sessions", async () => {
|
||||
loadConfigMock.mockReturnValue({
|
||||
session: { scope: "per-sender", mainKey: "main" },
|
||||
tools: {
|
||||
agentToAgent: { enabled: false },
|
||||
sessions: { visibility: "all" },
|
||||
},
|
||||
});
|
||||
const threadSessionKey = "agent:main:discord:channel:123456:thread:987654";
|
||||
callGatewayMock.mockResolvedValueOnce({ key: threadSessionKey });
|
||||
const tool = createMainSessionsSendTool();
|
||||
|
||||
const result = await tool.execute("call-thread-label", {
|
||||
label: "active thread",
|
||||
message: "hi",
|
||||
timeoutSeconds: 0,
|
||||
});
|
||||
|
||||
expect(result.details).toMatchObject({
|
||||
status: "error",
|
||||
sessionKey: threadSessionKey,
|
||||
});
|
||||
expect((result.details as { error?: string } | undefined)?.error ?? "").toContain(
|
||||
"cannot target a thread session",
|
||||
);
|
||||
expect(callGatewayMock).toHaveBeenCalledTimes(1);
|
||||
expect(callGatewayMock.mock.calls[0]?.[0]).toMatchObject({ method: "sessions.resolve" });
|
||||
});
|
||||
|
||||
it("does not reuse a stale assistant reply when no new reply appears", async () => {
|
||||
const tool = createMainSessionsSendTool();
|
||||
let historyCalls = 0;
|
||||
|
||||
Reference in New Issue
Block a user