feat: rewrite plugin as v2 with globalThis-based turn management
Complete rewrite of the Dirigent plugin turn management system to work correctly with OpenClaw's VM-context-per-session architecture: - All turn state stored on globalThis (persists across VM context hot-reloads) - Hooks registered unconditionally on every api instance; event-level dedup (runId Set for agent_end, WeakSet for before_model_resolve) prevents double-processing - Gateway lifecycle events (gateway_start/stop) guarded once via globalThis flag - Shared initializingChannels lock prevents concurrent channel init across VM contexts in message_received and before_model_resolve - New ChannelStore and IdentityRegistry replace old policy/session-state modules - Added agent_end hook with tail-match polling for Discord delivery confirmation - Added web control page, padded-cell auto-scan, discussion tool support - Removed obsolete v1 modules: channel-resolver, channel-modes, discussion-service, session-state, turn-bootstrap, policy/store, rules, decision-input Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -133,21 +133,6 @@ function getJson(pathKey) {
|
||||
try { return JSON.parse(out); } catch { return undefined; }
|
||||
}
|
||||
function setJson(pathKey, value) { runOpenclaw(["config", "set", pathKey, JSON.stringify(value), "--json"]); }
|
||||
function isPlainObject(v) { return !!v && typeof v === "object" && !Array.isArray(v); }
|
||||
function mergePreservingExisting(base, updates) {
|
||||
if (!isPlainObject(updates)) return updates;
|
||||
const out = isPlainObject(base) ? { ...base } : {};
|
||||
for (const [key, nextValue] of Object.entries(updates)) {
|
||||
const currentValue = out[key];
|
||||
if (nextValue === undefined) continue;
|
||||
if (isPlainObject(nextValue)) { out[key] = mergePreservingExisting(currentValue, nextValue); continue; }
|
||||
if (nextValue === null) { if (currentValue === undefined) out[key] = null; continue; }
|
||||
if (typeof nextValue === "string") { if (nextValue === "" && currentValue !== undefined) continue; out[key] = nextValue; continue; }
|
||||
if (Array.isArray(nextValue)) { if (nextValue.length === 0 && Array.isArray(currentValue) && currentValue.length > 0) continue; out[key] = nextValue; continue; }
|
||||
out[key] = nextValue;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function syncDirRecursive(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
fs.cpSync(src, dest, { recursive: true, force: true });
|
||||
@@ -187,24 +172,34 @@ if (mode === "install") {
|
||||
}
|
||||
|
||||
step(4, 7, "configure plugin entry");
|
||||
const plugins = getJson("plugins") || {};
|
||||
const loadPaths = Array.isArray(plugins?.load?.paths) ? plugins.load.paths : [];
|
||||
if (!loadPaths.includes(PLUGIN_INSTALL_DIR)) loadPaths.push(PLUGIN_INSTALL_DIR);
|
||||
plugins.load = plugins.load || {}; plugins.load.paths = loadPaths;
|
||||
plugins.entries = plugins.entries || {};
|
||||
const existingDirigent = isPlainObject(plugins.entries.dirigent) ? plugins.entries.dirigent : {};
|
||||
const desired = {
|
||||
enabled: true,
|
||||
config: {
|
||||
enabled: true, discordOnly: true, listMode: "human-list",
|
||||
humanList: [], agentList: [],
|
||||
channelPoliciesFile: path.join(OPENCLAW_DIR, "dirigent-channel-policies.json"),
|
||||
endSymbols: ["🔚"], schedulingIdentifier: "➡️",
|
||||
noReplyProvider: NO_REPLY_PROVIDER_ID, noReplyModel: NO_REPLY_MODEL_ID, noReplyPort: NO_REPLY_PORT,
|
||||
},
|
||||
};
|
||||
plugins.entries.dirigent = mergePreservingExisting(existingDirigent, desired);
|
||||
setJson("plugins", plugins);
|
||||
// Plugin load path — safe to read/write (not sensitive)
|
||||
const loadPaths = getJson("plugins.load.paths") || [];
|
||||
if (!loadPaths.includes(PLUGIN_INSTALL_DIR)) {
|
||||
loadPaths.push(PLUGIN_INSTALL_DIR);
|
||||
setJson("plugins.load.paths", loadPaths);
|
||||
}
|
||||
// For each config field: only write the default if the field has no value.
|
||||
// Sensitive fields (e.g. moderatorBotToken) are never touched — user sets them manually.
|
||||
// `getJson` returns undefined if the field is unset; __OPENCLAW_REDACTED__ counts as "set".
|
||||
function setIfMissing(pathKey, defaultVal) {
|
||||
const existing = getJson(pathKey);
|
||||
if (existing === undefined || existing === null) setJson(pathKey, defaultVal);
|
||||
}
|
||||
setIfMissing("plugins.entries.dirigent.enabled", true);
|
||||
const cp = "plugins.entries.dirigent.config";
|
||||
setIfMissing(`${cp}.enabled`, true);
|
||||
setIfMissing(`${cp}.discordOnly`, true);
|
||||
setIfMissing(`${cp}.listMode`, "human-list");
|
||||
setIfMissing(`${cp}.humanList`, []);
|
||||
setIfMissing(`${cp}.agentList`, []);
|
||||
setIfMissing(`${cp}.channelPoliciesFile`, path.join(OPENCLAW_DIR, "dirigent-channel-policies.json"));
|
||||
setIfMissing(`${cp}.endSymbols`, ["🔚"]);
|
||||
setIfMissing(`${cp}.schedulingIdentifier`, "➡️");
|
||||
setIfMissing(`${cp}.noReplyProvider`, NO_REPLY_PROVIDER_ID);
|
||||
setIfMissing(`${cp}.noReplyModel`, NO_REPLY_MODEL_ID);
|
||||
setIfMissing(`${cp}.noReplyPort`, NO_REPLY_PORT);
|
||||
// moderatorBotToken: intentionally not touched — set manually via:
|
||||
// openclaw config set plugins.entries.dirigent.config.moderatorBotToken "<token>"
|
||||
ok("plugin configured");
|
||||
|
||||
step(5, 7, "configure no-reply provider");
|
||||
|
||||
Reference in New Issue
Block a user