mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 18:14:31 +00:00
Discord: thread bindings idle + max-age lifecycle (#27845) (thanks @osolmaz)
* refactor discord thread bindings to idle and max-age lifecycle * fix: migrate legacy thread binding expiry and reduce hot-path disk writes * refactor: remove remaining thread-binding ttl legacy paths * fix: harden thread-binding lifecycle persistence * Discord: fix thread binding types in message/reply paths * Infra: handle win32 unknown inode in file identity checks * Infra: relax win32 guarded-open identity checks * Config: migrate threadBindings ttlHours to idleHours * Revert "Infra: relax win32 guarded-open identity checks" This reverts commitde94126771. * Revert "Infra: handle win32 unknown inode in file identity checks" This reverts commit96fc5ddfb3. * Discord: re-read live binding state before sweep unbind * fix: add changelog note for thread binding lifecycle update (#27845) (thanks @osolmaz) --------- Co-authored-by: Onur Solmaz <onur@textcortex.com>
This commit is contained in:
@@ -3,25 +3,25 @@ import { prefixSystemMessage } from "../infra/system-message.js";
|
||||
const DEFAULT_THREAD_BINDING_FAREWELL_TEXT =
|
||||
"Session ended. Messages here will no longer be routed.";
|
||||
|
||||
function normalizeThreadBindingMessageTtlMs(raw: unknown): number {
|
||||
function normalizeThreadBindingDurationMs(raw: unknown): number {
|
||||
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
||||
return 0;
|
||||
}
|
||||
const ttlMs = Math.floor(raw);
|
||||
if (ttlMs < 0) {
|
||||
const durationMs = Math.floor(raw);
|
||||
if (durationMs < 0) {
|
||||
return 0;
|
||||
}
|
||||
return ttlMs;
|
||||
return durationMs;
|
||||
}
|
||||
|
||||
export function formatThreadBindingTtlLabel(ttlMs: number): string {
|
||||
if (ttlMs <= 0) {
|
||||
export function formatThreadBindingDurationLabel(durationMs: number): string {
|
||||
if (durationMs <= 0) {
|
||||
return "disabled";
|
||||
}
|
||||
if (ttlMs < 60_000) {
|
||||
if (durationMs < 60_000) {
|
||||
return "<1m";
|
||||
}
|
||||
const totalMinutes = Math.floor(ttlMs / 60_000);
|
||||
const totalMinutes = Math.floor(durationMs / 60_000);
|
||||
if (totalMinutes % 60 === 0) {
|
||||
return `${Math.floor(totalMinutes / 60)}h`;
|
||||
}
|
||||
@@ -41,14 +41,16 @@ export function resolveThreadBindingThreadName(params: {
|
||||
export function resolveThreadBindingIntroText(params: {
|
||||
agentId?: string;
|
||||
label?: string;
|
||||
sessionTtlMs?: number;
|
||||
idleTimeoutMs?: number;
|
||||
maxAgeMs?: number;
|
||||
sessionCwd?: string;
|
||||
sessionDetails?: string[];
|
||||
}): string {
|
||||
const label = params.label?.trim();
|
||||
const base = label || params.agentId?.trim() || "agent";
|
||||
const normalized = base.replace(/\s+/g, " ").trim().slice(0, 100) || "agent";
|
||||
const ttlMs = normalizeThreadBindingMessageTtlMs(params.sessionTtlMs);
|
||||
const idleTimeoutMs = normalizeThreadBindingDurationMs(params.idleTimeoutMs);
|
||||
const maxAgeMs = normalizeThreadBindingDurationMs(params.maxAgeMs);
|
||||
const cwd = params.sessionCwd?.trim();
|
||||
const details = (params.sessionDetails ?? [])
|
||||
.map((entry) => entry.trim())
|
||||
@@ -56,10 +58,22 @@ export function resolveThreadBindingIntroText(params: {
|
||||
if (cwd) {
|
||||
details.unshift(`cwd: ${cwd}`);
|
||||
}
|
||||
|
||||
const lifecycle: string[] = [];
|
||||
if (idleTimeoutMs > 0) {
|
||||
lifecycle.push(
|
||||
`idle auto-unfocus after ${formatThreadBindingDurationLabel(idleTimeoutMs)} inactivity`,
|
||||
);
|
||||
}
|
||||
if (maxAgeMs > 0) {
|
||||
lifecycle.push(`max age ${formatThreadBindingDurationLabel(maxAgeMs)}`);
|
||||
}
|
||||
|
||||
const intro =
|
||||
ttlMs > 0
|
||||
? `${normalized} session active (auto-unfocus in ${formatThreadBindingTtlLabel(ttlMs)}). Messages here go directly to this session.`
|
||||
lifecycle.length > 0
|
||||
? `${normalized} session active (${lifecycle.join("; ")}). Messages here go directly to this session.`
|
||||
: `${normalized} session active. Messages here go directly to this session.`;
|
||||
|
||||
if (details.length === 0) {
|
||||
return prefixSystemMessage(intro);
|
||||
}
|
||||
@@ -69,16 +83,31 @@ export function resolveThreadBindingIntroText(params: {
|
||||
export function resolveThreadBindingFarewellText(params: {
|
||||
reason?: string;
|
||||
farewellText?: string;
|
||||
sessionTtlMs: number;
|
||||
idleTimeoutMs: number;
|
||||
maxAgeMs: number;
|
||||
}): string {
|
||||
const custom = params.farewellText?.trim();
|
||||
if (custom) {
|
||||
return prefixSystemMessage(custom);
|
||||
}
|
||||
if (params.reason === "ttl-expired") {
|
||||
|
||||
if (params.reason === "idle-expired") {
|
||||
const label = formatThreadBindingDurationLabel(
|
||||
normalizeThreadBindingDurationMs(params.idleTimeoutMs),
|
||||
);
|
||||
return prefixSystemMessage(
|
||||
`Session ended automatically after ${formatThreadBindingTtlLabel(params.sessionTtlMs)}. Messages here will no longer be routed.`,
|
||||
`Session ended automatically after ${label} of inactivity. Messages here will no longer be routed.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (params.reason === "max-age-expired") {
|
||||
const label = formatThreadBindingDurationLabel(
|
||||
normalizeThreadBindingDurationMs(params.maxAgeMs),
|
||||
);
|
||||
return prefixSystemMessage(
|
||||
`Session ended automatically at max age of ${label}. Messages here will no longer be routed.`,
|
||||
);
|
||||
}
|
||||
|
||||
return prefixSystemMessage(DEFAULT_THREAD_BINDING_FAREWELL_TEXT);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
|
||||
export const DISCORD_THREAD_BINDING_CHANNEL = "discord";
|
||||
const DEFAULT_THREAD_BINDING_TTL_HOURS = 24;
|
||||
const DEFAULT_THREAD_BINDING_IDLE_HOURS = 24;
|
||||
const DEFAULT_THREAD_BINDING_MAX_AGE_HOURS = 0;
|
||||
|
||||
type SessionThreadBindingsConfigShape = {
|
||||
enabled?: unknown;
|
||||
ttlHours?: unknown;
|
||||
idleHours?: unknown;
|
||||
maxAgeHours?: unknown;
|
||||
spawnSubagentSessions?: unknown;
|
||||
spawnAcpSessions?: unknown;
|
||||
};
|
||||
@@ -38,7 +40,7 @@ function normalizeBoolean(value: unknown): boolean | undefined {
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeThreadBindingTtlHours(raw: unknown): number | undefined {
|
||||
function normalizeThreadBindingHours(raw: unknown): number | undefined {
|
||||
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -48,15 +50,26 @@ function normalizeThreadBindingTtlHours(raw: unknown): number | undefined {
|
||||
return raw;
|
||||
}
|
||||
|
||||
export function resolveThreadBindingSessionTtlMs(params: {
|
||||
channelTtlHoursRaw: unknown;
|
||||
sessionTtlHoursRaw: unknown;
|
||||
export function resolveThreadBindingIdleTimeoutMs(params: {
|
||||
channelIdleHoursRaw: unknown;
|
||||
sessionIdleHoursRaw: unknown;
|
||||
}): number {
|
||||
const ttlHours =
|
||||
normalizeThreadBindingTtlHours(params.channelTtlHoursRaw) ??
|
||||
normalizeThreadBindingTtlHours(params.sessionTtlHoursRaw) ??
|
||||
DEFAULT_THREAD_BINDING_TTL_HOURS;
|
||||
return Math.floor(ttlHours * 60 * 60 * 1000);
|
||||
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: {
|
||||
@@ -124,7 +137,7 @@ export function resolveThreadBindingSpawnPolicy(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveThreadBindingSessionTtlMsForChannel(params: {
|
||||
export function resolveThreadBindingIdleTimeoutMsForChannel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
accountId?: string;
|
||||
@@ -136,9 +149,27 @@ export function resolveThreadBindingSessionTtlMsForChannel(params: {
|
||||
channel,
|
||||
accountId,
|
||||
});
|
||||
return resolveThreadBindingSessionTtlMs({
|
||||
channelTtlHoursRaw: account?.ttlHours ?? root?.ttlHours,
|
||||
sessionTtlHoursRaw: params.cfg.session?.threadBindings?.ttlHours,
|
||||
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 channel = normalizeChannelId(params.channel);
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const { root, account } = resolveChannelThreadBindings({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
accountId,
|
||||
});
|
||||
return resolveThreadBindingMaxAgeMs({
|
||||
channelMaxAgeHoursRaw: account?.maxAgeHours ?? root?.maxAgeHours,
|
||||
sessionMaxAgeHoursRaw: params.cfg.session?.threadBindings?.maxAgeHours,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user