import fs from "node:fs"; import path from "node:path"; export type ChannelMode = "none" | "work" | "report" | "discussion" | "chat"; export type TurnManagerState = "disabled" | "dead" | "normal" | "shuffle" | "archived"; /** Modes that cannot be changed once set. */ const LOCKED_MODES = new Set(["work", "discussion"]); /** Derive turn-manager state from mode + agent count. */ export function deriveTurnManagerState(mode: ChannelMode, agentCount: number, concluded = false): TurnManagerState { if (mode === "none" || mode === "work") return "disabled"; if (mode === "report") return "dead"; if (mode === "discussion") { if (concluded) return "archived"; if (agentCount <= 1) return "disabled"; if (agentCount === 2) return "normal"; return "shuffle"; } if (mode === "chat") { if (agentCount <= 1) return "disabled"; if (agentCount === 2) return "normal"; return "shuffle"; } return "disabled"; } export type DiscussionMeta = { initiatorAgentId: string; callbackGuildId: string; callbackChannelId: string; concluded: boolean; }; export type ChannelRecord = { mode: ChannelMode; /** For discussion channels: metadata about the discussion. */ discussion?: DiscussionMeta; }; export class ChannelStore { private filePath: string; private records: Record = {}; 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.records = {}; return; } try { const raw = fs.readFileSync(this.filePath, "utf8"); this.records = JSON.parse(raw) ?? {}; } catch { this.records = {}; } } 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.records, null, 2), "utf8"); } getMode(channelId: string): ChannelMode { this.load(); return this.records[channelId]?.mode ?? "none"; } getRecord(channelId: string): ChannelRecord { this.load(); return this.records[channelId] ?? { mode: "none" }; } /** * Set channel mode. Throws if the channel is currently in a locked mode, * or if the requested mode is locked (must use setLockedMode instead). */ setMode(channelId: string, mode: ChannelMode): void { this.load(); const current = this.records[channelId]?.mode ?? "none"; if (LOCKED_MODES.has(current)) { throw new Error(`Channel ${channelId} is in locked mode "${current}" and cannot be changed.`); } if (LOCKED_MODES.has(mode)) { throw new Error(`Mode "${mode}" can only be set at channel creation via the dedicated tool.`); } this.records[channelId] = { ...this.records[channelId], mode }; this.save(); } /** * Set a locked mode (work or discussion). Only callable from creation tools. * Throws if the channel already has any mode set. */ setLockedMode(channelId: string, mode: ChannelMode, discussion?: DiscussionMeta): void { this.load(); if (this.records[channelId]) { throw new Error(`Channel ${channelId} already has mode "${this.records[channelId].mode}".`); } const record: ChannelRecord = { mode }; if (discussion) record.discussion = discussion; this.records[channelId] = record; this.save(); } /** Mark a discussion as concluded (sets archived state). */ concludeDiscussion(channelId: string): void { this.load(); const rec = this.records[channelId]; if (!rec || rec.mode !== "discussion") { throw new Error(`Channel ${channelId} is not a discussion channel.`); } if (!rec.discussion) { throw new Error(`Channel ${channelId} has no discussion metadata.`); } rec.discussion = { ...rec.discussion, concluded: true }; this.save(); } isLocked(channelId: string): boolean { this.load(); return LOCKED_MODES.has(this.records[channelId]?.mode ?? "none"); } listAll(): Array<{ channelId: string } & ChannelRecord> { this.load(); return Object.entries(this.records).map(([channelId, rec]) => ({ channelId, ...rec })); } }