213 lines
7.1 KiB
TypeScript
213 lines
7.1 KiB
TypeScript
import type { OpenClawConfig } from "../config/config.js";
|
|
import { normalizeAccountId } from "../routing/session-key.js";
|
|
|
|
export const DISCORD_THREAD_BINDING_CHANNEL = "discord";
|
|
export const MATRIX_THREAD_BINDING_CHANNEL = "matrix";
|
|
const DEFAULT_THREAD_BINDING_IDLE_HOURS = 24;
|
|
const DEFAULT_THREAD_BINDING_MAX_AGE_HOURS = 0;
|
|
|
|
type SessionThreadBindingsConfigShape = {
|
|
enabled?: unknown;
|
|
idleHours?: unknown;
|
|
maxAgeHours?: unknown;
|
|
spawnSubagentSessions?: unknown;
|
|
spawnAcpSessions?: unknown;
|
|
};
|
|
|
|
type ChannelThreadBindingsContainerShape = {
|
|
threadBindings?: SessionThreadBindingsConfigShape;
|
|
accounts?: Record<string, { threadBindings?: SessionThreadBindingsConfigShape } | undefined>;
|
|
};
|
|
|
|
export type ThreadBindingSpawnKind = "subagent" | "acp";
|
|
|
|
export type ThreadBindingSpawnPolicy = {
|
|
channel: string;
|
|
accountId: string;
|
|
enabled: boolean;
|
|
spawnEnabled: boolean;
|
|
};
|
|
|
|
function normalizeChannelId(value: string | undefined | null): string {
|
|
return String(value ?? "")
|
|
.trim()
|
|
.toLowerCase();
|
|
}
|
|
|
|
function normalizeBoolean(value: unknown): boolean | undefined {
|
|
if (typeof value !== "boolean") {
|
|
return undefined;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function normalizeThreadBindingHours(raw: unknown): number | undefined {
|
|
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
return undefined;
|
|
}
|
|
if (raw < 0) {
|
|
return undefined;
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
export function resolveThreadBindingIdleTimeoutMs(params: {
|
|
channelIdleHoursRaw: unknown;
|
|
sessionIdleHoursRaw: unknown;
|
|
}): number {
|
|
const idleHours =
|
|
normalizeThreadBindingHours(params.channelIdleHoursRaw) ??
|
|
normalizeThreadBindingHours(params.sessionIdleHoursRaw) ??
|
|
DEFAULT_THREAD_BINDING_IDLE_HOURS;
|
|
return Math.floor(idleHours * 60 * 60 * 1000);
|
|
}
|
|
|
|
export function resolveThreadBindingMaxAgeMs(params: {
|
|
channelMaxAgeHoursRaw: unknown;
|
|
sessionMaxAgeHoursRaw: unknown;
|
|
}): number {
|
|
const maxAgeHours =
|
|
normalizeThreadBindingHours(params.channelMaxAgeHoursRaw) ??
|
|
normalizeThreadBindingHours(params.sessionMaxAgeHoursRaw) ??
|
|
DEFAULT_THREAD_BINDING_MAX_AGE_HOURS;
|
|
return Math.floor(maxAgeHours * 60 * 60 * 1000);
|
|
}
|
|
|
|
export function resolveThreadBindingsEnabled(params: {
|
|
channelEnabledRaw: unknown;
|
|
sessionEnabledRaw: unknown;
|
|
}): boolean {
|
|
return (
|
|
normalizeBoolean(params.channelEnabledRaw) ?? normalizeBoolean(params.sessionEnabledRaw) ?? true
|
|
);
|
|
}
|
|
|
|
function resolveChannelThreadBindings(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
accountId: string;
|
|
}): {
|
|
root?: SessionThreadBindingsConfigShape;
|
|
account?: SessionThreadBindingsConfigShape;
|
|
} {
|
|
const channels = params.cfg.channels as Record<string, unknown> | undefined;
|
|
const channelConfig = channels?.[params.channel] as
|
|
| ChannelThreadBindingsContainerShape
|
|
| undefined;
|
|
const accountConfig = channelConfig?.accounts?.[params.accountId];
|
|
return {
|
|
root: channelConfig?.threadBindings,
|
|
account: accountConfig?.threadBindings,
|
|
};
|
|
}
|
|
|
|
function resolveSpawnFlagKey(
|
|
kind: ThreadBindingSpawnKind,
|
|
): "spawnSubagentSessions" | "spawnAcpSessions" {
|
|
return kind === "subagent" ? "spawnSubagentSessions" : "spawnAcpSessions";
|
|
}
|
|
|
|
export function resolveThreadBindingSpawnPolicy(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
accountId?: string;
|
|
kind: ThreadBindingSpawnKind;
|
|
}): ThreadBindingSpawnPolicy {
|
|
const channel = normalizeChannelId(params.channel);
|
|
const accountId = normalizeAccountId(params.accountId);
|
|
const { root, account } = resolveChannelThreadBindings({
|
|
cfg: params.cfg,
|
|
channel,
|
|
accountId,
|
|
});
|
|
const enabled =
|
|
normalizeBoolean(account?.enabled) ??
|
|
normalizeBoolean(root?.enabled) ??
|
|
normalizeBoolean(params.cfg.session?.threadBindings?.enabled) ??
|
|
true;
|
|
const spawnFlagKey = resolveSpawnFlagKey(params.kind);
|
|
const spawnEnabledRaw =
|
|
normalizeBoolean(account?.[spawnFlagKey]) ?? normalizeBoolean(root?.[spawnFlagKey]);
|
|
const spawnEnabled =
|
|
spawnEnabledRaw ??
|
|
(channel !== DISCORD_THREAD_BINDING_CHANNEL && channel !== MATRIX_THREAD_BINDING_CHANNEL);
|
|
return {
|
|
channel,
|
|
accountId,
|
|
enabled,
|
|
spawnEnabled,
|
|
};
|
|
}
|
|
|
|
export function resolveThreadBindingIdleTimeoutMsForChannel(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
accountId?: string;
|
|
}): number {
|
|
const { root, account } = resolveThreadBindingChannelScope(params);
|
|
return resolveThreadBindingIdleTimeoutMs({
|
|
channelIdleHoursRaw: account?.idleHours ?? root?.idleHours,
|
|
sessionIdleHoursRaw: params.cfg.session?.threadBindings?.idleHours,
|
|
});
|
|
}
|
|
|
|
export function resolveThreadBindingMaxAgeMsForChannel(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
accountId?: string;
|
|
}): number {
|
|
const { root, account } = resolveThreadBindingChannelScope(params);
|
|
return resolveThreadBindingMaxAgeMs({
|
|
channelMaxAgeHoursRaw: account?.maxAgeHours ?? root?.maxAgeHours,
|
|
sessionMaxAgeHoursRaw: params.cfg.session?.threadBindings?.maxAgeHours,
|
|
});
|
|
}
|
|
|
|
function resolveThreadBindingChannelScope(params: {
|
|
cfg: OpenClawConfig;
|
|
channel: string;
|
|
accountId?: string;
|
|
}) {
|
|
const channel = normalizeChannelId(params.channel);
|
|
const accountId = normalizeAccountId(params.accountId);
|
|
return resolveChannelThreadBindings({
|
|
cfg: params.cfg,
|
|
channel,
|
|
accountId,
|
|
});
|
|
}
|
|
|
|
export function formatThreadBindingDisabledError(params: {
|
|
channel: string;
|
|
accountId: string;
|
|
kind: ThreadBindingSpawnKind;
|
|
}): string {
|
|
if (params.channel === DISCORD_THREAD_BINDING_CHANNEL) {
|
|
return "Discord thread bindings are disabled (set channels.discord.threadBindings.enabled=true to override for this account, or session.threadBindings.enabled=true globally).";
|
|
}
|
|
if (params.channel === MATRIX_THREAD_BINDING_CHANNEL) {
|
|
return "Matrix thread bindings are disabled (set channels.matrix.threadBindings.enabled=true to override for this account, or session.threadBindings.enabled=true globally).";
|
|
}
|
|
return `Thread bindings are disabled for ${params.channel} (set session.threadBindings.enabled=true to enable).`;
|
|
}
|
|
|
|
export function formatThreadBindingSpawnDisabledError(params: {
|
|
channel: string;
|
|
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).";
|
|
}
|
|
if (params.channel === MATRIX_THREAD_BINDING_CHANNEL && params.kind === "acp") {
|
|
return "Matrix thread-bound ACP spawns are disabled for this account (set channels.matrix.threadBindings.spawnAcpSessions=true to enable).";
|
|
}
|
|
if (params.channel === MATRIX_THREAD_BINDING_CHANNEL && params.kind === "subagent") {
|
|
return "Matrix thread-bound subagent spawns are disabled for this account (set channels.matrix.threadBindings.spawnSubagentSessions=true to enable).";
|
|
}
|
|
return `Thread-bound ${params.kind} spawns are disabled for ${params.channel}.`;
|
|
}
|