From 63009f39250dc79ad9e660c836e7565327a99fd5 Mon Sep 17 00:00:00 2001 From: orion Date: Sun, 8 Mar 2026 05:43:52 +0000 Subject: [PATCH] refactor(plugin): extract shared session state and decision pruning --- plugin/core/session-state.ts | 31 +++++++++++++++++++++++++++ plugin/index.ts | 41 ++++++++++-------------------------- 2 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 plugin/core/session-state.ts diff --git a/plugin/core/session-state.ts b/plugin/core/session-state.ts new file mode 100644 index 0000000..108bef7 --- /dev/null +++ b/plugin/core/session-state.ts @@ -0,0 +1,31 @@ +import type { Decision } from "../rules.js"; + +export type DecisionRecord = { + decision: Decision; + createdAt: number; + needsRestore?: boolean; +}; + +export const MAX_SESSION_DECISIONS = 2000; +export const DECISION_TTL_MS = 5 * 60 * 1000; + +export const sessionDecision = new Map(); +export const sessionAllowed = new Map(); +export const sessionInjected = new Set(); +export const sessionChannelId = new Map(); +export const sessionAccountId = new Map(); +export const sessionTurnHandled = new Set(); + +export function pruneDecisionMap(now = Date.now()): void { + for (const [k, v] of sessionDecision.entries()) { + if (now - v.createdAt > DECISION_TTL_MS) sessionDecision.delete(k); + } + + if (sessionDecision.size <= MAX_SESSION_DECISIONS) return; + const keys = sessionDecision.keys(); + while (sessionDecision.size > MAX_SESSION_DECISIONS) { + const k = keys.next(); + if (k.done) break; + sessionDecision.delete(k.value); + } +} diff --git a/plugin/index.ts b/plugin/index.ts index 0e94d59..1ecd89c 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; -import type { Decision, DirigentConfig } from "./rules.js"; +import type { DirigentConfig } from "./rules.js"; import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js"; import { registerMessageReceivedHook } from "./hooks/message-received.js"; import { registerBeforeModelResolveHook } from "./hooks/before-model-resolve.js"; @@ -18,27 +18,22 @@ import { ensureTurnOrder, recordChannelAccount } from "./core/turn-bootstrap.js" import { debugCtxSummary, pickDefined, shouldDebugLog } from "./core/utils.js"; import { resolveDiscordUserId, sendModeratorMessage } from "./core/moderator-discord.js"; import { startNoReplyApi, stopNoReplyApi } from "./core/no-reply-process.js"; - -type DecisionRecord = { - decision: Decision; - createdAt: number; - needsRestore?: boolean; -}; +import { + DECISION_TTL_MS, + pruneDecisionMap, + sessionAccountId, + sessionAllowed, + sessionChannelId, + sessionDecision, + sessionInjected, + sessionTurnHandled, +} from "./core/session-state.js"; type DebugConfig = { enableDebugLogs?: boolean; debugLogChannelIds?: string[]; }; -const sessionDecision = new Map(); -const sessionAllowed = new Map(); // Track if session was allowed to speak (true) or forced no-reply (false) -const sessionInjected = new Set(); // Track which sessions have already injected the end marker -const sessionChannelId = new Map(); // Track sessionKey -> channelId mapping -const sessionAccountId = new Map(); // Track sessionKey -> accountId mapping -const sessionTurnHandled = new Set(); // Track sessions where turn was already advanced in before_message_write -const MAX_SESSION_DECISIONS = 2000; -const DECISION_TTL_MS = 5 * 60 * 1000; - function buildEndMarkerInstruction(endSymbols: string[], isGroupChat: boolean, schedulingIdentifier: string, waitIdentifier: string): string { const symbols = endSymbols.length > 0 ? endSymbols.join("") : "🔚"; let instruction = `Your response MUST end with ${symbols}. Exception: gateway keywords (e.g. NO_REPLY, HEARTBEAT_OK) must NOT include ${symbols}.`; @@ -53,20 +48,6 @@ function buildSchedulingIdentifierInstruction(schedulingIdentifier: string): str return `\n\nScheduling identifier: "${schedulingIdentifier}". This identifier itself is meaningless — it carries no semantic content. When you receive a message containing <@YOUR_USER_ID> followed by the scheduling identifier, check recent chat history and decide whether you have something to say. If not, reply NO_REPLY.`; } -function pruneDecisionMap(now = Date.now()) { - for (const [k, v] of sessionDecision.entries()) { - if (now - v.createdAt > DECISION_TTL_MS) sessionDecision.delete(k); - } - - if (sessionDecision.size <= MAX_SESSION_DECISIONS) return; - const keys = sessionDecision.keys(); - while (sessionDecision.size > MAX_SESSION_DECISIONS) { - const k = keys.next(); - if (k.done) break; - sessionDecision.delete(k.value); - } -} - export default { id: "dirigent", name: "Dirigent",