import { randomBytes } from 'crypto';
import Database from 'better-sqlite3';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

function evaluateRelayRule(rules, fromMemberId, text) {
  const ordered = [...rules].sort((a, b) => a.priority - b.priority);
  for (const rule of ordered) {
    if (rule.fromMemberId !== "ANY" && rule.fromMemberId !== fromMemberId) {
      continue;
    }
    if (rule.toMemberId === fromMemberId) {
      continue;
    }
    if (!matchesPattern(text, rule.matchPattern)) {
      continue;
    }
    return rule;
  }
  return null;
}
function isValidRoutingRule(rule) {
  return rule.toMemberId !== "ANY" && rule.fromMemberId !== rule.toMemberId;
}
function matchesPattern(text, pattern) {
  if (!pattern) {
    return true;
  }
  return text.toLowerCase().includes(pattern.toLowerCase());
}
const DB_DIR = path.join(os.homedir(), ".shooter");
const DB_PATH = path.join(DB_DIR, "shooter.db");
class RelayStore {
  db;
  constructor() {
    fs.mkdirSync(DB_DIR, { recursive: true });
    this.db = new Database(DB_PATH);
    this.db.pragma("journal_mode = WAL");
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS super_sessions (
        id            TEXT PRIMARY KEY,
        label         TEXT NOT NULL,
        routing_rules TEXT NOT NULL DEFAULT '[]',
        status        TEXT NOT NULL DEFAULT 'active',
        created_at    TEXT NOT NULL
      );
      CREATE TABLE IF NOT EXISTS sos_sessions (
        id                TEXT PRIMARY KEY,
        super_session_id  TEXT NOT NULL,
        session_key       TEXT NOT NULL,
        terminal_id       TEXT,
        provider          TEXT NOT NULL,
        capability        TEXT NOT NULL DEFAULT '',
        status            TEXT NOT NULL DEFAULT 'Idle',
        registered_at     TEXT NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_sos_sessions_super ON sos_sessions(super_session_id);
    `);
  }
  addMember(superSessionId, member) {
    this.db.prepare(
      `INSERT OR REPLACE INTO sos_sessions
         (id, super_session_id, session_key, terminal_id, provider, capability, status, registered_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
    ).run(
      member.id,
      superSessionId,
      member.sessionKey,
      member.terminalId,
      member.provider,
      member.capability,
      member.status,
      member.registeredAt
    );
  }
  createSuperSession(session) {
    this.db.prepare(
      `INSERT OR REPLACE INTO super_sessions (id, label, routing_rules, status, created_at)
         VALUES (?, ?, ?, ?, ?)`
    ).run(
      session.id,
      session.label,
      JSON.stringify(session.routingRules),
      session.status,
      session.createdAt
    );
  }
  deleteSuperSession(id) {
    const tx = this.db.transaction((sid) => {
      this.db.prepare("DELETE FROM sos_sessions WHERE super_session_id = ?").run(sid);
      this.db.prepare("DELETE FROM super_sessions WHERE id = ?").run(sid);
    });
    tx(id);
  }
  /** Rebuild every persisted super-session (members included, transcript empty). */
  loadAll() {
    const rows = this.db.prepare("SELECT * FROM super_sessions").all();
    return rows.map((row) => {
      const id = row.id;
      const memberRows = this.db.prepare("SELECT * FROM sos_sessions WHERE super_session_id = ? ORDER BY registered_at").all(id);
      return {
        createdAt: row.created_at,
        id,
        label: row.label,
        members: memberRows.map(rowToMember),
        routingRules: safeParseRules(row.routing_rules),
        status: row.status,
        transcript: []
      };
    });
  }
  removeMember(memberId) {
    this.db.prepare("DELETE FROM sos_sessions WHERE id = ?").run(memberId);
  }
  updateMemberStatus(memberId, status) {
    this.db.prepare("UPDATE sos_sessions SET status = ? WHERE id = ?").run(status, memberId);
  }
  updateSuperSessionStatus(id, status) {
    this.db.prepare("UPDATE super_sessions SET status = ? WHERE id = ?").run(status, id);
  }
}
function rowToMember(row) {
  return {
    capability: row.capability ?? "",
    id: row.id,
    provider: row.provider,
    registeredAt: row.registered_at,
    sessionKey: row.session_key,
    status: row.status,
    terminalId: row.terminal_id ?? null
  };
}
function safeParseRules(raw) {
  if (typeof raw !== "string") {
    return [];
  }
  try {
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
}
const RS_GLOBAL_KEY = "__shooter_relay_store";
const relayStore = globalThis[RS_GLOBAL_KEY] || new RelayStore();
globalThis[RS_GLOBAL_KEY] = relayStore;
const MAX_TRANSCRIPT = 500;
const MAX_LABEL_LEN = 200;
const RELAY_COOLDOWN_MS = 5e3;
const ESCALATE_TIMEOUT_MS = 12e4;
const MAX_RELAY_TEXT = 10240;
class SuperSessionCoordinator {
  injector = null;
  runtimes = /* @__PURE__ */ new Map();
  watcher = null;
  /**
   * Add a member to a super-session: persist it, replay its history into the
   * merged transcript, and subscribe for live updates. Returns the new member,
   * or null if the super-session does not exist.
   */
  addMember(superSessionId, spec) {
    const rt = this.runtimes.get(superSessionId);
    if (!rt) {
      return null;
    }
    const member = {
      capability: spec.capability ?? "",
      id: randomBytes(8).toString("hex"),
      provider: spec.provider,
      registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
      sessionKey: spec.sessionKey,
      status: "Idle",
      terminalId: spec.terminalId ?? null
    };
    rt.session.members.push(member);
    relayStore.addMember(superSessionId, member);
    this.subscribeMember(rt, member);
    this.emit(rt, { member, type: "sos-member-added" });
    return member;
  }
  /** Human approves an escalated relay: inject it now, then resolve. */
  approveRelay(superSessionId, relayId) {
    const rt = this.runtimes.get(superSessionId);
    const pending = rt?.pending.get(relayId);
    if (!rt || !pending) {
      return false;
    }
    const result = this.doRelay(rt, pending.toMemberId, pending.text);
    if (!result.ok) {
      this.emit(rt, { message: result.error ?? "Injection failed", type: "sos-error" });
      return false;
    }
    this.resolvePending(rt, relayId, "approved");
    return true;
  }
  createSuperSession(label) {
    const session = {
      createdAt: (/* @__PURE__ */ new Date()).toISOString(),
      id: randomBytes(8).toString("hex"),
      label: label.slice(0, MAX_LABEL_LEN),
      members: [],
      routingRules: [],
      status: "active",
      transcript: []
    };
    relayStore.createSuperSession(session);
    this.runtimes.set(session.id, {
      cooldowns: /* @__PURE__ */ new Map(),
      listeners: /* @__PURE__ */ new Set(),
      pending: /* @__PURE__ */ new Map(),
      session,
      unsubscribes: /* @__PURE__ */ new Map()
    });
    return session;
  }
  deleteSuperSession(id) {
    const rt = this.runtimes.get(id);
    if (!rt) {
      return false;
    }
    for (const unsub of rt.unsubscribes.values()) {
      unsub();
    }
    rt.unsubscribes.clear();
    for (const p of rt.pending.values()) {
      if (p.timer) {
        clearTimeout(p.timer);
      }
    }
    rt.pending.clear();
    relayStore.deleteSuperSession(id);
    this.runtimes.delete(id);
    return true;
  }
  /** Human denies an escalated relay: drop it. */
  denyRelay(superSessionId, relayId) {
    const rt = this.runtimes.get(superSessionId);
    if (!rt?.pending.has(relayId)) {
      return false;
    }
    this.resolvePending(rt, relayId, "denied");
    return true;
  }
  getRoutingRules(superSessionId) {
    return this.runtimes.get(superSessionId)?.session.routingRules ?? null;
  }
  getSuperSession(id) {
    return this.runtimes.get(id)?.session;
  }
  listSuperSessions() {
    return [...this.runtimes.values()].map((rt) => rt.session);
  }
  /** Rebuild persisted super-sessions and re-subscribe members. Call once on boot. */
  reconnectAll() {
    for (const rt of this.runtimes.values()) {
      for (const unsub of rt.unsubscribes.values()) {
        unsub();
      }
      for (const p of rt.pending.values()) {
        if (p.timer) {
          clearTimeout(p.timer);
        }
      }
    }
    this.runtimes.clear();
    for (const session of relayStore.loadAll()) {
      const rt = {
        cooldowns: /* @__PURE__ */ new Map(),
        listeners: /* @__PURE__ */ new Set(),
        pending: /* @__PURE__ */ new Map(),
        session: { ...session, transcript: [] },
        unsubscribes: /* @__PURE__ */ new Map()
      };
      this.runtimes.set(session.id, rt);
      for (const member of rt.session.members) {
        this.subscribeMember(rt, member);
      }
    }
  }
  /**
   * Human-driven relay: inject text into a member's Shooter-owned terminal and
   * record it as a relayed entry in the transcript. Returns an error string on
   * failure, or null on success.
   */
  relayForward(superSessionId, toMemberId, text) {
    const rt = this.runtimes.get(superSessionId);
    if (!rt) {
      return "Super-session not found";
    }
    const member = rt.session.members.find((m) => m.id === toMemberId);
    if (!member) {
      return "Target member not found";
    }
    if (!member.terminalId) {
      return "Target member has no Shooter-owned terminal (cannot inject into an observed session)";
    }
    if (!this.injector) {
      return "Injector not configured";
    }
    const result = this.doRelay(rt, toMemberId, text);
    return result.ok ? null : result.error ?? "Injection failed";
  }
  removeMember(superSessionId, memberId) {
    const rt = this.runtimes.get(superSessionId);
    if (!rt) {
      return false;
    }
    const idx = rt.session.members.findIndex((m) => m.id === memberId);
    if (idx === -1) {
      return false;
    }
    rt.unsubscribes.get(memberId)?.();
    rt.unsubscribes.delete(memberId);
    rt.session.members.splice(idx, 1);
    relayStore.removeMember(memberId);
    this.emit(rt, { memberId, type: "sos-member-removed" });
    return true;
  }
  /** Inject the relay function (server.ts owns PtyManager). */
  setInjector(injector) {
    this.injector = injector;
  }
  /**
   * Replace a super-session's routing rules (Phase 2). Rejects self-relay rules.
   * Returns an error string on invalid input, or null on success.
   */
  setRoutingRules(superSessionId, rules) {
    const rt = this.runtimes.get(superSessionId);
    if (!rt) {
      return "Super-session not found";
    }
    for (const rule of rules) {
      if (!isValidRoutingRule(rule)) {
        return `Invalid rule ${rule.id}: a rule may not relay a member to itself or to ANY`;
      }
    }
    rt.session.routingRules = rules;
    relayStore.createSuperSession(rt.session);
    return null;
  }
  /** Update a super-session's lifecycle status. Returns false if unknown id. */
  setStatus(id, status) {
    const rt = this.runtimes.get(id);
    if (!rt) {
      return false;
    }
    rt.session.status = status;
    relayStore.updateSuperSessionStatus(id, status);
    return true;
  }
  /** Inject the session-watcher adapter (same one the WS session handler uses). */
  setWatcher(watcher) {
    this.watcher = watcher;
  }
  /**
   * Register a WS listener for a super-session and replay the current merged
   * transcript to it. Returns an unsubscribe function, or null if unknown id.
   */
  subscribe(superSessionId, listener) {
    const rt = this.runtimes.get(superSessionId);
    if (!rt) {
      return null;
    }
    rt.listeners.add(listener);
    listener({ entries: [...rt.session.transcript], type: "sos-history" });
    return () => {
      rt.listeners.delete(listener);
    };
  }
  appendEntry(rt, entry) {
    rt.session.transcript.push(entry);
    if (rt.session.transcript.length > MAX_TRANSCRIPT) {
      rt.session.transcript.splice(0, rt.session.transcript.length - MAX_TRANSCRIPT);
    }
    this.emit(rt, { entry, type: "sos-message" });
  }
  /** Run Tier-1 routing for one new message and dispatch the matched action. */
  applyPolicy(rt, fromMemberId, text) {
    if (!text) {
      return;
    }
    const rule = evaluateRelayRule(rt.session.routingRules, fromMemberId, text);
    if (!rule) {
      return;
    }
    if (rule.action === "relay") {
      this.autoRelay(rt, fromMemberId, rule.toMemberId, text);
    } else if (rule.action === "escalate") {
      this.escalate(rt, fromMemberId, rule.toMemberId, text);
    }
  }
  /** Rule-driven relay, suppressed by the per-pair cooldown. */
  autoRelay(rt, fromMemberId, toMemberId, text) {
    const key = `${fromMemberId}:${toMemberId}`;
    const now = Date.now();
    if (now - (rt.cooldowns.get(key) ?? 0) < RELAY_COOLDOWN_MS) {
      return;
    }
    rt.cooldowns.set(key, now);
    this.doRelay(rt, toMemberId, text);
  }
  /**
   * Inject text into a member's terminal and record a relayed transcript entry
   * (relayed=true is the primary loop guard). Shared by manual + auto relay.
   */
  doRelay(rt, toMemberId, text) {
    const member = rt.session.members.find((m) => m.id === toMemberId);
    if (!member) {
      return { error: "Target member not found", ok: false };
    }
    if (!member.terminalId) {
      return {
        error: "Target member has no Shooter-owned terminal (cannot inject into an observed session)",
        ok: false
      };
    }
    if (!this.injector) {
      return { error: "Injector not configured", ok: false };
    }
    const capped = text.length > MAX_RELAY_TEXT ? text.slice(0, MAX_RELAY_TEXT) : text;
    const result = this.injector(member.terminalId, capped);
    if (!result.ok) {
      return { error: result.error ?? "Injection failed", ok: false };
    }
    this.appendEntry(rt, {
      memberId: toMemberId,
      message: {
        id: `relay-${randomBytes(6).toString("hex")}`,
        parts: [{ content: capped, type: "text" }],
        role: "user",
        timestamp: (/* @__PURE__ */ new Date()).toISOString()
      },
      provider: member.provider,
      relayed: true
    });
    return { ok: true };
  }
  emit(rt, msg) {
    for (const listener of rt.listeners) {
      try {
        listener(msg);
      } catch (err) {
        console.error("[sos] listener error:", err);
      }
    }
  }
  /** Queue an escalated relay for human approval and notify listeners. */
  escalate(rt, fromMemberId, toMemberId, text) {
    const relayId = randomBytes(8).toString("hex");
    const expiresAt = new Date(Date.now() + ESCALATE_TIMEOUT_MS).toISOString();
    const timer = setTimeout(() => {
      this.resolvePending(rt, relayId, "expired");
    }, ESCALATE_TIMEOUT_MS);
    if (typeof timer.unref === "function") {
      timer.unref();
    }
    rt.pending.set(relayId, {
      createdAt: (/* @__PURE__ */ new Date()).toISOString(),
      expiresAt,
      fromMemberId,
      id: relayId,
      text,
      timer,
      toMemberId
    });
    this.emit(rt, {
      expiresAt,
      fromMemberId,
      preview: text.slice(0, 200),
      relayId,
      toMemberId,
      type: "sos-relay-pending"
    });
  }
  resolvePending(rt, relayId, decision) {
    const pending = rt.pending.get(relayId);
    if (!pending) {
      return;
    }
    if (pending.timer) {
      clearTimeout(pending.timer);
    }
    rt.pending.delete(relayId);
    this.emit(rt, { decision, relayId, type: "sos-relay-resolved" });
  }
  /** Replay history + subscribe a single member to the merged transcript. */
  subscribeMember(rt, member) {
    if (!this.watcher) {
      return;
    }
    try {
      for (const message of this.watcher.getHistory(member.sessionKey)) {
        this.appendEntry(rt, {
          memberId: member.id,
          message,
          provider: member.provider,
          relayed: false
        });
      }
    } catch (err) {
      console.error(`[sos] history replay failed for ${member.sessionKey}:`, err);
    }
    try {
      const unsub = this.watcher.subscribe(member.sessionKey, (messages) => {
        for (const message of messages) {
          this.appendEntry(rt, {
            memberId: member.id,
            message,
            provider: member.provider,
            relayed: false
          });
          this.applyPolicy(rt, member.id, textOf(message));
        }
      });
      rt.unsubscribes.set(member.id, unsub);
    } catch (err) {
      console.error(`[sos] subscribe failed for ${member.sessionKey}:`, err);
    }
  }
}
function textOf(message) {
  return message.parts.filter((p) => p.type === "text").map((p) => p.type === "text" ? p.content : "").join(" ").trim();
}
const SC_GLOBAL_KEY = "__shooter_sos_coordinator";
const sosCoordinator = globalThis[SC_GLOBAL_KEY] || new SuperSessionCoordinator();
globalThis[SC_GLOBAL_KEY] = sosCoordinator;

export { sosCoordinator as s };
//# sourceMappingURL=coordinator-DMU_ADXf.js.map
