Files
Dirigent/plugin/core/identity-registry.ts
hzhang b5196e972c 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>
2026-04-08 22:41:25 +01:00

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;
}
}