import {
  readSummarySnapshot,
  upsertSummarySnapshot,
  writeSummarySnapshot
} from "./chunk-ETOW6ACV.js";
import {
  ModelRegistry
} from "./chunk-FEMOX5AD.js";
import {
  LocalLlmClient
} from "./chunk-R2XRID2N.js";
import {
  FallbackLlmClient
} from "./chunk-CRU27Q4J.js";
import {
  extractJsonCandidates
} from "./chunk-UZB5KHKX.js";
import {
  log
} from "./chunk-2ODBA7MQ.js";

// src/summarizer.ts
import { mkdir, readFile, writeFile, readdir } from "fs/promises";
import path from "path";
import { z } from "zod";
var HourlySummarySchema = z.object({
  bullets: z.array(z.string()).describe("3-5 bullet points summarizing the hour's activity")
});
var HourlySummaryExtendedSchema = z.object({
  topics: z.array(z.string()).default([]),
  decisions: z.array(z.string()).default([]),
  actionItems: z.array(z.string()).default([]),
  rejected: z.array(z.string()).default([])
});
var HourlySummarizer = class {
  summariesDir;
  config;
  localLlm;
  fallbackLlm;
  modelRegistry;
  transcript;
  constructor(config, gatewayConfig, modelRegistry, transcript) {
    this.config = config;
    this.summariesDir = path.join(config.memoryDir, "summaries", "hourly");
    this.modelRegistry = modelRegistry ?? new ModelRegistry(config.memoryDir);
    this.transcript = transcript;
    this.localLlm = new LocalLlmClient(config, this.modelRegistry);
    this.fallbackLlm = new FallbackLlmClient(gatewayConfig);
    if (!gatewayConfig?.agents?.defaults?.model?.primary && !config.localLlmEnabled && config.modelSource !== "gateway") {
      log.warn("no gateway default AI and local LLM disabled \u2014 hourly summarization disabled");
    }
  }
  get useGatewayModelSource() {
    return this.config.modelSource === "gateway";
  }
  get shouldUseLocalLlm() {
    return this.config.localLlmEnabled && !this.useGatewayModelSource;
  }
  withGatewayAgent(options) {
    if (!this.useGatewayModelSource) return options;
    const agentId = this.config.gatewayAgentId || void 0;
    return agentId ? { ...options, agentId } : options;
  }
  async initialize() {
    await mkdir(this.summariesDir, { recursive: true });
    log.info("hourly summarizer initialized");
  }
  // Generate summary for a specific hour and session
  async generateSummary(sessionKey, hourStart, entries) {
    if (entries.length === 0) return null;
    const conversation = entries.map((e) => `[${e.role}] ${e.content}`).join("\n\n");
    if (this.config.hourlySummariesExtendedEnabled) {
      const extended = await this.generateExtended(sessionKey, hourStart, conversation, entries);
      if (!extended) return null;
      const meta = {
        userTurns: entries.filter((e) => e.role === "user").length,
        assistantTurns: entries.filter((e) => e.role === "assistant").length,
        toolCalls: extended._meta.toolCalls,
        toolCounts: extended._meta.toolCounts
      };
      const base = {
        hour: hourStart.toISOString(),
        sessionKey,
        bullets: extended.topics.length > 0 ? extended.topics.slice(0, 5) : ["(summary generated)"],
        turnCount: entries.length,
        generatedAt: (/* @__PURE__ */ new Date()).toISOString()
      };
      const withExtras = base;
      withExtras._extended = extended;
      withExtras._extendedMeta = meta;
      return base;
    }
    const hourIso = hourStart.toISOString();
    const startTime = Date.now();
    if (this.shouldUseLocalLlm) {
      try {
        const localResult = await this.generateWithLocalLlm(conversation);
        if (localResult) {
          const durationMs = Date.now() - startTime;
          log.debug(
            `generated hourly summary for ${sessionKey} at ${hourIso} in ${durationMs}ms using local LLM`
          );
          return {
            hour: hourIso,
            sessionKey,
            bullets: localResult.bullets,
            turnCount: entries.length,
            generatedAt: (/* @__PURE__ */ new Date()).toISOString()
          };
        }
        if (!this.config.localLlmFallback) {
          log.warn("summary generation: local LLM failed and fallback disabled");
          return null;
        }
        log.info("summary generation: local LLM unavailable, falling back to gateway default AI");
      } catch (err) {
        if (!this.config.localLlmFallback) {
          log.warn("summary generation: local LLM error and fallback disabled:", err);
          return null;
        }
        log.info("summary generation: local LLM error, falling back to gateway default AI:", err);
      }
    }
    log.info("summary generation: falling back to gateway default AI");
    try {
      const messages = [
        {
          role: "system",
          content: `You are a conversation summarization system. Summarize the following conversation transcript into 3-5 concise bullet points.

Guidelines:
- Focus on what was accomplished, decided, or discussed
- Include specific topics, projects, or entities mentioned
- Note any significant user requests or agent actions
- Keep bullets brief but informative (1-2 sentences each)
- Skip trivial greetings or meta-conversation
- Use present tense for ongoing work, past for completed items

Respond with valid JSON matching this schema:
{
  "bullets": ["bullet 1", "bullet 2", "bullet 3"]
}`
        },
        { role: "user", content: `Summarize this conversation:

${conversation}` }
      ];
      const result = await this.fallbackLlm.parseWithSchema(
        messages,
        HourlySummarySchema,
        this.withGatewayAgent({ temperature: 0.3, maxTokens: 8192 })
      );
      const durationMs = Date.now() - startTime;
      if (result) {
        log.debug(
          `generated hourly summary for ${sessionKey} at ${hourIso} in ${durationMs}ms via fallback`
        );
        return {
          hour: hourIso,
          sessionKey,
          bullets: result.bullets,
          turnCount: entries.length,
          generatedAt: (/* @__PURE__ */ new Date()).toISOString()
        };
      }
      log.warn("summary generation fallback returned no parsed output");
      return null;
    } catch (err) {
      log.error("summary generation fallback failed", err);
      return null;
    }
  }
  async generateExtended(sessionKey, hourStart, conversation, entries) {
    const hourIso = hourStart.toISOString();
    const startTime = Date.now();
    const hourEnd = new Date(hourStart.getTime() + 60 * 60 * 1e3);
    let toolCounts = {};
    if (this.config.hourlySummariesIncludeToolStats && this.transcript) {
      const uses = await this.transcript.readToolUse(sessionKey, hourStart, hourEnd);
      for (const u of uses) toolCounts[u.tool] = (toolCounts[u.tool] ?? 0) + 1;
    }
    const sys = `You are a conversation summarization system.

Summarize the hour into structured sections.

Return valid JSON matching:
{
  "topics": ["..."],
  "decisions": ["..."],
  "actionItems": ["..."],
  "rejected": ["..."]
}

Guidelines:
- Prefer concrete topics and decisions.
- Action items should be imperative.
- Rejected ideas are things that were explicitly discarded or reversed.
- If there are none for a section, return an empty array.
`;
    const toolStatsLine = Object.keys(toolCounts).length > 0 ? `Tools used (counts): ${Object.entries(toolCounts).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([k, v]) => `${k}=${v}`).join(", ")}

` : "";
    const userTurns = entries.filter((e) => e.role === "user").length;
    const assistantTurns = entries.filter((e) => e.role === "assistant").length;
    const toolCalls = Object.values(toolCounts).reduce((a, b) => a + b, 0);
    const statsLine = `Stats: userTurns=${userTurns}, assistantTurns=${assistantTurns}, toolCalls=${toolCalls}

`;
    const user = `Hour: ${hourIso}
Session: ${sessionKey}

${statsLine}${toolStatsLine}Conversation:
${conversation}
`;
    if (this.shouldUseLocalLlm) {
      try {
        const contextSizes = this.modelRegistry.calculateContextSizes(this.config.localLlmModel, this.config.localLlmMaxContext);
        const truncated = user.length > contextSizes.maxInputChars ? user.slice(0, contextSizes.maxInputChars) + "\n\n[truncated]" : user;
        const response = await this.localLlm.chatCompletion(
          [
            { role: "system", content: "Output valid JSON only." },
            { role: "user", content: sys + "\n\n" + truncated }
          ],
          {
            temperature: 0.2,
            maxTokens: contextSizes.maxOutputTokens,
            operation: "hourly_summary_extended",
            priority: "background"
          }
        );
        if (response?.content) {
          const content = response.content.trim();
          for (const candidate of extractJsonCandidates(content)) {
            try {
              const parsed = JSON.parse(candidate);
              const result = HourlySummaryExtendedSchema.parse(parsed);
              log.debug(
                `generated extended hourly summary for ${sessionKey} at ${hourIso} in ${Date.now() - startTime}ms (local)`
              );
              return { ...result, _meta: { userTurns, assistantTurns, toolCalls, toolCounts } };
            } catch {
            }
          }
        }
      } catch (err) {
        if (!this.config.localLlmFallback) return null;
        log.info("extended summary: local failed, falling back:", err);
      }
    }
    try {
      const result = await this.fallbackLlm.parseWithSchema(
        [
          { role: "system", content: sys },
          { role: "user", content: user }
        ],
        HourlySummaryExtendedSchema,
        this.withGatewayAgent({ temperature: 0.2, maxTokens: 2048 })
      );
      if (result) {
        log.debug(`generated extended hourly summary for ${sessionKey} at ${hourIso} in ${Date.now() - startTime}ms (fallback)`);
        return { ...result, _meta: { userTurns, assistantTurns, toolCalls, toolCounts } };
      }
      return null;
    } catch (err) {
      log.error("extended summary generation failed", err);
      return null;
    }
  }
  /**
   * Generate summary using local LLM with JSON mode.
   * Uses dynamic context sizing based on model capabilities.
   */
  async generateWithLocalLlm(conversation) {
    const contextSizes = this.modelRegistry.calculateContextSizes(
      this.config.localLlmModel,
      this.config.localLlmMaxContext
    );
    log.debug(`Summarizer model context: ${contextSizes.description}`);
    const maxConversationChars = contextSizes.maxInputChars;
    const truncatedConversation = conversation.length > maxConversationChars ? conversation.slice(0, maxConversationChars) + "\n\n[truncated]" : conversation;
    const instructions = `You are a conversation summarization system. Summarize the following conversation transcript into 3-5 concise bullet points.

Guidelines:
- Focus on what was accomplished, decided, or discussed
- Include specific topics, projects, or entities mentioned
- Note any significant user requests or agent actions
- Keep bullets brief but informative (1-2 sentences each)
- Skip trivial greetings or meta-conversation
- Use present tense for ongoing work, past for completed items

Respond with valid JSON matching this schema:
{
  "bullets": ["bullet 1", "bullet 2", "bullet 3"]
}`;
    const fullPrompt = `${instructions}

Conversation to summarize:
${truncatedConversation}`;
    const response = await this.localLlm.chatCompletion(
      [
        { role: "system", content: "You are a conversation summarization system. Output valid JSON only." },
        { role: "user", content: fullPrompt }
      ],
      {
        temperature: 0.3,
        maxTokens: contextSizes.maxOutputTokens,
        operation: "hourly_summary",
        priority: "background"
      }
    );
    if (!response?.content) {
      return null;
    }
    try {
      const content = response.content.trim();
      for (const candidate of extractJsonCandidates(content)) {
        try {
          const parsed = JSON.parse(candidate);
          return {
            bullets: Array.isArray(parsed.bullets) ? parsed.bullets : []
          };
        } catch {
        }
      }
      return null;
    } catch (err) {
      log.warn("local LLM summary: failed to parse JSON response:", err);
      return null;
    }
  }
  // Save summary to file
  async saveSummary(summary) {
    const sessionDir = path.join(this.summariesDir, summary.sessionKey);
    await mkdir(sessionDir, { recursive: true });
    const dateStr = summary.hour.slice(0, 10);
    const filePath = path.join(sessionDir, `${dateStr}.md`);
    const hourStr = summary.hour.slice(11, 13);
    const lines = [];
    let existingContent = "";
    try {
      existingContent = await readFile(filePath, "utf-8");
    } catch {
    }
    const hourHeader = `## ${hourStr}:00`;
    if (existingContent.includes(hourHeader)) {
      const beforeHour = existingContent.split(hourHeader)[0];
      const afterMatch = existingContent.split(hourHeader)[1];
      const afterHour = afterMatch ? afterMatch.split("\n## ")[1] : void 0;
      const newSection = this.formatHourSection(summary, hourHeader);
      if (afterHour) {
        existingContent = beforeHour + newSection + "\n## " + afterHour;
      } else {
        existingContent = beforeHour + newSection;
      }
      await writeFile(filePath, existingContent, "utf-8");
      log.debug(`updated hourly summary for ${summary.sessionKey} at ${hourStr}:00`);
    } else {
      const newSection = this.formatHourSection(summary, hourHeader);
      if (existingContent) {
        await writeFile(filePath, existingContent.trimEnd() + "\n\n" + newSection, "utf-8");
      } else {
        const header = `# Hourly Summaries \u2014 ${dateStr}

*Session: ${summary.sessionKey}*
`;
        await writeFile(filePath, header + "\n" + newSection, "utf-8");
      }
      log.debug(`saved hourly summary for ${summary.sessionKey} at ${hourStr}:00`);
    }
    try {
      await upsertSummarySnapshot(this.config.memoryDir, summary);
    } catch (error) {
      log.warn(
        `hourly summarizer: failed to update summary snapshot for ${summary.sessionKey} (fail-open): ${String(error)}`
      );
    }
  }
  formatHourSection(summary, hourHeader) {
    const ext = summary._extended;
    const meta = summary._extendedMeta;
    const lines = [hourHeader, ""];
    if (this.config.hourlySummariesExtendedEnabled && ext) {
      lines.push("### Topics Discussed");
      for (const t of ext.topics) lines.push(`- ${t}`);
      lines.push("");
      lines.push("### Decisions Made");
      for (const d of ext.decisions) lines.push(`- ${d}`);
      lines.push("");
      lines.push("### Action Items");
      for (const a of ext.actionItems) lines.push(`- ${a}`);
      lines.push("");
      lines.push("### Rejected Ideas / Reversals");
      for (const r of ext.rejected) lines.push(`- ${r}`);
      lines.push("");
      if (meta && Object.keys(meta.toolCounts).length > 0) {
        lines.push("### Tools Used");
        const top = Object.entries(meta.toolCounts).sort((a, b) => b[1] - a[1]).slice(0, 12);
        for (const [name, count] of top) lines.push(`- ${name}: ${count}`);
        lines.push("");
      }
      lines.push("### Stats");
      lines.push(`- Turns: ${summary.turnCount}`);
      if (meta) {
        lines.push(`- User turns: ${meta.userTurns}`);
        lines.push(`- Assistant turns: ${meta.assistantTurns}`);
        lines.push(`- Tool calls: ${meta.toolCalls}`);
      }
      lines.push("");
      return lines.join("\n");
    }
    for (const bullet of summary.bullets) lines.push(`- ${bullet}`);
    lines.push(`  *(${summary.turnCount} turns)*`);
    lines.push("");
    return lines.join("\n");
  }
  // Read recent summaries for recall injection
  async readRecent(sessionKey, hours) {
    try {
      const cutoffTime = Date.now() - hours * 60 * 60 * 1e3;
      const snapshot = await readSummarySnapshot(this.config.memoryDir, sessionKey);
      if (snapshot) {
        return snapshot.filter((s) => new Date(s.hour).getTime() >= cutoffTime).sort((a, b) => new Date(b.hour).getTime() - new Date(a.hour).getTime());
      }
      const sessionDir = path.join(this.summariesDir, sessionKey);
      const files = await readdir(sessionDir);
      const mdFiles = files.filter((f) => f.endsWith(".md"));
      const summaries = [];
      for (const file of mdFiles) {
        const filePath = path.join(sessionDir, file);
        const content = await readFile(filePath, "utf-8");
        const parsed = this.parseSummaryFile(content, sessionKey, file);
        summaries.push(...parsed);
      }
      const sortedSummaries = summaries.sort(
        (a, b) => new Date(b.hour).getTime() - new Date(a.hour).getTime()
      );
      const recent = sortedSummaries.filter(
        (s) => new Date(s.hour).getTime() >= cutoffTime
      );
      if (sortedSummaries.length > 0) {
        try {
          await writeSummarySnapshot(
            this.config.memoryDir,
            sessionKey,
            sortedSummaries
          );
        } catch (error) {
          log.warn(
            `hourly summarizer: failed to materialize summary snapshot for ${sessionKey} (fail-open): ${String(error)}`
          );
        }
      }
      return recent;
    } catch {
      return [];
    }
  }
  parseSummaryFile(content, sessionKey, filename) {
    const summaries = [];
    const dateMatch = filename.match(/^(\d{4}-\d{2}-\d{2})\.md$/);
    if (!dateMatch) return summaries;
    const dateStr = dateMatch[1];
    const hourSections = content.split(/\n## (\d{2}):00\n/);
    for (let i = 1; i < hourSections.length; i += 2) {
      const hourStr = hourSections[i];
      const sectionContent = hourSections[i + 1] || "";
      const bullets = [];
      const lines = sectionContent.split("\n");
      let turnCount = 0;
      let inTopics = false;
      let sawExtendedTopicsHeader = false;
      for (const line of lines) {
        if (line.startsWith("### Topics")) {
          inTopics = true;
          sawExtendedTopicsHeader = true;
          continue;
        }
        if (line.startsWith("### ") && !line.startsWith("### Topics")) {
          inTopics = false;
        }
        const bulletMatch = line.match(/^- (.+)$/);
        if (bulletMatch) {
          if (!sawExtendedTopicsHeader || inTopics) bullets.push(bulletMatch[1]);
        }
        const turnMatch = line.match(/\((\d+) turns?\)/);
        if (turnMatch) turnCount = parseInt(turnMatch[1], 10);
      }
      if (bullets.length > 0) {
        summaries.push({
          hour: `${dateStr}T${hourStr}:00:00.000Z`,
          sessionKey,
          bullets,
          turnCount,
          generatedAt: ""
          // Not stored in file, not needed for recall
        });
      }
    }
    return summaries;
  }
  // Format summaries for recall injection
  formatForRecall(summaries, maxCount) {
    if (summaries.length === 0) return "";
    const limited = summaries.slice(0, maxCount);
    const lines = [`## Recent Activity (last ${limited.length} hours)`];
    for (const summary of limited) {
      const hourStr = summary.hour.slice(11, 16);
      for (const bullet of summary.bullets) {
        lines.push(`- ${hourStr}: ${bullet}`);
      }
    }
    return lines.join("\n");
  }
  // Main entry point for cron job
  async runHourly() {
    log.debug("running hourly summary generation");
    const sessions = await this.getActiveSessions();
    for (const sessionKey of sessions) {
      const now = /* @__PURE__ */ new Date();
      const hourStart = new Date(now.getTime() - 60 * 60 * 1e3);
      hourStart.setMinutes(0, 0, 0);
      const hourEnd = new Date(hourStart.getTime() + 60 * 60 * 1e3);
      const entries = await this.getTranscriptEntries(sessionKey, hourStart, hourEnd);
      if (entries.length === 0) {
        log.debug(`no transcript entries for ${sessionKey} at ${hourStart.toISOString()}`);
        continue;
      }
      const summary = await this.generateSummary(sessionKey, hourStart, entries);
      if (summary) {
        await this.saveSummary(summary);
        log.info(`generated hourly summary for ${sessionKey} (${entries.length} turns)`);
      }
    }
  }
  // Get list of active sessions from transcript directory
  async getActiveSessions() {
    const transcriptDir = path.join(this.config.memoryDir, "transcripts");
    try {
      const sessionKeys = /* @__PURE__ */ new Set();
      const typeEntries = await readdir(transcriptDir, { withFileTypes: true });
      for (const typeEnt of typeEntries) {
        if (!typeEnt.isDirectory()) continue;
        const typeDir = path.join(transcriptDir, typeEnt.name);
        const idEntries = await readdir(typeDir, { withFileTypes: true });
        for (const idEnt of idEntries) {
          if (!idEnt.isDirectory()) continue;
          const chanDir = path.join(typeDir, idEnt.name);
          const files = (await readdir(chanDir)).filter((f) => f.endsWith(".jsonl")).sort();
          const last = files[files.length - 1];
          if (!last) continue;
          try {
            const raw = await readFile(path.join(chanDir, last), "utf-8");
            const firstLine = raw.split("\n").find((l) => l.trim().length > 0);
            if (!firstLine) continue;
            const entry = JSON.parse(firstLine);
            if (typeof entry.sessionKey === "string" && entry.sessionKey.length > 0) {
              sessionKeys.add(entry.sessionKey);
            }
          } catch {
          }
        }
      }
      return Array.from(sessionKeys);
    } catch {
      return [];
    }
  }
  // Get transcript entries for a session within a time range
  async getTranscriptEntries(sessionKey, startTime, endTime) {
    const parts = sessionKey.split(":");
    let channelType = "other";
    let channelId = "default";
    if (parts.length >= 3) {
      channelType = parts[2];
      if (channelType === "main") {
        channelId = "default";
      } else if (channelType === "discord" && parts.length >= 5 && parts[3] === "channel") {
        channelId = parts[4];
      } else if (channelType === "slack" && parts.length >= 5 && parts[3] === "channel") {
        channelId = parts[4];
      } else if (channelType === "cron" && parts.length >= 4) {
        channelId = parts[3];
      } else if (parts.length >= 4) {
        channelId = parts[3];
      }
    }
    const transcriptDir = path.join(this.config.memoryDir, "transcripts", channelType, channelId);
    try {
      const files = await readdir(transcriptDir);
      const entries = [];
      for (const file of files) {
        if (!file.endsWith(".jsonl")) continue;
        const transcriptPath = path.join(transcriptDir, file);
        try {
          const content = await readFile(transcriptPath, "utf-8");
          const lines = content.trim().split("\n");
          for (const line of lines) {
            if (!line.trim()) continue;
            try {
              const entry = JSON.parse(line);
              const entryTime = new Date(entry.timestamp).getTime();
              if (entryTime >= startTime.getTime() && entryTime < endTime.getTime()) {
                entries.push(entry);
              }
            } catch {
            }
          }
        } catch {
        }
      }
      return entries;
    } catch {
      return [];
    }
  }
};

export {
  HourlySummarizer
};
//# sourceMappingURL=chunk-B2TL6GA2.js.map