mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
matrix-js: require explicit thread-bound spawn config
This commit is contained in:
@@ -242,7 +242,7 @@ Matrix-js supports native Matrix threads for both automatic replies and message-
|
||||
- Inbound threaded messages include the thread root message as extra agent context.
|
||||
- Message-tool sends now auto-inherit the current Matrix thread when the target is the same room, or the same DM user target, unless an explicit `threadId` is provided.
|
||||
- Runtime thread bindings are supported for Matrix-js. `/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`, and thread-bound `/acp spawn` now work in Matrix rooms and DMs.
|
||||
- Top-level Matrix room/DM `/focus` creates a new Matrix thread and binds it to the target session.
|
||||
- Top-level Matrix room/DM `/focus` creates a new Matrix thread and binds it to the target session when `threadBindings.spawnSubagentSessions=true`.
|
||||
- Running `/focus` or `/acp spawn --thread here` inside an existing Matrix thread binds that current thread instead.
|
||||
|
||||
### Thread Binding Config
|
||||
@@ -255,7 +255,10 @@ Matrix-js inherits global defaults from `session.threadBindings`, and also suppo
|
||||
- `threadBindings.spawnSubagentSessions`
|
||||
- `threadBindings.spawnAcpSessions`
|
||||
|
||||
For Matrix-js, spawn flags default to enabled unless you turn them off explicitly.
|
||||
Matrix-js thread-bound spawn flags are opt-in:
|
||||
|
||||
- Set `threadBindings.spawnSubagentSessions: true` to allow top-level `/focus` to create and bind new Matrix threads.
|
||||
- Set `threadBindings.spawnAcpSessions: true` to allow `/acp spawn --thread auto|here` to bind ACP sessions to Matrix threads.
|
||||
|
||||
## Reactions
|
||||
|
||||
|
||||
@@ -651,6 +651,7 @@ Run multiple accounts per channel (each with its own `accountId`):
|
||||
### Other extension channels
|
||||
|
||||
Many extension channels are configured as `channels.<id>` and documented in their dedicated channel pages (for example Feishu, Matrix, LINE, Nostr, Zalo, Nextcloud Talk, Synology Chat, and Twitch).
|
||||
Matrix-js also supports top-level `bindings[]` entries with `type: "acp"` for persistent ACP bindings. Use the Matrix room id or Matrix thread root event id in `match.peer.id`.
|
||||
See the full channel index: [Channels](/channels).
|
||||
|
||||
### Group chat mention gating
|
||||
@@ -1358,7 +1359,7 @@ Run multiple isolated agents inside one Gateway. See [Multi-Agent](/concepts/mul
|
||||
|
||||
Within each tier, the first matching `bindings` entry wins.
|
||||
|
||||
For `type: "acp"` entries, OpenClaw resolves by exact conversation identity (`match.channel` + account + `match.peer.id`) and does not use the route binding tier order above.
|
||||
For `type: "acp"` entries, OpenClaw resolves by exact conversation identity (`match.channel` + account + `match.peer.id`) and does not use the route binding tier order above. For example, use a Discord channel/thread id, a Matrix room id or thread root event id, or a Telegram canonical topic id.
|
||||
|
||||
### Per-agent access profiles
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ Required feature flags for thread-bound ACP:
|
||||
- `acp.dispatch.enabled` is on by default (set `false` to pause ACP dispatch)
|
||||
- Channel-adapter ACP thread-spawn flag enabled (adapter-specific)
|
||||
- Discord: `channels.discord.threadBindings.spawnAcpSessions=true`
|
||||
- Matrix-js: `channels["matrix-js"].threadBindings.spawnAcpSessions=true`
|
||||
- Telegram: `channels.telegram.threadBindings.spawnAcpSessions=true`
|
||||
|
||||
### Thread supporting channels
|
||||
@@ -86,6 +87,7 @@ Required feature flags for thread-bound ACP:
|
||||
- Any channel adapter that exposes session/thread binding capability.
|
||||
- Current built-in support:
|
||||
- Discord threads/channels
|
||||
- Matrix-js room threads and DMs
|
||||
- Telegram topics (forum topics in groups/supergroups and DM topics)
|
||||
- Plugin channels can add support through the same binding interface.
|
||||
|
||||
@@ -98,6 +100,7 @@ For non-ephemeral workflows, configure persistent ACP bindings in top-level `bin
|
||||
- `bindings[].type="acp"` marks a persistent ACP conversation binding.
|
||||
- `bindings[].match` identifies the target conversation:
|
||||
- Discord channel or thread: `match.channel="discord"` + `match.peer.id="<channelOrThreadId>"`
|
||||
- Matrix room or thread: `match.channel="matrix-js"` + `match.peer.id="<roomIdOrThreadRootEventId>"`
|
||||
- Telegram forum topic: `match.channel="telegram"` + `match.peer.id="<chatId>:topic:<topicId>"`
|
||||
- `bindings[].agentId` is the owning OpenClaw agent id.
|
||||
- Optional ACP overrides live under `bindings[].acp`:
|
||||
@@ -375,6 +378,7 @@ Notes:
|
||||
- On non-thread binding surfaces, default behavior is effectively `off`.
|
||||
- Thread-bound spawn requires channel policy support:
|
||||
- Discord: `channels.discord.threadBindings.spawnAcpSessions=true`
|
||||
- Matrix-js: `channels["matrix-js"].threadBindings.spawnAcpSessions=true`
|
||||
- Telegram: `channels.telegram.threadBindings.spawnAcpSessions=true`
|
||||
|
||||
## ACP controls
|
||||
@@ -475,7 +479,7 @@ Core ACP baseline:
|
||||
}
|
||||
```
|
||||
|
||||
Thread binding config is channel-adapter specific. Example for Discord:
|
||||
Thread binding config is channel-adapter specific. Example for Discord and Matrix-js:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -493,6 +497,12 @@ Thread binding config is channel-adapter specific. Example for Discord:
|
||||
spawnAcpSessions: true,
|
||||
},
|
||||
},
|
||||
"matrix-js": {
|
||||
threadBindings: {
|
||||
enabled: true,
|
||||
spawnAcpSessions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -500,6 +510,7 @@ Thread binding config is channel-adapter specific. Example for Discord:
|
||||
If thread-bound ACP spawn does not work, verify the adapter feature flag first:
|
||||
|
||||
- Discord: `channels.discord.threadBindings.spawnAcpSessions=true`
|
||||
- Matrix-js: `channels["matrix-js"].threadBindings.spawnAcpSessions=true`
|
||||
|
||||
See [Configuration Reference](/gateway/configuration-reference).
|
||||
|
||||
|
||||
@@ -99,7 +99,11 @@ When thread bindings are enabled for a channel, a sub-agent can stay bound to a
|
||||
|
||||
### Thread supporting channels
|
||||
|
||||
- Discord (currently the only supported channel): supports persistent thread-bound subagent sessions (`sessions_spawn` with `thread: true`), manual thread controls (`/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`), and adapter keys `channels.discord.threadBindings.enabled`, `channels.discord.threadBindings.idleHours`, `channels.discord.threadBindings.maxAgeHours`, and `channels.discord.threadBindings.spawnSubagentSessions`.
|
||||
- `sessions_spawn` with `thread: true`: currently supported on Discord only.
|
||||
- Manual thread/conversation controls:
|
||||
- Discord: `/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`
|
||||
- Matrix-js: `/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`
|
||||
- Telegram: `/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`
|
||||
|
||||
Quick flow:
|
||||
|
||||
|
||||
@@ -963,6 +963,18 @@ describe("spawnAcpDirect", () => {
|
||||
});
|
||||
|
||||
it("keeps inline delivery for thread-bound ACP session mode", async () => {
|
||||
hoisted.state.cfg = {
|
||||
...hoisted.state.cfg,
|
||||
channels: {
|
||||
...hoisted.state.cfg.channels,
|
||||
telegram: {
|
||||
threadBindings: {
|
||||
spawnAcpSessions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await spawnAcpDirect(
|
||||
{
|
||||
task: "Investigate flaky tests",
|
||||
|
||||
@@ -243,9 +243,10 @@ function createSessionBindingCapabilities() {
|
||||
type AcpBindInput = {
|
||||
targetSessionKey: string;
|
||||
conversation: {
|
||||
channel?: "discord" | "telegram";
|
||||
channel?: "discord" | "matrix-js" | "telegram";
|
||||
accountId: string;
|
||||
conversationId: string;
|
||||
parentConversationId?: string;
|
||||
};
|
||||
placement: "current" | "child";
|
||||
metadata?: Record<string, unknown>;
|
||||
@@ -266,11 +267,18 @@ function createAcpThreadBinding(input: AcpBindInput): FakeBinding {
|
||||
conversationId: nextConversationId,
|
||||
parentConversationId: "parent-1",
|
||||
}
|
||||
: {
|
||||
channel: "telegram",
|
||||
accountId: input.conversation.accountId,
|
||||
conversationId: nextConversationId,
|
||||
},
|
||||
: channel === "matrix-js"
|
||||
? {
|
||||
channel: "matrix-js",
|
||||
accountId: input.conversation.accountId,
|
||||
conversationId: nextConversationId,
|
||||
parentConversationId: input.conversation.parentConversationId ?? "!room:example",
|
||||
}
|
||||
: {
|
||||
channel: "telegram",
|
||||
accountId: input.conversation.accountId,
|
||||
conversationId: nextConversationId,
|
||||
},
|
||||
metadata: { boundBy, webhookId: "wh-1" },
|
||||
});
|
||||
}
|
||||
@@ -334,6 +342,24 @@ function createTelegramDmParams(commandBody: string, cfg: OpenClawConfig = baseC
|
||||
return params;
|
||||
}
|
||||
|
||||
function createMatrixRoomParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
const params = buildCommandTestParams(commandBody, cfg, {
|
||||
Provider: "matrix-js",
|
||||
Surface: "matrix-js",
|
||||
OriginatingChannel: "matrix-js",
|
||||
OriginatingTo: "room:!room:example",
|
||||
AccountId: "default",
|
||||
});
|
||||
params.command.senderId = "user-1";
|
||||
return params;
|
||||
}
|
||||
|
||||
function createMatrixThreadParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
const params = createMatrixRoomParams(commandBody, cfg);
|
||||
params.ctx.MessageThreadId = "$thread-42";
|
||||
return params;
|
||||
}
|
||||
|
||||
async function runDiscordAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(createDiscordParams(commandBody, cfg), true);
|
||||
}
|
||||
@@ -350,6 +376,14 @@ async function runTelegramDmAcpCommand(commandBody: string, cfg: OpenClawConfig
|
||||
return handleAcpCommand(createTelegramDmParams(commandBody, cfg), true);
|
||||
}
|
||||
|
||||
async function runMatrixRoomAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(createMatrixRoomParams(commandBody, cfg), true);
|
||||
}
|
||||
|
||||
async function runMatrixThreadAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(createMatrixThreadParams(commandBody, cfg), true);
|
||||
}
|
||||
|
||||
describe("/acp command", () => {
|
||||
beforeEach(() => {
|
||||
acpManagerTesting.resetAcpSessionManagerForTests();
|
||||
@@ -518,7 +552,18 @@ describe("/acp command", () => {
|
||||
});
|
||||
|
||||
it("binds Telegram topic ACP spawns to full conversation ids", async () => {
|
||||
const result = await runTelegramAcpCommand("/acp spawn codex --thread here");
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
channels: {
|
||||
...baseCfg.channels,
|
||||
telegram: {
|
||||
threadBindings: {
|
||||
spawnAcpSessions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
const result = await runTelegramAcpCommand("/acp spawn codex --thread here", cfg);
|
||||
|
||||
expect(result?.reply?.text).toContain("Spawned ACP session agent:codex:acp:");
|
||||
expect(result?.reply?.text).toContain("Bound this conversation to");
|
||||
@@ -536,7 +581,18 @@ describe("/acp command", () => {
|
||||
});
|
||||
|
||||
it("binds Telegram DM ACP spawns to the DM conversation id", async () => {
|
||||
const result = await runTelegramDmAcpCommand("/acp spawn codex --thread here");
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
channels: {
|
||||
...baseCfg.channels,
|
||||
telegram: {
|
||||
threadBindings: {
|
||||
spawnAcpSessions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
const result = await runTelegramDmAcpCommand("/acp spawn codex --thread here", cfg);
|
||||
|
||||
expect(result?.reply?.text).toContain("Spawned ACP session agent:codex:acp:");
|
||||
expect(result?.reply?.text).toContain("Bound this conversation to");
|
||||
@@ -592,6 +648,47 @@ describe("/acp command", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects Matrix thread-bound ACP spawn when spawnAcpSessions is not enabled", async () => {
|
||||
const result = await runMatrixRoomAcpCommand("/acp spawn codex --thread auto");
|
||||
|
||||
expect(result?.reply?.text).toContain(
|
||||
"channels.matrix-js.threadBindings.spawnAcpSessions=true",
|
||||
);
|
||||
expect(hoisted.closeMock).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.sessionBindingBindMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("binds Matrix thread-bound ACP spawns when enabled explicitly", async () => {
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
channels: {
|
||||
...baseCfg.channels,
|
||||
"matrix-js": {
|
||||
threadBindings: {
|
||||
enabled: true,
|
||||
spawnAcpSessions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const result = await runMatrixThreadAcpCommand("/acp spawn codex --thread here", cfg);
|
||||
|
||||
expect(result?.reply?.text).toContain("Spawned ACP session agent:codex:acp:");
|
||||
expect(result?.reply?.text).toContain("Bound this thread to");
|
||||
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
placement: "current",
|
||||
conversation: expect.objectContaining({
|
||||
channel: "matrix-js",
|
||||
accountId: "default",
|
||||
conversationId: "$thread-42",
|
||||
parentConversationId: "!room:example",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forbids /acp spawn from sandboxed requester sessions", async () => {
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
|
||||
@@ -157,6 +157,9 @@ async function bindSpawnedAcpSessionToThread(params: {
|
||||
channel: spawnPolicy.channel,
|
||||
accountId: spawnPolicy.accountId,
|
||||
conversationId: currentConversationId,
|
||||
...(bindingContext.parentConversationId
|
||||
? { parentConversationId: bindingContext.parentConversationId }
|
||||
: {}),
|
||||
});
|
||||
const boundBy =
|
||||
typeof existingBinding?.metadata?.boundBy === "string"
|
||||
@@ -181,6 +184,9 @@ async function bindSpawnedAcpSessionToThread(params: {
|
||||
channel: spawnPolicy.channel,
|
||||
accountId: spawnPolicy.accountId,
|
||||
conversationId,
|
||||
...(bindingContext.parentConversationId
|
||||
? { parentConversationId: bindingContext.parentConversationId }
|
||||
: {}),
|
||||
},
|
||||
placement,
|
||||
metadata: {
|
||||
|
||||
@@ -105,8 +105,8 @@ function createTelegramTopicCommandParams(commandBody: string) {
|
||||
return params;
|
||||
}
|
||||
|
||||
function createMatrixCommandParams(commandBody: string) {
|
||||
const params = buildCommandTestParams(commandBody, baseCfg, {
|
||||
function createMatrixCommandParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
const params = buildCommandTestParams(commandBody, cfg, {
|
||||
Provider: "matrix-js",
|
||||
Surface: "matrix-js",
|
||||
OriginatingChannel: "matrix-js",
|
||||
@@ -236,7 +236,17 @@ describe("/focus, /unfocus, /agents", () => {
|
||||
});
|
||||
|
||||
it("/focus creates Matrix child thread bindings from top-level rooms", async () => {
|
||||
const result = await focusCodexAcp(createMatrixCommandParams("/focus codex-acp"));
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
channels: {
|
||||
"matrix-js": {
|
||||
threadBindings: {
|
||||
spawnAcpSessions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
const result = await focusCodexAcp(createMatrixCommandParams("/focus codex-acp", cfg));
|
||||
|
||||
expect(result?.reply?.text).toContain("created thread");
|
||||
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
|
||||
@@ -251,6 +261,15 @@ describe("/focus, /unfocus, /agents", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("/focus rejects Matrix child thread creation when spawn config is not enabled", async () => {
|
||||
const result = await focusCodexAcp(createMatrixCommandParams("/focus codex-acp"));
|
||||
|
||||
expect(result?.reply?.text).toContain(
|
||||
"channels.matrix-js.threadBindings.spawnAcpSessions=true",
|
||||
);
|
||||
expect(hoisted.sessionBindingBindMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("/focus includes ACP session identifiers in intro text when available", async () => {
|
||||
hoisted.readAcpSessionEntryMock.mockReturnValue({
|
||||
sessionKey: "agent:codex-acp:session-1",
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
resolveThreadBindingThreadName,
|
||||
} from "../../../channels/thread-bindings-messages.js";
|
||||
import {
|
||||
formatThreadBindingSpawnDisabledError,
|
||||
resolveThreadBindingIdleTimeoutMsForChannel,
|
||||
resolveThreadBindingMaxAgeMsForChannel,
|
||||
resolveThreadBindingSpawnPolicy,
|
||||
} from "../../../channels/thread-bindings-policy.js";
|
||||
import { getSessionBindingService } from "../../../infra/outbound/session-binding-service.js";
|
||||
import type { CommandHandlerResult } from "../commands-types.js";
|
||||
@@ -183,6 +185,23 @@ export async function handleSubagentsFocusAction(
|
||||
if (!capabilities.placements.includes(bindingContext.placement)) {
|
||||
return stopWithText(`⚠️ ${channel} bindings are unavailable for this account.`);
|
||||
}
|
||||
if (bindingContext.channel === "matrix-js" && bindingContext.placement === "child") {
|
||||
const spawnPolicy = resolveThreadBindingSpawnPolicy({
|
||||
cfg: params.cfg,
|
||||
channel: bindingContext.channel,
|
||||
accountId,
|
||||
kind: focusTarget.targetKind === "acp" ? "acp" : "subagent",
|
||||
});
|
||||
if (!spawnPolicy.spawnEnabled) {
|
||||
return stopWithText(
|
||||
`⚠️ ${formatThreadBindingSpawnDisabledError({
|
||||
channel: spawnPolicy.channel,
|
||||
accountId: spawnPolicy.accountId,
|
||||
kind: focusTarget.targetKind === "acp" ? "acp" : "subagent",
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let binding;
|
||||
try {
|
||||
|
||||
62
src/channels/thread-bindings-policy.test.ts
Normal file
62
src/channels/thread-bindings-policy.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveThreadBindingSpawnPolicy } from "./thread-bindings-policy.js";
|
||||
|
||||
const baseCfg = {
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
describe("resolveThreadBindingSpawnPolicy", () => {
|
||||
it("defaults thread-bound spawns to opt-in across supported channels", () => {
|
||||
expect(
|
||||
resolveThreadBindingSpawnPolicy({
|
||||
cfg: baseCfg,
|
||||
channel: "discord",
|
||||
kind: "subagent",
|
||||
}).spawnEnabled,
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolveThreadBindingSpawnPolicy({
|
||||
cfg: baseCfg,
|
||||
channel: "matrix-js",
|
||||
kind: "subagent",
|
||||
}).spawnEnabled,
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolveThreadBindingSpawnPolicy({
|
||||
cfg: baseCfg,
|
||||
channel: "telegram",
|
||||
kind: "acp",
|
||||
}).spawnEnabled,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("honors explicit per-channel spawn flags", () => {
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
channels: {
|
||||
"matrix-js": {
|
||||
threadBindings: {
|
||||
spawnSubagentSessions: true,
|
||||
spawnAcpSessions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveThreadBindingSpawnPolicy({
|
||||
cfg,
|
||||
channel: "matrix-js",
|
||||
kind: "subagent",
|
||||
}).spawnEnabled,
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolveThreadBindingSpawnPolicy({
|
||||
cfg,
|
||||
channel: "matrix-js",
|
||||
kind: "acp",
|
||||
}).spawnEnabled,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,8 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
|
||||
export const DISCORD_THREAD_BINDING_CHANNEL = "discord";
|
||||
export const MATRIX_JS_THREAD_BINDING_CHANNEL = "matrix-js";
|
||||
export const TELEGRAM_THREAD_BINDING_CHANNEL = "telegram";
|
||||
const DEFAULT_THREAD_BINDING_IDLE_HOURS = 24;
|
||||
const DEFAULT_THREAD_BINDING_MAX_AGE_HOURS = 0;
|
||||
|
||||
@@ -106,6 +108,22 @@ function resolveSpawnFlagKey(
|
||||
return kind === "subagent" ? "spawnSubagentSessions" : "spawnAcpSessions";
|
||||
}
|
||||
|
||||
function resolveSpawnConfigPath(params: {
|
||||
channel: string;
|
||||
kind: ThreadBindingSpawnKind;
|
||||
}): string | undefined {
|
||||
const suffix =
|
||||
params.kind === "subagent" ? "spawnSubagentSessions=true" : "spawnAcpSessions=true";
|
||||
if (
|
||||
params.channel === DISCORD_THREAD_BINDING_CHANNEL ||
|
||||
params.channel === MATRIX_JS_THREAD_BINDING_CHANNEL ||
|
||||
params.channel === TELEGRAM_THREAD_BINDING_CHANNEL
|
||||
) {
|
||||
return `channels.${params.channel}.threadBindings.${suffix}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveThreadBindingSpawnPolicy(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
@@ -127,8 +145,7 @@ export function resolveThreadBindingSpawnPolicy(params: {
|
||||
const spawnFlagKey = resolveSpawnFlagKey(params.kind);
|
||||
const spawnEnabledRaw =
|
||||
normalizeBoolean(account?.[spawnFlagKey]) ?? normalizeBoolean(root?.[spawnFlagKey]);
|
||||
// Non-Discord channels currently have no dedicated spawn gate config keys.
|
||||
const spawnEnabled = spawnEnabledRaw ?? channel !== DISCORD_THREAD_BINDING_CHANNEL;
|
||||
const spawnEnabled = spawnEnabledRaw ?? false;
|
||||
return {
|
||||
channel,
|
||||
accountId,
|
||||
@@ -191,11 +208,21 @@ export function formatThreadBindingSpawnDisabledError(params: {
|
||||
accountId: string;
|
||||
kind: ThreadBindingSpawnKind;
|
||||
}): string {
|
||||
if (params.channel === DISCORD_THREAD_BINDING_CHANNEL && params.kind === "acp") {
|
||||
return "Discord thread-bound ACP spawns are disabled for this account (set channels.discord.threadBindings.spawnAcpSessions=true to enable).";
|
||||
}
|
||||
if (params.channel === DISCORD_THREAD_BINDING_CHANNEL && params.kind === "subagent") {
|
||||
return "Discord thread-bound subagent spawns are disabled for this account (set channels.discord.threadBindings.spawnSubagentSessions=true to enable).";
|
||||
const configPath = resolveSpawnConfigPath({
|
||||
channel: params.channel,
|
||||
kind: params.kind,
|
||||
});
|
||||
const label =
|
||||
params.channel === DISCORD_THREAD_BINDING_CHANNEL
|
||||
? "Discord"
|
||||
: params.channel === MATRIX_JS_THREAD_BINDING_CHANNEL
|
||||
? "Matrix-js"
|
||||
: params.channel === TELEGRAM_THREAD_BINDING_CHANNEL
|
||||
? "Telegram"
|
||||
: params.channel;
|
||||
if (configPath) {
|
||||
const noun = params.kind === "acp" ? "ACP" : "subagent";
|
||||
return `${label} thread-bound ${noun} spawns are disabled for this account (set ${configPath} to enable).`;
|
||||
}
|
||||
return `Thread-bound ${params.kind} spawns are disabled for ${params.channel}.`;
|
||||
}
|
||||
|
||||
@@ -1550,9 +1550,9 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"channels.matrix-js.threadBindings.maxAgeHours":
|
||||
"Optional hard max age in hours for Matrix-js thread-bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set.",
|
||||
"channels.matrix-js.threadBindings.spawnSubagentSessions":
|
||||
"Allow subagent spawns/focus flows to auto-create and bind Matrix threads when starting from a top-level Matrix room or DM.",
|
||||
"Allow top-level /focus flows to auto-create and bind Matrix threads for subagent/session targets (default: false; opt-in). Set true to enable Matrix thread creation/binding from room or DM contexts.",
|
||||
"channels.matrix-js.threadBindings.spawnAcpSessions":
|
||||
"Allow /acp spawn to auto-create and bind Matrix threads for ACP sessions when starting from a top-level Matrix room or DM.",
|
||||
"Allow /acp spawn to create or bind Matrix threads for ACP sessions (default: false; opt-in). Set true to enable thread-bound ACP spawns for this account/channel.",
|
||||
"channels.discord.ui.components.accentColor":
|
||||
"Accent color for Discord component containers (hex). Set per account via channels.discord.accounts.<id>.ui.components.accentColor.",
|
||||
"channels.discord.voice.enabled":
|
||||
|
||||
Reference in New Issue
Block a user