Skip to content

Commit 74d0c39

Browse files
committed
refactor: move session lifecycle and outbound fallbacks into plugins
1 parent 49251de commit 74d0c39

13 files changed

Lines changed: 114 additions & 243 deletions

src/auto-reply/reply/commands-session-lifecycle.test.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
23
import type { OpenClawConfig } from "../../config/config.js";
34
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
5+
import { setActivePluginRegistry } from "../../plugins/runtime.js";
6+
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
47

58
const hoisted = vi.hoisted(() => {
69
const getThreadBindingManagerMock = vi.fn();
@@ -19,28 +22,34 @@ const hoisted = vi.hoisted(() => {
1922
};
2023
});
2124

22-
vi.mock("../../../extensions/discord/src/monitor/thread-bindings.js", async (importOriginal) => {
23-
const actual =
24-
await importOriginal<
25-
typeof import("../../../extensions/discord/src/monitor/thread-bindings.js")
26-
>();
27-
return {
28-
...actual,
29-
getThreadBindingManager: hoisted.getThreadBindingManagerMock,
30-
setThreadBindingIdleTimeoutBySessionKey: hoisted.setThreadBindingIdleTimeoutBySessionKeyMock,
31-
setThreadBindingMaxAgeBySessionKey: hoisted.setThreadBindingMaxAgeBySessionKeyMock,
32-
};
33-
});
34-
35-
vi.mock("../../../extensions/telegram/src/thread-bindings.js", async (importOriginal) => {
36-
const actual =
37-
await importOriginal<typeof import("../../../extensions/telegram/src/thread-bindings.js")>();
25+
vi.mock("../../plugins/runtime/index.js", async () => {
26+
const discordThreadBindings = await vi.importActual<
27+
typeof import("../../../extensions/discord/src/monitor/thread-bindings.js")
28+
>("../../../extensions/discord/src/monitor/thread-bindings.js");
3829
return {
39-
...actual,
40-
setTelegramThreadBindingIdleTimeoutBySessionKey:
41-
hoisted.setTelegramThreadBindingIdleTimeoutBySessionKeyMock,
42-
setTelegramThreadBindingMaxAgeBySessionKey:
43-
hoisted.setTelegramThreadBindingMaxAgeBySessionKeyMock,
30+
createPluginRuntime: () => ({
31+
channel: {
32+
discord: {
33+
threadBindings: {
34+
getManager: hoisted.getThreadBindingManagerMock,
35+
resolveIdleTimeoutMs: discordThreadBindings.resolveThreadBindingIdleTimeoutMs,
36+
resolveInactivityExpiresAt:
37+
discordThreadBindings.resolveThreadBindingInactivityExpiresAt,
38+
resolveMaxAgeMs: discordThreadBindings.resolveThreadBindingMaxAgeMs,
39+
resolveMaxAgeExpiresAt: discordThreadBindings.resolveThreadBindingMaxAgeExpiresAt,
40+
setIdleTimeoutBySessionKey: hoisted.setThreadBindingIdleTimeoutBySessionKeyMock,
41+
setMaxAgeBySessionKey: hoisted.setThreadBindingMaxAgeBySessionKeyMock,
42+
unbindBySessionKey: vi.fn(),
43+
},
44+
},
45+
telegram: {
46+
threadBindings: {
47+
setIdleTimeoutBySessionKey: hoisted.setTelegramThreadBindingIdleTimeoutBySessionKeyMock,
48+
setMaxAgeBySessionKey: hoisted.setTelegramThreadBindingMaxAgeBySessionKeyMock,
49+
},
50+
},
51+
},
52+
}),
4453
};
4554
});
4655

@@ -168,6 +177,9 @@ function createFakeThreadBindingManager(binding: FakeBinding | null) {
168177

169178
describe("/session idle and /session max-age", () => {
170179
beforeEach(() => {
180+
setActivePluginRegistry(
181+
createTestRegistry([{ pluginId: "telegram", source: "test", plugin: telegramPlugin }]),
182+
);
171183
hoisted.getThreadBindingManagerMock.mockReset();
172184
hoisted.setThreadBindingIdleTimeoutBySessionKeyMock.mockReset();
173185
hoisted.setThreadBindingMaxAgeBySessionKeyMock.mockReset();

src/auto-reply/reply/commands-session.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,13 @@
1-
import {
2-
formatThreadBindingDurationLabel,
3-
getThreadBindingManager,
4-
resolveThreadBindingIdleTimeoutMs,
5-
resolveThreadBindingInactivityExpiresAt,
6-
resolveThreadBindingMaxAgeExpiresAt,
7-
resolveThreadBindingMaxAgeMs,
8-
setThreadBindingIdleTimeoutBySessionKey,
9-
setThreadBindingMaxAgeBySessionKey,
10-
} from "../../../extensions/discord/src/monitor/thread-bindings.js";
11-
import {
12-
setTelegramThreadBindingIdleTimeoutBySessionKey,
13-
setTelegramThreadBindingMaxAgeBySessionKey,
14-
} from "../../../extensions/telegram/src/thread-bindings.js";
151
import { resolveFastModeState } from "../../agents/fast-mode.js";
2+
import { formatThreadBindingDurationLabel } from "../../channels/thread-bindings-messages.js";
163
import { parseDurationMs } from "../../cli/parse-duration.js";
174
import { isRestartEnabled } from "../../config/commands.js";
185
import { logVerbose } from "../../globals.js";
196
import { getSessionBindingService } from "../../infra/outbound/session-binding-service.js";
207
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
218
import { scheduleGatewaySigusr1Restart, triggerOpenClawRestart } from "../../infra/restart.js";
229
import { loadCostUsageSummary, loadSessionCostSummary } from "../../infra/session-cost-usage.js";
10+
import { createPluginRuntime } from "../../plugins/runtime/index.js";
2311
import { formatTokenCount, formatUsd } from "../../utils/usage-format.js";
2412
import { parseActivationCommand } from "../group-activation.js";
2513
import { parseSendPolicyCommand } from "../send-policy.js";
@@ -34,6 +22,7 @@ const SESSION_COMMAND_PREFIX = "/session";
3422
const SESSION_DURATION_OFF_VALUES = new Set(["off", "disable", "disabled", "none", "0"]);
3523
const SESSION_ACTION_IDLE = "idle";
3624
const SESSION_ACTION_MAX_AGE = "max-age";
25+
const channelRuntime = createPluginRuntime().channel;
3726

3827
function resolveSessionCommandUsage() {
3928
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
385374
params.ctx.MessageThreadId != null ? String(params.ctx.MessageThreadId).trim() : "";
386375
const telegramConversationId = onTelegram ? resolveTelegramConversationId(params) : undefined;
387376

388-
const discordManager = onDiscord ? getThreadBindingManager(accountId) : null;
377+
const discordManager = onDiscord
378+
? channelRuntime.discord.threadBindings.getManager(accountId)
379+
: null;
389380
if (onDiscord && !discordManager) {
390381
return {
391382
shouldContinue: false,
@@ -433,27 +424,27 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
433424
}
434425

435426
const idleTimeoutMs = onDiscord
436-
? resolveThreadBindingIdleTimeoutMs({
427+
? channelRuntime.discord.threadBindings.resolveIdleTimeoutMs({
437428
record: discordBinding!,
438429
defaultIdleTimeoutMs: discordManager!.getIdleTimeoutMs(),
439430
})
440431
: resolveTelegramBindingDurationMs(telegramBinding!, "idleTimeoutMs", 24 * 60 * 60 * 1000);
441432
const idleExpiresAt = onDiscord
442-
? resolveThreadBindingInactivityExpiresAt({
433+
? channelRuntime.discord.threadBindings.resolveInactivityExpiresAt({
443434
record: discordBinding!,
444435
defaultIdleTimeoutMs: discordManager!.getIdleTimeoutMs(),
445436
})
446437
: idleTimeoutMs > 0
447438
? resolveTelegramBindingLastActivityAt(telegramBinding!) + idleTimeoutMs
448439
: undefined;
449440
const maxAgeMs = onDiscord
450-
? resolveThreadBindingMaxAgeMs({
441+
? channelRuntime.discord.threadBindings.resolveMaxAgeMs({
451442
record: discordBinding!,
452443
defaultMaxAgeMs: discordManager!.getMaxAgeMs(),
453444
})
454445
: resolveTelegramBindingDurationMs(telegramBinding!, "maxAgeMs", 0);
455446
const maxAgeExpiresAt = onDiscord
456-
? resolveThreadBindingMaxAgeExpiresAt({
447+
? channelRuntime.discord.threadBindings.resolveMaxAgeExpiresAt({
457448
record: discordBinding!,
458449
defaultMaxAgeMs: discordManager!.getMaxAgeMs(),
459450
})
@@ -528,24 +519,24 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
528519
const updatedBindings = (() => {
529520
if (onDiscord) {
530521
return action === SESSION_ACTION_IDLE
531-
? setThreadBindingIdleTimeoutBySessionKey({
522+
? channelRuntime.discord.threadBindings.setIdleTimeoutBySessionKey({
532523
targetSessionKey: discordBinding!.targetSessionKey,
533524
accountId,
534525
idleTimeoutMs: durationMs,
535526
})
536-
: setThreadBindingMaxAgeBySessionKey({
527+
: channelRuntime.discord.threadBindings.setMaxAgeBySessionKey({
537528
targetSessionKey: discordBinding!.targetSessionKey,
538529
accountId,
539530
maxAgeMs: durationMs,
540531
});
541532
}
542533
return action === SESSION_ACTION_IDLE
543-
? setTelegramThreadBindingIdleTimeoutBySessionKey({
534+
? channelRuntime.telegram.threadBindings.setIdleTimeoutBySessionKey({
544535
targetSessionKey: telegramBinding!.targetSessionKey,
545536
accountId,
546537
idleTimeoutMs: durationMs,
547538
})
548-
: setTelegramThreadBindingMaxAgeBySessionKey({
539+
: channelRuntime.telegram.threadBindings.setMaxAgeBySessionKey({
549540
targetSessionKey: telegramBinding!.targetSessionKey,
550541
accountId,
551542
maxAgeMs: durationMs,

src/gateway/server.sessions.gateway-server-sessions-a.test.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,21 @@ vi.mock("../plugins/hook-runner-global.js", async (importOriginal) => {
102102
};
103103
});
104104

105-
vi.mock("../../extensions/discord/src/monitor/thread-bindings.js", async (importOriginal) => {
106-
const actual =
107-
await importOriginal<
108-
typeof import("../../extensions/discord/src/monitor/thread-bindings.js")
109-
>();
105+
vi.mock("../plugins/runtime/runtime-discord.js", async (importOriginal) => {
106+
const actual = await importOriginal<typeof import("../plugins/runtime/runtime-discord.js")>();
110107
return {
111108
...actual,
112-
unbindThreadBindingsBySessionKey: (params: unknown) =>
113-
threadBindingMocks.unbindThreadBindingsBySessionKey(params),
109+
createRuntimeDiscord: () => {
110+
const runtime = actual.createRuntimeDiscord();
111+
return {
112+
...runtime,
113+
threadBindings: {
114+
...runtime.threadBindings,
115+
unbindBySessionKey: (params: unknown) =>
116+
threadBindingMocks.unbindThreadBindingsBySessionKey(params),
117+
},
118+
};
119+
},
114120
};
115121
});
116122

src/gateway/session-reset-service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { randomUUID } from "node:crypto";
2-
import { unbindThreadBindingsBySessionKey } from "../../extensions/discord/src/monitor/thread-bindings.js";
32
import { getAcpSessionManager } from "../acp/control-plane/manager.js";
43
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
54
import { clearBootstrapSnapshot } from "../agents/bootstrap-cache.js";
@@ -16,6 +15,7 @@ import {
1615
import { logVerbose } from "../globals.js";
1716
import { createInternalHookEvent, triggerInternalHook } from "../hooks/internal-hooks.js";
1817
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
18+
import { createPluginRuntime } from "../plugins/runtime/index.js";
1919
import {
2020
isSubagentSessionKey,
2121
normalizeAgentId,
@@ -31,6 +31,7 @@ import {
3131
} from "./session-utils.js";
3232

3333
const ACP_RUNTIME_CLEANUP_TIMEOUT_MS = 15_000;
34+
const channelRuntime = createPluginRuntime().channel;
3435

3536
function stripRuntimeModelState(entry?: SessionEntry): SessionEntry | undefined {
3637
if (!entry) {
@@ -70,7 +71,7 @@ export async function emitSessionUnboundLifecycleEvent(params: {
7071
emitHooks?: boolean;
7172
}) {
7273
const targetKind = isSubagentSessionKey(params.targetSessionKey) ? "subagent" : "acp";
73-
unbindThreadBindingsBySessionKey({
74+
channelRuntime.discord.threadBindings.unbindBySessionKey({
7475
targetSessionKey: params.targetSessionKey,
7576
targetKind,
7677
reason: params.reason,

src/infra/exec-approval-forwarder.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import type {
2222
ExecApprovalRequest,
2323
ExecApprovalResolved,
2424
} from "./exec-approvals.js";
25-
import { resolveBuiltInExecApprovalAdapter } from "./outbound/built-in-channel-adapters.js";
2625
import { deliverOutboundPayloads } from "./outbound/deliver.js";
2726

2827
const log = createSubsystemLogger("gateway/exec-approvals");
@@ -119,8 +118,7 @@ function shouldSkipForwardingFallback(params: {
119118
if (!channel) {
120119
return false;
121120
}
122-
const adapter =
123-
getChannelPlugin(channel)?.execApprovals ?? resolveBuiltInExecApprovalAdapter(channel);
121+
const adapter = getChannelPlugin(channel)?.execApprovals;
124122
return (
125123
adapter?.shouldSuppressForwardingFallback?.({
126124
cfg: params.cfg,
@@ -278,9 +276,7 @@ function buildRequestPayloadForTarget(
278276
): ReplyPayload {
279277
const channel = normalizeMessageChannel(target.channel) ?? target.channel;
280278
const pluginPayload = channel
281-
? (
282-
getChannelPlugin(channel)?.execApprovals ?? resolveBuiltInExecApprovalAdapter(channel)
283-
)?.buildPendingPayload?.({
279+
? getChannelPlugin(channel)?.execApprovals?.buildPendingPayload?.({
284280
cfg,
285281
request,
286282
target,
@@ -415,9 +411,7 @@ export function createExecApprovalForwarder(
415411
if (!channel) {
416412
return;
417413
}
418-
await (
419-
getChannelPlugin(channel)?.execApprovals ?? resolveBuiltInExecApprovalAdapter(channel)
420-
)?.beforeDeliverPending?.({
414+
await getChannelPlugin(channel)?.execApprovals?.beforeDeliverPending?.({
421415
cfg,
422416
target,
423417
payload,

0 commit comments

Comments
 (0)