// src/tmt.ts
import * as fs from "fs";
import * as path from "path";
import { mkdir, readFile, writeFile, readdir } from "fs/promises";
var TMT_DIR = "tmt";
var TMT_LEVEL_INPUT_LIMITS = {
  hour: { totalChars: 48e3, itemChars: 2e3, maxItems: 64 },
  day: { totalChars: 64e3, itemChars: 1800, maxItems: 96 },
  week: { totalChars: 72e3, itemChars: 1600, maxItems: 120 },
  persona: { totalChars: 8e4, itemChars: 1600, maxItems: 120 }
};
function capTmtSummaryInputs(inputs, level) {
  const { totalChars, itemChars, maxItems } = TMT_LEVEL_INPUT_LIMITS[level];
  if (inputs.length === 0) return [];
  const capped = [];
  let usedChars = 0;
  for (const raw of inputs) {
    if (capped.length >= maxItems || usedChars >= totalChars) break;
    const trimmed = raw.trim();
    if (!trimmed) continue;
    const next = trimmed.length > itemChars ? `${trimmed.slice(0, itemChars - 1)}\u2026` : trimmed;
    const separator = capped.length === 0 ? 0 : 2;
    if (usedChars + separator + next.length > totalChars) break;
    capped.push(next);
    usedChars += separator + next.length;
  }
  if (capped.length > 0) return capped;
  const fallback = inputs[0]?.trim() ?? "";
  if (!fallback) return [];
  return [fallback.length > itemChars ? `${fallback.slice(0, itemChars - 1)}\u2026` : fallback];
}
function tmtDir(baseDir) {
  return path.join(baseDir, TMT_DIR);
}
function hourNodePath(baseDir, date, hour) {
  return path.join(tmtDir(baseDir), date, `hour-${hour}.md`);
}
function dayNodePath(baseDir, date) {
  return path.join(tmtDir(baseDir), date, "day.md");
}
function weekNodePath(baseDir, weekKey) {
  return path.join(tmtDir(baseDir), `week-${weekKey}.md`);
}
function personaNodePath(baseDir) {
  return path.join(tmtDir(baseDir), "persona.md");
}
function serialiseTmtNode(fm, summary) {
  const yaml = [
    "---",
    `level: ${fm.level}`,
    `periodStart: "${fm.periodStart}"`,
    `periodEnd: "${fm.periodEnd}"`,
    `memoryCount: ${fm.memoryCount}`,
    `sourceIds: [${fm.sourceIds.map((id) => `"${id}"`).join(", ")}]`,
    `builtAt: "${fm.builtAt}"`,
    "---"
  ].join("\n");
  return `${yaml}

${summary}
`;
}
function parseIsoDate(iso) {
  return iso.slice(0, 10);
}
function parseIsoHour(iso) {
  return iso.slice(11, 13);
}
function isoWeekKey(date) {
  const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
  const dayNum = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - dayNum);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  const weekNo = Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
  return `${d.getUTCFullYear()}-${String(weekNo).padStart(2, "0")}`;
}
var TmtBuilder = class {
  constructor(baseDir, cfg) {
    this.baseDir = baseDir;
    this.cfg = cfg;
  }
  baseDir;
  cfg;
  /**
   * Called after each consolidation pass.
   * Groups memories by hour, builds any missing hour nodes,
   * then rolls up to day → week → persona if needed.
   * All errors are caught and logged; never throws.
   */
  async maybeRebuildNodes(memories, summarize) {
    if (!this.cfg.temporalMemoryTreeEnabled || memories.length === 0) return;
    try {
      await mkdir(tmtDir(this.baseDir), { recursive: true });
      await this.buildHourNodes(memories, summarize);
      await this.buildDayNodes(memories, summarize);
      await this.buildWeekNodes(memories, summarize);
      await this.buildPersonaNode(summarize);
    } catch (err) {
      console.warn(`[engram] tmt: rebuild failed (ignored): ${err}`);
    }
  }
  async buildHourNodes(memories, summarize) {
    const byHour = /* @__PURE__ */ new Map();
    for (const m of memories) {
      const date = parseIsoDate(m.created);
      const hour = parseIsoHour(m.created);
      const key = `${date}::${hour}`;
      if (!byHour.has(key)) byHour.set(key, []);
      byHour.get(key).push(m);
    }
    for (const [key, entries] of byHour) {
      if (entries.length < this.cfg.tmtHourlyMinMemories) continue;
      const [date, hour] = key.split("::");
      const nodePath = hourNodePath(this.baseDir, date, hour);
      let shouldBuild = !fs.existsSync(nodePath);
      if (!shouldBuild) {
        try {
          const existing = await readFile(nodePath, "utf8");
          const countMatch = existing.match(/memoryCount: (\d+)/);
          if (!countMatch || parseInt(countMatch[1], 10) < entries.length) {
            shouldBuild = true;
          }
        } catch {
          shouldBuild = true;
        }
      }
      if (!shouldBuild) continue;
      let summary;
      try {
        summary = await summarize(capTmtSummaryInputs(entries.map((e) => e.content), "hour"), "hour");
      } catch (err) {
        console.warn(`[engram] tmt: hour node summarize failed for ${key} (ignored): ${err}`);
        continue;
      }
      const sortedCreated = entries.map((e) => e.created).sort();
      const fm = {
        level: "hour",
        periodStart: sortedCreated[0],
        periodEnd: sortedCreated[sortedCreated.length - 1],
        memoryCount: entries.length,
        sourceIds: entries.map((e) => e.id),
        builtAt: (/* @__PURE__ */ new Date()).toISOString()
      };
      await mkdir(path.dirname(nodePath), { recursive: true });
      await writeFile(nodePath, serialiseTmtNode(fm, summary), "utf8");
    }
  }
  async buildDayNodes(memories, summarize) {
    const byDate = /* @__PURE__ */ new Map();
    for (const m of memories) {
      const date = parseIsoDate(m.created);
      if (!byDate.has(date)) byDate.set(date, []);
      byDate.get(date).push(m);
    }
    for (const [date, entries] of byDate) {
      const nodePath = dayNodePath(this.baseDir, date);
      let shouldBuild = !fs.existsSync(nodePath);
      if (!shouldBuild) {
        try {
          const existing = await readFile(nodePath, "utf8");
          const countMatch = existing.match(/memoryCount: (\d+)/);
          if (!countMatch || parseInt(countMatch[1], 10) < entries.length) {
            shouldBuild = true;
          }
        } catch {
          shouldBuild = true;
        }
      }
      if (!shouldBuild) continue;
      const inputs = [];
      const hourToEntries = /* @__PURE__ */ new Map();
      for (const e of entries) {
        const h = parseIsoHour(e.created);
        if (!hourToEntries.has(h)) hourToEntries.set(h, []);
        hourToEntries.get(h).push(e);
      }
      for (const [h, hourEntries] of hourToEntries) {
        const hPath = hourNodePath(this.baseDir, date, h);
        if (fs.existsSync(hPath)) {
          try {
            const hContent = await readFile(hPath, "utf8");
            const hSummary = hContent.replace(/^---[\s\S]*?---\n\n?/, "").trim();
            if (hSummary) {
              inputs.push(hSummary);
              continue;
            }
          } catch {
          }
        }
        inputs.push(...hourEntries.map((e) => e.content));
      }
      if (inputs.length === 0) inputs.push(...entries.map((e) => e.content));
      let summary;
      try {
        summary = await summarize(capTmtSummaryInputs(inputs, "day"), "day");
      } catch (err) {
        console.warn(`[engram] tmt: day node summarize failed for ${date} (ignored): ${err}`);
        continue;
      }
      const sortedCreated = entries.map((e) => e.created).sort();
      const fm = {
        level: "day",
        periodStart: sortedCreated[0],
        periodEnd: sortedCreated[sortedCreated.length - 1],
        memoryCount: entries.length,
        sourceIds: entries.map((e) => e.id),
        builtAt: (/* @__PURE__ */ new Date()).toISOString()
      };
      await mkdir(path.dirname(nodePath), { recursive: true });
      await writeFile(nodePath, serialiseTmtNode(fm, summary), "utf8");
    }
  }
  /**
   * Build or update week-level nodes from the day nodes already on disk.
   * Groups memories by ISO week; for each week that has a day node but no
   * (or stale) week node, reads day node summaries and builds a week node.
   * Fail-open: errors per week are caught and skipped.
   */
  async buildWeekNodes(memories, summarize) {
    const weekToEntries = /* @__PURE__ */ new Map();
    for (const m of memories) {
      const week = isoWeekKey(new Date(m.created));
      if (!weekToEntries.has(week)) weekToEntries.set(week, []);
      weekToEntries.get(week).push(m);
    }
    for (const [week, entries] of weekToEntries) {
      const nodePath = weekNodePath(this.baseDir, week);
      let shouldBuild = !fs.existsSync(nodePath);
      if (!shouldBuild) {
        try {
          const existing = await readFile(nodePath, "utf8");
          const countMatch = existing.match(/memoryCount: (\d+)/);
          if (!countMatch || parseInt(countMatch[1], 10) < entries.length) {
            shouldBuild = true;
          }
        } catch {
          shouldBuild = true;
        }
      }
      if (!shouldBuild) continue;
      try {
        const dir = tmtDir(this.baseDir);
        let allDirs = [];
        try {
          allDirs = await readdir(dir);
        } catch {
          continue;
        }
        const dateDirs = allDirs.filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d));
        const daySummaries = [];
        for (const dateDir of dateDirs) {
          const w = isoWeekKey(/* @__PURE__ */ new Date(dateDir + "T00:00:00Z"));
          if (w !== week) continue;
          const dayPath = dayNodePath(this.baseDir, dateDir);
          if (fs.existsSync(dayPath)) {
            try {
              const content = await readFile(dayPath, "utf8");
              const summary2 = content.replace(/^---[\s\S]*?---\n\n?/, "").trim();
              if (summary2) daySummaries.push(summary2);
            } catch {
            }
          }
        }
        const inputs = daySummaries.length > 0 ? daySummaries : entries.map((e) => e.content);
        const summary = await summarize(capTmtSummaryInputs(inputs, "week"), "week");
        const sortedCreated = entries.map((e) => e.created).sort();
        const fm = {
          level: "week",
          periodStart: sortedCreated[0],
          periodEnd: sortedCreated[sortedCreated.length - 1],
          memoryCount: entries.length,
          sourceIds: entries.map((e) => e.id),
          builtAt: (/* @__PURE__ */ new Date()).toISOString()
        };
        await mkdir(path.dirname(nodePath), { recursive: true });
        await writeFile(nodePath, serialiseTmtNode(fm, summary), "utf8");
      } catch (err) {
        console.warn(`[engram] tmt: week node build failed for ${week} (ignored): ${err}`);
      }
    }
  }
  /**
   * Build or update the persona node from the most recent week-level summaries.
   * Reads up to 4 recent week nodes and synthesizes a persona-level narrative.
   * Fail-open: skips silently if no week nodes exist or summarize fails.
   */
  async buildPersonaNode(summarize) {
    try {
      const dir = tmtDir(this.baseDir);
      let allFiles = [];
      try {
        allFiles = await readdir(dir);
      } catch {
        return;
      }
      const weekFiles = allFiles.filter((f) => /^week-\d{4}-\d{2}\.md$/.test(f)).sort().reverse().slice(0, 4);
      if (weekFiles.length === 0) return;
      const weekSummaries = [];
      let totalCount = 0;
      let earliestStart;
      let latestEnd;
      for (const f of weekFiles) {
        try {
          const content = await readFile(path.join(dir, f), "utf8");
          const summary2 = content.replace(/^---[\s\S]*?---\n\n?/, "").trim();
          if (summary2) weekSummaries.push(summary2);
          const countMatch = content.match(/memoryCount: (\d+)/);
          if (countMatch) totalCount += parseInt(countMatch[1], 10);
          const startMatch = content.match(/periodStart: "([^"]+)"/);
          const endMatch = content.match(/periodEnd: "([^"]+)"/);
          if (startMatch) {
            if (!earliestStart || startMatch[1] < earliestStart) earliestStart = startMatch[1];
          }
          if (endMatch) {
            if (!latestEnd || endMatch[1] > latestEnd) latestEnd = endMatch[1];
          }
        } catch {
        }
      }
      if (weekSummaries.length === 0) return;
      const nodePath = personaNodePath(this.baseDir);
      let shouldBuild = !fs.existsSync(nodePath);
      if (!shouldBuild) {
        try {
          const existing = await readFile(nodePath, "utf8");
          const countMatch = existing.match(/memoryCount: (\d+)/);
          if (!countMatch || parseInt(countMatch[1], 10) !== totalCount) {
            shouldBuild = true;
          }
        } catch {
          shouldBuild = true;
        }
      }
      if (!shouldBuild) return;
      const summary = await summarize(capTmtSummaryInputs(weekSummaries, "persona"), "persona");
      const now = (/* @__PURE__ */ new Date()).toISOString();
      const fm = {
        level: "persona",
        periodStart: earliestStart ?? now,
        periodEnd: latestEnd ?? now,
        memoryCount: totalCount,
        sourceIds: [],
        builtAt: now
      };
      await writeFile(nodePath, serialiseTmtNode(fm, summary), "utf8");
    } catch (err) {
      console.warn(`[engram] tmt: persona node build failed (ignored): ${err}`);
    }
  }
  /**
   * Return the summary text of the most relevant TMT node for a given prompt.
   * Preference: day node for today > most recent day node.
   * Returns null if no nodes exist or feature is disabled.
   */
  async getMostRelevantNode() {
    if (!this.cfg.temporalMemoryTreeEnabled) return null;
    try {
      const dir = tmtDir(this.baseDir);
      if (!fs.existsSync(dir)) return null;
      const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
      const todayDay = dayNodePath(this.baseDir, today);
      if (fs.existsSync(todayDay)) {
        const content = await readFile(todayDay, "utf8");
        const summary = content.replace(/^---[\s\S]*?---\n\n?/, "").trim();
        if (summary) return { level: "day", summary };
      }
      let entries = [];
      try {
        entries = await readdir(dir);
      } catch {
        return null;
      }
      const dateDirs = entries.filter((e) => /^\d{4}-\d{2}-\d{2}$/.test(e)).sort().reverse();
      for (const dateDir of dateDirs) {
        const dayPath = dayNodePath(this.baseDir, dateDir);
        if (fs.existsSync(dayPath)) {
          const content = await readFile(dayPath, "utf8");
          const summary = content.replace(/^---[\s\S]*?---\n\n?/, "").trim();
          if (summary) return { level: "day", summary };
        }
      }
      return null;
    } catch {
      return null;
    }
  }
};

export {
  TMT_DIR,
  capTmtSummaryInputs,
  tmtDir,
  hourNodePath,
  dayNodePath,
  weekNodePath,
  personaNodePath,
  serialiseTmtNode,
  parseIsoDate,
  parseIsoHour,
  isoWeekKey,
  TmtBuilder
};
//# sourceMappingURL=chunk-TPB3I2AC.js.map