refactor: move session lifecycle and outbound fallbacks into plugins

This commit is contained in:
Peter Steinberger
2026-03-16 00:40:32 -07:00
parent 49251def61
commit 74d0c39b32
13 changed files with 114 additions and 243 deletions

View File

@@ -1,6 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
const hoisted = vi.hoisted(() => {
const getThreadBindingManagerMock = vi.fn();
@@ -19,28 +22,34 @@ const hoisted = vi.hoisted(() => {
};
});
vi.mock("../../../extensions/discord/src/monitor/thread-bindings.js", async (importOriginal) => {
const actual =
await importOriginal<
typeof import("../../../extensions/discord/src/monitor/thread-bindings.js")
>();
vi.mock("../../plugins/runtime/index.js", async () => {
const discordThreadBindings = await vi.importActual<
typeof import("../../../extensions/discord/src/monitor/thread-bindings.js")
>("../../../extensions/discord/src/monitor/thread-bindings.js");
return {
...actual,
getThreadBindingManager: hoisted.getThreadBindingManagerMock,
setThreadBindingIdleTimeoutBySessionKey: hoisted.setThreadBindingIdleTimeoutBySessionKeyMock,
setThreadBindingMaxAgeBySessionKey: hoisted.setThreadBindingMaxAgeBySessionKeyMock,
};
});
vi.mock("../../../extensions/telegram/src/thread-bindings.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../../extensions/telegram/src/thread-bindings.js")>();
return {
...actual,
setTelegramThreadBindingIdleTimeoutBySessionKey:
hoisted.setTelegramThreadBindingIdleTimeoutBySessionKeyMock,
setTelegramThreadBindingMaxAgeBySessionKey:
hoisted.setTelegramThreadBindingMaxAgeBySessionKeyMock,
createPluginRuntime: () => ({
channel: {
discord: {
threadBindings: {
getManager: hoisted.getThreadBindingManagerMock,
resolveIdleTimeoutMs: discordThreadBindings.resolveThreadBindingIdleTimeoutMs,
resolveInactivityExpiresAt:
discordThreadBindings.resolveThreadBindingInactivityExpiresAt,
resolveMaxAgeMs: discordThreadBindings.resolveThreadBindingMaxAgeMs,
resolveMaxAgeExpiresAt: discordThreadBindings.resolveThreadBindingMaxAgeExpiresAt,
setIdleTimeoutBySessionKey: hoisted.setThreadBindingIdleTimeoutBySessionKeyMock,
setMaxAgeBySessionKey: hoisted.setThreadBindingMaxAgeBySessionKeyMock,
unbindBySessionKey: vi.fn(),
},
},
telegram: {
threadBindings: {
setIdleTimeoutBySessionKey: hoisted.setTelegramThreadBindingIdleTimeoutBySessionKeyMock,
setMaxAgeBySessionKey: hoisted.setTelegramThreadBindingMaxAgeBySessionKeyMock,
},
},
},
}),
};
});
@@ -168,6 +177,9 @@ function createFakeThreadBindingManager(binding: FakeBinding | null) {
describe("/session idle and /session max-age", () => {
beforeEach(() => {
setActivePluginRegistry(
createTestRegistry([{ pluginId: "telegram", source: "test", plugin: telegramPlugin }]),
);
hoisted.getThreadBindingManagerMock.mockReset();
hoisted.setThreadBindingIdleTimeoutBySessionKeyMock.mockReset();
hoisted.setThreadBindingMaxAgeBySessionKeyMock.mockReset();

View File

@@ -1,18 +1,5 @@
import {
formatThreadBindingDurationLabel,
getThreadBindingManager,
resolveThreadBindingIdleTimeoutMs,
resolveThreadBindingInactivityExpiresAt,
resolveThreadBindingMaxAgeExpiresAt,
resolveThreadBindingMaxAgeMs,
setThreadBindingIdleTimeoutBySessionKey,
setThreadBindingMaxAgeBySessionKey,
} from "../../../extensions/discord/src/monitor/thread-bindings.js";
import {
setTelegramThreadBindingIdleTimeoutBySessionKey,
setTelegramThreadBindingMaxAgeBySessionKey,
} from "../../../extensions/telegram/src/thread-bindings.js";
import { resolveFastModeState } from "../../agents/fast-mode.js";
import { formatThreadBindingDurationLabel } from "../../channels/thread-bindings-messages.js";
import { parseDurationMs } from "../../cli/parse-duration.js";
import { isRestartEnabled } from "../../config/commands.js";
import { logVerbose } from "../../globals.js";
@@ -20,6 +7,7 @@ import { getSessionBindingService } from "../../infra/outbound/session-binding-s
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
import { scheduleGatewaySigusr1Restart, triggerOpenClawRestart } from "../../infra/restart.js";
import { loadCostUsageSummary, loadSessionCostSummary } from "../../infra/session-cost-usage.js";
import { createPluginRuntime } from "../../plugins/runtime/index.js";
import { formatTokenCount, formatUsd } from "../../utils/usage-format.js";
import { parseActivationCommand } from "../group-activation.js";
import { parseSendPolicyCommand } from "../send-policy.js";
@@ -34,6 +22,7 @@ const SESSION_COMMAND_PREFIX = "/session";
const SESSION_DURATION_OFF_VALUES = new Set(["off", "disable", "disabled", "none", "0"]);
const SESSION_ACTION_IDLE = "idle";
const SESSION_ACTION_MAX_AGE = "max-age";
const channelRuntime = createPluginRuntime().channel;
function resolveSessionCommandUsage() {
return "Usage: /session idle <duration|off> | /session max-age <duration|off> (example: /session idle 24h)";
@@ -385,7 +374,9 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
params.ctx.MessageThreadId != null ? String(params.ctx.MessageThreadId).trim() : "";
const telegramConversationId = onTelegram ? resolveTelegramConversationId(params) : undefined;
const discordManager = onDiscord ? getThreadBindingManager(accountId) : null;
const discordManager = onDiscord
? channelRuntime.discord.threadBindings.getManager(accountId)
: null;
if (onDiscord && !discordManager) {
return {
shouldContinue: false,
@@ -433,13 +424,13 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
}
const idleTimeoutMs = onDiscord
? resolveThreadBindingIdleTimeoutMs({
? channelRuntime.discord.threadBindings.resolveIdleTimeoutMs({
record: discordBinding!,
defaultIdleTimeoutMs: discordManager!.getIdleTimeoutMs(),
})
: resolveTelegramBindingDurationMs(telegramBinding!, "idleTimeoutMs", 24 * 60 * 60 * 1000);
const idleExpiresAt = onDiscord
? resolveThreadBindingInactivityExpiresAt({
? channelRuntime.discord.threadBindings.resolveInactivityExpiresAt({
record: discordBinding!,
defaultIdleTimeoutMs: discordManager!.getIdleTimeoutMs(),
})
@@ -447,13 +438,13 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
? resolveTelegramBindingLastActivityAt(telegramBinding!) + idleTimeoutMs
: undefined;
const maxAgeMs = onDiscord
? resolveThreadBindingMaxAgeMs({
? channelRuntime.discord.threadBindings.resolveMaxAgeMs({
record: discordBinding!,
defaultMaxAgeMs: discordManager!.getMaxAgeMs(),
})
: resolveTelegramBindingDurationMs(telegramBinding!, "maxAgeMs", 0);
const maxAgeExpiresAt = onDiscord
? resolveThreadBindingMaxAgeExpiresAt({
? channelRuntime.discord.threadBindings.resolveMaxAgeExpiresAt({
record: discordBinding!,
defaultMaxAgeMs: discordManager!.getMaxAgeMs(),
})
@@ -528,24 +519,24 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
const updatedBindings = (() => {
if (onDiscord) {
return action === SESSION_ACTION_IDLE
? setThreadBindingIdleTimeoutBySessionKey({
? channelRuntime.discord.threadBindings.setIdleTimeoutBySessionKey({
targetSessionKey: discordBinding!.targetSessionKey,
accountId,
idleTimeoutMs: durationMs,
})
: setThreadBindingMaxAgeBySessionKey({
: channelRuntime.discord.threadBindings.setMaxAgeBySessionKey({
targetSessionKey: discordBinding!.targetSessionKey,
accountId,
maxAgeMs: durationMs,
});
}
return action === SESSION_ACTION_IDLE
? setTelegramThreadBindingIdleTimeoutBySessionKey({
? channelRuntime.telegram.threadBindings.setIdleTimeoutBySessionKey({
targetSessionKey: telegramBinding!.targetSessionKey,
accountId,
idleTimeoutMs: durationMs,
})
: setTelegramThreadBindingMaxAgeBySessionKey({
: channelRuntime.telegram.threadBindings.setMaxAgeBySessionKey({
targetSessionKey: telegramBinding!.targetSessionKey,
accountId,
maxAgeMs: durationMs,