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

@@ -176,6 +176,21 @@ function createMatrixThreadCommandParams(commandBody: string, overrides?: Record
});
}
function createMatrixTriggerThreadCommandParams(
commandBody: string,
overrides?: Record<string, unknown>,
) {
return buildCommandTestParams(commandBody, baseCfg, {
Provider: "matrix",
Surface: "matrix",
OriginatingChannel: "matrix",
OriginatingTo: "room:!room:example.org",
AccountId: "default",
MessageThreadId: "$root",
...overrides,
});
}
function createMatrixRoomCommandParams(commandBody: string, overrides?: Record<string, unknown>) {
return buildCommandTestParams(commandBody, baseCfg, {
Provider: "matrix",
@@ -248,6 +263,21 @@ function createMatrixBinding(overrides?: Partial<SessionBindingRecord>): Session
};
}
function createMatrixTriggerBinding(
overrides?: Partial<SessionBindingRecord>,
): SessionBindingRecord {
return createMatrixBinding({
bindingId: "default:$root",
conversation: {
channel: "matrix",
accountId: "default",
conversationId: "$root",
parentConversationId: "!room:example.org",
},
...overrides,
});
}
function expectIdleTimeoutSetReply(
mock: ReturnType<typeof vi.fn>,
text: string,
@@ -409,6 +439,40 @@ describe("/session idle and /session max-age", () => {
);
});
it("sets idle timeout for the triggering Matrix always-thread turn", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-02-20T00:00:00.000Z"));
hoisted.sessionBindingResolveByConversationMock.mockReturnValue(createMatrixTriggerBinding());
hoisted.setMatrixThreadBindingIdleTimeoutBySessionKeyMock.mockReturnValue([
{
targetSessionKey: "agent:main:subagent:child",
boundAt: Date.now(),
lastActivityAt: Date.now(),
idleTimeoutMs: 2 * 60 * 60 * 1000,
},
]);
const result = await handleSessionCommand(
createMatrixTriggerThreadCommandParams("/session idle 2h"),
true,
);
const text = result?.reply?.text ?? "";
expect(hoisted.sessionBindingResolveByConversationMock).toHaveBeenCalledWith({
channel: "matrix",
accountId: "default",
conversationId: "$root",
parentConversationId: "!room:example.org",
});
expectIdleTimeoutSetReply(
hoisted.setMatrixThreadBindingIdleTimeoutBySessionKeyMock,
text,
2 * 60 * 60 * 1000,
"2h",
);
});
it("sets max age for focused Matrix threads", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-02-20T00:00:00.000Z"));

View File

@@ -116,6 +116,22 @@ function createMatrixThreadCommandParams(commandBody: string, cfg: OpenClawConfi
return params;
}
function createMatrixTriggerThreadCommandParams(
commandBody: string,
cfg: OpenClawConfig = baseCfg,
) {
const params = buildCommandTestParams(commandBody, cfg, {
Provider: "matrix",
Surface: "matrix",
OriginatingChannel: "matrix",
OriginatingTo: "room:!room:example.org",
AccountId: "default",
MessageThreadId: "$root",
});
params.command.senderId = "user-1";
return params;
}
function createMatrixRoomCommandParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
const params = buildCommandTestParams(commandBody, cfg, {
Provider: "matrix",
@@ -282,6 +298,22 @@ describe("/focus, /unfocus, /agents", () => {
);
});
it("/focus treats the triggering Matrix always-thread turn as the current thread", async () => {
const result = await focusCodexAcp(createMatrixTriggerThreadCommandParams("/focus codex-acp"));
expect(result?.reply?.text).toContain("bound this thread");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "current",
conversation: expect.objectContaining({
channel: "matrix",
conversationId: "$root",
parentConversationId: "!room:example.org",
}),
}),
);
});
it("/focus rejects Matrix top-level thread creation when spawnSubagentSessions is disabled", async () => {
const cfg = {
...baseCfg,