feat(matrix): thread-isolated sessions and per-chat-type threadReplies (#57995)

Merged via squash.

Prepared head SHA: 9ed96dd063
Co-authored-by: teconomix <6959299+teconomix@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Teconomix
2026-03-31 04:45:32 +02:00
committed by GitHub
parent d859746862
commit 697dddbeb6
20 changed files with 564 additions and 67 deletions

View File

@@ -51,6 +51,28 @@ describe("resolveAnnounceTargetFromKey", () => {
},
},
},
{
pluginId: "matrix",
source: "test",
plugin: {
id: "matrix",
meta: {
id: "matrix",
label: "Matrix",
selectionLabel: "Matrix",
docsPath: "/channels/matrix",
blurb: "Matrix test stub.",
},
capabilities: { chatTypes: ["direct", "channel", "thread"] },
messaging: {
resolveSessionTarget: ({ id }: { id: string }) => `channel:${id}`,
},
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
},
},
},
{
pluginId: "telegram",
source: "test",
@@ -107,4 +129,16 @@ describe("resolveAnnounceTargetFromKey", () => {
threadId: "1699999999.0001",
});
});
it("preserves colon-delimited matrix ids for channel and thread targets", () => {
expect(
resolveAnnounceTargetFromKey(
"agent:main:matrix:channel:!room:example.org:thread:$AbC123:example.org",
),
).toEqual({
channel: "matrix",
to: "channel:!room:example.org",
threadId: "$AbC123:example.org",
});
});
});

View File

@@ -4,6 +4,7 @@ import {
} from "../../channels/plugins/index.js";
import { normalizeChannelId as normalizeChatChannelId } from "../../channels/registry.js";
import type { OpenClawConfig } from "../../config/config.js";
import { parseSessionThreadInfo } from "../../config/sessions/delivery-info.js";
const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP";
const REPLY_SKIP_TOKEN = "REPLY_SKIP";
@@ -28,20 +29,9 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget
return null;
}
// Extract topic/thread ID from rest (supports both :topic: and :thread:)
// Telegram uses :topic:, other platforms use :thread:
let threadId: string | undefined;
const restJoined = rest.join(":");
const topicMatch = restJoined.match(/:topic:([^:]+)$/);
const threadMatch = restJoined.match(/:thread:([^:]+)$/);
const match = topicMatch || threadMatch;
if (match) {
threadId = match[1]; // Keep as string to match AgentCommandOpts.threadId
}
// Remove :topic:N or :thread:N suffix from ID for target
const id = match ? restJoined.replace(/:(topic|thread):[^:]+$/, "") : restJoined.trim();
const { baseSessionKey, threadId } = parseSessionThreadInfo(restJoined);
const id = (baseSessionKey ?? restJoined).trim();
if (!id) {
return null;