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>
94 lines
2.4 KiB
TypeScript
94 lines
2.4 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
export type IdentityEntry = {
|
|
discordUserId: string;
|
|
agentId: string;
|
|
agentName: string;
|
|
};
|
|
|
|
export class IdentityRegistry {
|
|
private filePath: string;
|
|
private entries: IdentityEntry[] = [];
|
|
private loaded = false;
|
|
|
|
constructor(filePath: string) {
|
|
this.filePath = filePath;
|
|
}
|
|
|
|
private load(): void {
|
|
if (this.loaded) return;
|
|
this.loaded = true;
|
|
if (!fs.existsSync(this.filePath)) {
|
|
this.entries = [];
|
|
return;
|
|
}
|
|
try {
|
|
const raw = fs.readFileSync(this.filePath, "utf8");
|
|
const parsed = JSON.parse(raw);
|
|
this.entries = Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
this.entries = [];
|
|
}
|
|
}
|
|
|
|
private save(): void {
|
|
const dir = path.dirname(this.filePath);
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(this.filePath, JSON.stringify(this.entries, null, 2), "utf8");
|
|
}
|
|
|
|
upsert(entry: IdentityEntry): void {
|
|
this.load();
|
|
const idx = this.entries.findIndex((e) => e.agentId === entry.agentId);
|
|
if (idx >= 0) {
|
|
this.entries[idx] = entry;
|
|
} else {
|
|
this.entries.push(entry);
|
|
}
|
|
this.save();
|
|
}
|
|
|
|
remove(agentId: string): boolean {
|
|
this.load();
|
|
const before = this.entries.length;
|
|
this.entries = this.entries.filter((e) => e.agentId !== agentId);
|
|
if (this.entries.length !== before) {
|
|
this.save();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
findByAgentId(agentId: string): IdentityEntry | undefined {
|
|
this.load();
|
|
return this.entries.find((e) => e.agentId === agentId);
|
|
}
|
|
|
|
findByDiscordUserId(discordUserId: string): IdentityEntry | undefined {
|
|
this.load();
|
|
return this.entries.find((e) => e.discordUserId === discordUserId);
|
|
}
|
|
|
|
list(): IdentityEntry[] {
|
|
this.load();
|
|
return [...this.entries];
|
|
}
|
|
|
|
/** Build a map from discordUserId → agentId for fast lookup. */
|
|
buildDiscordToAgentMap(): Map<string, string> {
|
|
this.load();
|
|
const map = new Map<string, string>();
|
|
for (const e of this.entries) map.set(e.discordUserId, e.agentId);
|
|
return map;
|
|
}
|
|
|
|
/** Build a map from agentId → discordUserId for fast lookup. */
|
|
buildAgentToDiscordMap(): Map<string, string> {
|
|
this.load();
|
|
const map = new Map<string, string>();
|
|
for (const e of this.entries) map.set(e.agentId, e.discordUserId);
|
|
return map;
|
|
}
|
|
}
|