refactor(plugin): extract shared session state and decision pruning

This commit is contained in:
2026-03-08 05:43:52 +00:00
parent ea501ccef8
commit 63009f3925
2 changed files with 42 additions and 30 deletions

View File

@@ -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<string, DecisionRecord>();
const sessionAllowed = new Map<string, boolean>(); // Track if session was allowed to speak (true) or forced no-reply (false)
const sessionInjected = new Set<string>(); // Track which sessions have already injected the end marker
const sessionChannelId = new Map<string, string>(); // Track sessionKey -> channelId mapping
const sessionAccountId = new Map<string, string>(); // Track sessionKey -> accountId mapping
const sessionTurnHandled = new Set<string>(); // 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",