mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 19:44:44 +00:00
fix(slack): scope dm last-route updates
Co-authored-by: clawSean <260045960+clawSean@users.noreply.github.com>
This commit is contained in:
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Slack: describe `download-file` file ids separately from message timestamps and return a targeted recovery error when agents pass `messageId` instead of `fileId`. (#74155) Thanks @jarvis-ai-gregmoser.
|
||||
- Slack: retain processed room messages for `requireMention=false` channels so always-on Slack rooms keep recent conversation context between turns. (#38658) Thanks @syedamaann.
|
||||
- Slack: compile interactive reply directives for direct outbound sends without bypassing the `interactiveReplies` capability gate, preserving Block Kit for Slack CLI and cron deliveries. (#78220) Thanks @kazamak.
|
||||
- Slack: keep DM last-route updates scoped to the active non-main DM session, including threaded DM turns, so isolated Slack DM sessions do not overwrite the shared main route. (#73085) Thanks @clawSean.
|
||||
- Gateway/agents: keep structured reasons when active-run queueing fails and deprecate the legacy boolean queue helper, so steering and subagent wake diagnostics distinguish completed, non-streaming, and compacting runs. Fixes #80156. Thanks @markus-lassfolk.
|
||||
- Agents/UI: compact exec and tool progress rows by hiding redundant shell tool names, replacing known workspace paths with short context markers, and preserving Discord trace scrubbing for compact command lines.
|
||||
- ACPX: run and await the embedded ACP backend startup probe by default so the gateway `ready` signal no longer fires before the acpx runtime has either become usable or reported a probe failure; set `OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE=0` to restore lazy startup. Fixes #79596. Thanks @bzelones.
|
||||
|
||||
@@ -8,6 +8,7 @@ const createSlackDraftStreamMock = vi.fn();
|
||||
const deliverRepliesMock = vi.fn(async () => {});
|
||||
const finalizeSlackPreviewEditMock = vi.fn(async () => {});
|
||||
const postMessageMock = vi.fn(async () => ({ ok: true, ts: "171234.999" }));
|
||||
const updateLastRouteMock = vi.fn(async () => {});
|
||||
const appendSlackStreamMock = vi.fn(async () => {});
|
||||
const startSlackStreamMock = vi.fn(async () => ({
|
||||
channel: "C123",
|
||||
@@ -192,6 +193,14 @@ function createPreparedSlackMessage(params?: {
|
||||
user: string;
|
||||
}>;
|
||||
replyToMode?: "off" | "first" | "all" | "batched";
|
||||
isDirectMessage?: boolean;
|
||||
route?: Partial<{
|
||||
agentId: string;
|
||||
accountId: string;
|
||||
mainSessionKey: string;
|
||||
sessionKey: string;
|
||||
lastRoutePolicy: "main" | "session";
|
||||
}>;
|
||||
setSlackThreadStatus?: (params: {
|
||||
channelId: string;
|
||||
threadTs?: string;
|
||||
@@ -201,6 +210,11 @@ function createPreparedSlackMessage(params?: {
|
||||
ackReactionMessageTs?: string;
|
||||
ackReactionPromise?: Promise<boolean> | null;
|
||||
}) {
|
||||
const routeSessionKey = params?.route?.sessionKey ?? "agent:agent-1:slack:C123";
|
||||
const mainSessionKey = params?.route?.mainSessionKey ?? "main";
|
||||
const lastRoutePolicy =
|
||||
params?.route?.lastRoutePolicy ?? (routeSessionKey === mainSessionKey ? "main" : "session");
|
||||
|
||||
return {
|
||||
ctx: {
|
||||
cfg: params?.cfg ?? {},
|
||||
@@ -230,8 +244,10 @@ function createPreparedSlackMessage(params?: {
|
||||
route: {
|
||||
agentId: "agent-1",
|
||||
accountId: "default",
|
||||
mainSessionKey: "main",
|
||||
sessionKey: "agent:agent-1:slack:C123",
|
||||
mainSessionKey,
|
||||
sessionKey: routeSessionKey,
|
||||
lastRoutePolicy,
|
||||
...params?.route,
|
||||
},
|
||||
channelConfig: null,
|
||||
replyTarget: "channel:C123",
|
||||
@@ -244,7 +260,7 @@ function createPreparedSlackMessage(params?: {
|
||||
record: {},
|
||||
},
|
||||
replyToMode: params?.replyToMode ?? "all",
|
||||
isDirectMessage: false,
|
||||
isDirectMessage: params?.isDirectMessage ?? false,
|
||||
isRoomish: false,
|
||||
historyKey: "history-key",
|
||||
preview: "",
|
||||
@@ -588,7 +604,7 @@ vi.mock("../allow-list.js", () => ({
|
||||
|
||||
vi.mock("../config.runtime.js", () => ({
|
||||
resolveStorePath: () => "/tmp/openclaw-store.json",
|
||||
updateLastRoute: async () => {},
|
||||
updateLastRoute: updateLastRouteMock,
|
||||
}));
|
||||
|
||||
vi.mock("../replies.js", () => ({
|
||||
@@ -699,6 +715,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
|
||||
deliverRepliesMock.mockReset();
|
||||
finalizeSlackPreviewEditMock.mockReset();
|
||||
postMessageMock.mockClear();
|
||||
updateLastRouteMock.mockReset();
|
||||
appendSlackStreamMock.mockReset();
|
||||
startSlackStreamMock.mockReset();
|
||||
stopSlackStreamMock.mockReset();
|
||||
@@ -741,6 +758,75 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
|
||||
expectDeliverReplyCall(0, FINAL_REPLY_TEXT);
|
||||
});
|
||||
|
||||
it("updates non-main DM last-route metadata on the prepared thread session", async () => {
|
||||
await dispatchPreparedSlackMessage(
|
||||
createPreparedSlackMessage({
|
||||
cfg: { session: { dmScope: "per-channel-peer" } },
|
||||
isDirectMessage: true,
|
||||
message: {
|
||||
channel: "D123",
|
||||
user: "U1",
|
||||
ts: "501.000",
|
||||
thread_ts: "500.000",
|
||||
},
|
||||
route: {
|
||||
agentId: "main",
|
||||
mainSessionKey: "agent:main:main",
|
||||
sessionKey: "agent:main:slack:direct:u1",
|
||||
lastRoutePolicy: "session",
|
||||
},
|
||||
ctxPayload: {
|
||||
MessageThreadId: "500.000",
|
||||
SessionKey: "agent:main:slack:direct:u1:thread:500.000",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(updateLastRouteMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:slack:direct:u1:thread:500.000",
|
||||
deliveryContext: expect.objectContaining({
|
||||
threadId: "500.000",
|
||||
to: "user:U1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps default main-scope DM last-route metadata on the main session", async () => {
|
||||
await dispatchPreparedSlackMessage(
|
||||
createPreparedSlackMessage({
|
||||
isDirectMessage: true,
|
||||
message: {
|
||||
channel: "D123",
|
||||
user: "U1",
|
||||
ts: "601.000",
|
||||
thread_ts: "600.000",
|
||||
},
|
||||
route: {
|
||||
agentId: "main",
|
||||
mainSessionKey: "agent:main:main",
|
||||
sessionKey: "agent:main:main",
|
||||
lastRoutePolicy: "main",
|
||||
},
|
||||
ctxPayload: {
|
||||
MessageThreadId: "600.000",
|
||||
SessionKey: "agent:main:main:thread:600.000",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(updateLastRouteMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:main",
|
||||
deliveryContext: expect.objectContaining({
|
||||
threadId: "600.000",
|
||||
to: "user:U1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("finalizes fast draft preview text without sending a duplicate normal reply", async () => {
|
||||
const draftStream = {
|
||||
...createDraftStreamStub(),
|
||||
|
||||
@@ -38,6 +38,7 @@ import { resolveAgentOutboundIdentity } from "openclaw/plugin-sdk/outbound-runti
|
||||
import { clearHistoryEntriesIfEnabled } from "openclaw/plugin-sdk/reply-history";
|
||||
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
||||
import type { ReplyDispatchKind, ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { danger, logVerbose, shouldLogVerbose, sleep } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -331,7 +332,10 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
} else {
|
||||
await updateLastRoute({
|
||||
storePath,
|
||||
sessionKey: route.mainSessionKey,
|
||||
sessionKey: resolveInboundLastRouteSessionKey({
|
||||
route,
|
||||
sessionKey: prepared.ctxPayload.SessionKey ?? route.sessionKey,
|
||||
}),
|
||||
deliveryContext: {
|
||||
channel: "slack",
|
||||
to: `user:${message.user}`,
|
||||
|
||||
@@ -1278,6 +1278,61 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
expect(prepared.ctxPayload.MessageThreadId).toBe("500.000");
|
||||
});
|
||||
|
||||
it("records non-main DM last-route metadata on the prepared thread session", async () => {
|
||||
const { storePath } = storeFixture.makeTmpStorePath();
|
||||
const slackCtx = createInboundSlackCtx({
|
||||
cfg: {
|
||||
session: { store: storePath, dmScope: "per-channel-peer" },
|
||||
channels: { slack: { enabled: true, replyToMode: "all" } },
|
||||
} as OpenClawConfig,
|
||||
replyToMode: "all",
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
createSlackAccount({ replyToMode: "all" }),
|
||||
createSlackMessage({
|
||||
text: "thread reply",
|
||||
ts: "501.000",
|
||||
thread_ts: "500.000",
|
||||
}),
|
||||
);
|
||||
|
||||
assertPrepared(prepared);
|
||||
expect(prepared.route.sessionKey).toBe("agent:main:slack:direct:u1");
|
||||
expect(prepared.ctxPayload.SessionKey).toBe("agent:main:slack:direct:u1:thread:500.000");
|
||||
expect(
|
||||
(prepared.turn.record as { updateLastRoute?: { sessionKey?: string } }).updateLastRoute,
|
||||
).toEqual(expect.objectContaining({ sessionKey: prepared.ctxPayload.SessionKey }));
|
||||
});
|
||||
|
||||
it("keeps default main-scope DM last-route metadata on the main session", async () => {
|
||||
const slackCtx = createInboundSlackCtx({
|
||||
cfg: {
|
||||
channels: { slack: { enabled: true, replyToMode: "all" } },
|
||||
} as OpenClawConfig,
|
||||
replyToMode: "all",
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
createSlackAccount({ replyToMode: "all" }),
|
||||
createSlackMessage({
|
||||
text: "thread reply",
|
||||
ts: "601.000",
|
||||
thread_ts: "600.000",
|
||||
}),
|
||||
);
|
||||
|
||||
assertPrepared(prepared);
|
||||
expect(prepared.ctxPayload.SessionKey).toBe("agent:main:main:thread:600.000");
|
||||
expect(
|
||||
(prepared.turn.record as { updateLastRoute?: { sessionKey?: string } }).updateLastRoute,
|
||||
).toEqual(expect.objectContaining({ sessionKey: "agent:main:main" }));
|
||||
});
|
||||
|
||||
it("routes Slack thread replies through runtime conversation bindings", async () => {
|
||||
const targetSessionKey = "agent:review:acp:session-67739";
|
||||
const binding: SessionBindingRecord = {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
} from "openclaw/plugin-sdk/reply-history";
|
||||
import type { FinalizedMsgContext } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
|
||||
import {
|
||||
@@ -991,7 +992,7 @@ export async function prepareSlackMessage(params: {
|
||||
record: {
|
||||
updateLastRoute: isDirectMessage
|
||||
? {
|
||||
sessionKey: route.mainSessionKey,
|
||||
sessionKey: resolveInboundLastRouteSessionKey({ route, sessionKey }),
|
||||
channel: "slack",
|
||||
to: `user:${message.user}`,
|
||||
accountId: route.accountId,
|
||||
|
||||
Reference in New Issue
Block a user