mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:20:43 +00:00
feat: simplify thread-bound session spawning
This commit is contained in:
@@ -26,6 +26,8 @@ const matrixThreadBindingsSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
idleHours: z.number().nonnegative().optional(),
|
||||
maxAgeHours: z.number().nonnegative().optional(),
|
||||
spawnSessions: z.boolean().optional(),
|
||||
defaultSpawnContext: z.enum(["isolated", "fork"]).optional(),
|
||||
spawnSubagentSessions: z.boolean().optional(),
|
||||
spawnAcpSessions: z.boolean().optional(),
|
||||
})
|
||||
|
||||
@@ -90,9 +90,8 @@ describe("handleMatrixSubagentSpawning", () => {
|
||||
getManagerMock.mockReset();
|
||||
resolveMatrixBaseConfigMock.mockReset();
|
||||
findMatrixAccountConfigMock.mockReset();
|
||||
// Default: bindings enabled, spawn enabled
|
||||
resolveMatrixBaseConfigMock.mockReturnValue({
|
||||
threadBindings: { enabled: true, spawnSubagentSessions: true },
|
||||
threadBindings: { enabled: true, spawnSessions: true },
|
||||
});
|
||||
findMatrixAccountConfigMock.mockReturnValue(undefined);
|
||||
getCapabilitiesMock.mockReturnValue({
|
||||
@@ -140,40 +139,46 @@ describe("handleMatrixSubagentSpawning", () => {
|
||||
});
|
||||
|
||||
it("returns error when thread bindings are disabled", async () => {
|
||||
resolveMatrixBaseConfigMock.mockReturnValue({
|
||||
threadBindings: { enabled: false, spawnSubagentSessions: true },
|
||||
});
|
||||
const result = await handleMatrixSubagentSpawning(fakeApi, makeSpawnEvent());
|
||||
const result = await handleMatrixSubagentSpawning(
|
||||
{
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
threadBindings: { enabled: false, spawnSessions: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
makeSpawnEvent(),
|
||||
);
|
||||
expect(result).toEqual(expect.objectContaining({ status: "error" }));
|
||||
expect((result as { error?: string }).error).toMatch(/thread bindings are disabled/i);
|
||||
});
|
||||
|
||||
it("returns error when spawnSessions is false", async () => {
|
||||
const result = await handleMatrixSubagentSpawning(
|
||||
{
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
threadBindings: { enabled: true, spawnSessions: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
makeSpawnEvent(),
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "error",
|
||||
error: expect.stringContaining("thread bindings are disabled"),
|
||||
error: expect.stringContaining("spawnSessions"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns error when spawnSubagentSessions is false", async () => {
|
||||
resolveMatrixBaseConfigMock.mockReturnValue({
|
||||
threadBindings: { enabled: true, spawnSubagentSessions: false },
|
||||
});
|
||||
it("allows thread-bound subagent spawn by default", async () => {
|
||||
const result = await handleMatrixSubagentSpawning(fakeApi, makeSpawnEvent());
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "error",
|
||||
error: expect.stringContaining("spawnSubagentSessions"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns error when spawnSubagentSessions defaults to false (no config)", async () => {
|
||||
resolveMatrixBaseConfigMock.mockReturnValue({});
|
||||
const result = await handleMatrixSubagentSpawning(fakeApi, makeSpawnEvent());
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "error",
|
||||
error: expect.stringContaining("spawnSubagentSessions"),
|
||||
}),
|
||||
);
|
||||
expect(result).toMatchObject({ status: "ok", threadBindingReady: true });
|
||||
});
|
||||
|
||||
it("returns error when requester.to has no room target", async () => {
|
||||
@@ -295,17 +300,23 @@ describe("handleMatrixSubagentSpawning", () => {
|
||||
});
|
||||
|
||||
it("respects per-account threadBindings override over base config", async () => {
|
||||
// Base says spawnSubagentSessions=false; account override says true
|
||||
resolveMatrixBaseConfigMock.mockReturnValue({
|
||||
threadBindings: { enabled: true, spawnSubagentSessions: false },
|
||||
});
|
||||
findMatrixAccountConfigMock.mockReturnValue({
|
||||
threadBindings: { spawnSubagentSessions: true },
|
||||
});
|
||||
bindMock.mockResolvedValue({ conversation: {} });
|
||||
|
||||
const result = await handleMatrixSubagentSpawning(
|
||||
fakeApi,
|
||||
{
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
threadBindings: { enabled: true, spawnSessions: false },
|
||||
accounts: {
|
||||
forge: {
|
||||
threadBindings: { spawnSessions: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
makeSpawnEvent({ accountId: "forge" }),
|
||||
);
|
||||
expect(result).toMatchObject({ status: "ok", threadBindingReady: true });
|
||||
@@ -322,7 +333,7 @@ describe("matrix subagent hook registration", () => {
|
||||
listBindingsForAccountMock.mockReset();
|
||||
listAllBindingsMock.mockReset();
|
||||
resolveMatrixBaseConfigMock.mockReturnValue({
|
||||
threadBindings: { enabled: true, spawnSubagentSessions: true },
|
||||
threadBindings: { enabled: true, spawnSessions: true },
|
||||
});
|
||||
findMatrixAccountConfigMock.mockReturnValue(undefined);
|
||||
getCapabilitiesMock.mockReturnValue({
|
||||
@@ -784,7 +795,7 @@ describe("concurrent spawns across accounts", () => {
|
||||
resolveMatrixBaseConfigMock.mockReset();
|
||||
findMatrixAccountConfigMock.mockReset();
|
||||
resolveMatrixBaseConfigMock.mockReturnValue({
|
||||
threadBindings: { enabled: true, spawnSubagentSessions: true },
|
||||
threadBindings: { enabled: true, spawnSessions: true },
|
||||
});
|
||||
findMatrixAccountConfigMock.mockReturnValue(undefined);
|
||||
getCapabilitiesMock.mockReturnValue({
|
||||
|
||||
@@ -3,9 +3,13 @@ import {
|
||||
getSessionBindingService,
|
||||
type SessionBindingRecord,
|
||||
} from "openclaw/plugin-sdk/conversation-binding-runtime";
|
||||
import {
|
||||
formatThreadBindingDisabledError,
|
||||
formatThreadBindingSpawnDisabledError,
|
||||
resolveThreadBindingSpawnPolicy,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { findMatrixAccountConfig, resolveMatrixBaseConfig } from "./account-config.js";
|
||||
import { resolveMatrixTargetIdentity } from "./target-ids.js";
|
||||
import {
|
||||
getMatrixThreadBindingManager,
|
||||
@@ -76,28 +80,6 @@ function summarizeError(err: unknown): string {
|
||||
return "error";
|
||||
}
|
||||
|
||||
function resolveThreadBindingFlags(
|
||||
api: OpenClawPluginApi,
|
||||
accountId?: string,
|
||||
): { enabled: boolean; spawnSubagentSessions: boolean } {
|
||||
const matrix = resolveMatrixBaseConfig(api.config);
|
||||
const baseThreadBindings = matrix.threadBindings;
|
||||
const accountThreadBindings = accountId
|
||||
? findMatrixAccountConfig(api.config, accountId)?.threadBindings
|
||||
: undefined;
|
||||
return {
|
||||
enabled:
|
||||
accountThreadBindings?.enabled ??
|
||||
baseThreadBindings?.enabled ??
|
||||
api.config.session?.threadBindings?.enabled ??
|
||||
true,
|
||||
spawnSubagentSessions:
|
||||
accountThreadBindings?.spawnSubagentSessions ??
|
||||
baseThreadBindings?.spawnSubagentSessions ??
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMatrixBindingThreadId(binding: SessionBindingRecord): string | undefined {
|
||||
const { conversationId, parentConversationId } = binding.conversation;
|
||||
return parentConversationId && parentConversationId !== conversationId
|
||||
@@ -136,20 +118,31 @@ export async function handleMatrixSubagentSpawning(
|
||||
// Falls back to DEFAULT_ACCOUNT_ID so accounts.default.threadBindings.* is
|
||||
// respected even when the requester omits accountId.
|
||||
const accountId = normalizeOptionalString(event.requester?.accountId) || DEFAULT_ACCOUNT_ID;
|
||||
const flags = resolveThreadBindingFlags(api, accountId);
|
||||
const policy = resolveThreadBindingSpawnPolicy({
|
||||
cfg: api.config,
|
||||
channel: "matrix",
|
||||
accountId,
|
||||
kind: "subagent",
|
||||
});
|
||||
|
||||
if (!flags.enabled) {
|
||||
if (!policy.enabled) {
|
||||
return {
|
||||
status: "error",
|
||||
error:
|
||||
"Matrix thread bindings are disabled (set channels.matrix.threadBindings.enabled=true to override for this account, or session.threadBindings.enabled=true globally).",
|
||||
error: formatThreadBindingDisabledError({
|
||||
channel: policy.channel,
|
||||
accountId: policy.accountId,
|
||||
kind: "subagent",
|
||||
}),
|
||||
} satisfies SpawningResult;
|
||||
}
|
||||
if (!flags.spawnSubagentSessions) {
|
||||
if (!policy.spawnEnabled) {
|
||||
return {
|
||||
status: "error",
|
||||
error:
|
||||
"Matrix thread-bound subagent spawns are disabled for this account (set channels.matrix.threadBindings.spawnSubagentSessions=true to enable).",
|
||||
error: formatThreadBindingSpawnDisabledError({
|
||||
channel: policy.channel,
|
||||
accountId: policy.accountId,
|
||||
kind: "subagent",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,11 @@ type MatrixThreadBindingsConfig = {
|
||||
enabled?: boolean;
|
||||
idleHours?: number;
|
||||
maxAgeHours?: number;
|
||||
spawnSessions?: boolean;
|
||||
defaultSpawnContext?: "isolated" | "fork";
|
||||
/** @deprecated Use spawnSessions instead. */
|
||||
spawnSubagentSessions?: boolean;
|
||||
/** @deprecated Use spawnSessions instead. */
|
||||
spawnAcpSessions?: boolean;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user