import {
  cloneDefaultSessionObserverBands
} from "./chunk-Z5LAYHGJ.js";
import {
  log
} from "./chunk-2ODBA7MQ.js";

// src/session-observer-state.ts
import path from "path";
import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
function sanitizeNonNegativeInt(value) {
  if (!Number.isFinite(value)) return 0;
  return Math.max(0, Math.floor(value));
}
function parseIsoMs(value) {
  if (!value) return 0;
  const ms = Date.parse(value);
  return Number.isFinite(ms) ? ms : 0;
}
function mergeSessionCursor(existing, incoming) {
  const existingObservedMs = parseIsoMs(existing.lastObservedAt);
  const incomingObservedMs = parseIsoMs(incoming.lastObservedAt);
  const existingTriggeredMs = parseIsoMs(existing.lastTriggeredAt);
  const incomingTriggeredMs = parseIsoMs(incoming.lastTriggeredAt);
  const existingResetMs = parseIsoMs(existing.lastResetAt);
  const incomingResetMs = parseIsoMs(incoming.lastResetAt);
  const observedAt = incomingObservedMs >= existingObservedMs ? incoming.lastObservedAt : existing.lastObservedAt;
  const triggeredAt = incomingTriggeredMs >= existingTriggeredMs ? incoming.lastTriggeredAt : existing.lastTriggeredAt;
  const incomingIsNewer = incomingObservedMs >= existingObservedMs;
  const incomingHasNewerReset = incomingResetMs > existingResetMs;
  const allowIncomingReset = incomingIsNewer && incomingHasNewerReset;
  const keepExistingReset = existingResetMs > incomingResetMs && existingObservedMs >= incomingObservedMs;
  let cursorBytes = Math.max(
    sanitizeNonNegativeInt(existing.cursorBytes),
    sanitizeNonNegativeInt(incoming.cursorBytes)
  );
  let cursorTokens = Math.max(
    sanitizeNonNegativeInt(existing.cursorTokens),
    sanitizeNonNegativeInt(incoming.cursorTokens)
  );
  if (keepExistingReset) {
    cursorBytes = sanitizeNonNegativeInt(existing.cursorBytes);
    cursorTokens = sanitizeNonNegativeInt(existing.cursorTokens);
  } else if (allowIncomingReset) {
    cursorBytes = sanitizeNonNegativeInt(incoming.cursorBytes);
    cursorTokens = sanitizeNonNegativeInt(incoming.cursorTokens);
  }
  return {
    sessionKey: existing.sessionKey,
    cursorBytes,
    cursorTokens,
    lastObservedAt: observedAt,
    lastTriggeredAt: triggeredAt,
    lastResetAt: incomingResetMs >= existingResetMs ? incoming.lastResetAt : existing.lastResetAt
  };
}
function normalizeObserverBands(bands) {
  const normalized = bands.map((band) => ({
    maxBytes: sanitizeNonNegativeInt(band.maxBytes),
    triggerDeltaBytes: sanitizeNonNegativeInt(band.triggerDeltaBytes),
    triggerDeltaTokens: sanitizeNonNegativeInt(band.triggerDeltaTokens)
  })).filter((band) => band.maxBytes > 0).sort((a, b) => a.maxBytes - b.maxBytes);
  if (normalized.length === 0) {
    return cloneDefaultSessionObserverBands();
  }
  const last = normalized[normalized.length - 1];
  if (last && last.maxBytes < 1e9) {
    normalized.push({
      maxBytes: 1e9,
      triggerDeltaBytes: last.triggerDeltaBytes,
      triggerDeltaTokens: last.triggerDeltaTokens
    });
  }
  return normalized;
}
var SessionObserverState = class {
  statePath;
  lockPath;
  lockStaleMs = 12e4;
  debounceMs;
  bands;
  sessions = /* @__PURE__ */ new Map();
  saveQueue = Promise.resolve();
  async readPersistedState() {
    try {
      const raw = await readFile(this.statePath, "utf-8");
      const parsed = JSON.parse(raw);
      if (parsed?.version !== 1 || !parsed.sessions || typeof parsed.sessions !== "object") {
        return null;
      }
      return parsed;
    } catch {
      return null;
    }
  }
  normalizePersistedSessions(sessions) {
    const next = /* @__PURE__ */ new Map();
    for (const [sessionKey, value] of Object.entries(sessions)) {
      if (!value || typeof value !== "object") continue;
      next.set(sessionKey, {
        sessionKey,
        cursorBytes: sanitizeNonNegativeInt(value.cursorBytes),
        cursorTokens: sanitizeNonNegativeInt(value.cursorTokens),
        lastObservedAt: typeof value.lastObservedAt === "string" ? value.lastObservedAt : (/* @__PURE__ */ new Date(0)).toISOString(),
        lastTriggeredAt: typeof value.lastTriggeredAt === "string" ? value.lastTriggeredAt : void 0,
        lastResetAt: typeof value.lastResetAt === "string" ? value.lastResetAt : void 0
      });
    }
    return next;
  }
  constructor(opts) {
    this.statePath = path.join(opts.memoryDir, "state", "session-observer-state.json");
    this.lockPath = path.join(opts.memoryDir, "state", "session-observer-state.lock");
    this.debounceMs = Math.max(0, Math.floor(opts.debounceMs));
    this.bands = normalizeObserverBands(opts.bands);
  }
  async withSaveLock(fn) {
    await mkdir(path.dirname(this.lockPath), { recursive: true });
    for (let attempt = 0; attempt < 80; attempt++) {
      try {
        const handle = await open(this.lockPath, "wx");
        try {
          await fn();
        } finally {
          await handle.close();
          await unlink(this.lockPath).catch(() => {
          });
        }
        return;
      } catch (err) {
        if (err?.code !== "EEXIST") throw err;
        try {
          const lockInfo = await stat(this.lockPath);
          if (Date.now() - lockInfo.mtimeMs > this.lockStaleMs) {
            await unlink(this.lockPath).catch(() => {
            });
            continue;
          }
        } catch {
        }
        await new Promise((resolve) => setTimeout(resolve, 25));
      }
    }
    const error = new Error("session observer save lock timeout");
    log.debug(error.message);
    throw error;
  }
  async load() {
    const parsed = await this.readPersistedState();
    if (!parsed) {
      this.sessions.clear();
      return;
    }
    this.sessions = this.normalizePersistedSessions(parsed.sessions);
  }
  async save() {
    await this.withSaveLock(async () => {
      const merged = /* @__PURE__ */ new Map();
      const persisted = await this.readPersistedState();
      if (persisted) {
        for (const [key, value] of this.normalizePersistedSessions(persisted.sessions).entries()) {
          merged.set(key, value);
        }
      }
      for (const [key, current] of this.sessions.entries()) {
        const existing = merged.get(key);
        if (!existing) {
          merged.set(key, current);
          continue;
        }
        merged.set(key, mergeSessionCursor(existing, current));
      }
      this.sessions = merged;
      const sessions = {};
      for (const [key, value] of merged.entries()) {
        sessions[key] = value;
      }
      const payload = { version: 1, sessions };
      await mkdir(path.dirname(this.statePath), { recursive: true });
      await writeFile(this.statePath, JSON.stringify(payload, null, 2), "utf-8");
    });
  }
  enqueueSave() {
    this.saveQueue = this.saveQueue.catch(() => void 0).then(() => this.save());
    return this.saveQueue;
  }
  bandForTotalBytes(totalBytes) {
    const bytes = sanitizeNonNegativeInt(totalBytes);
    for (const band of this.bands) {
      if (bytes <= band.maxBytes) return band;
    }
    return this.bands[this.bands.length - 1];
  }
  async observe(input) {
    const nowIso = input.observedAt ?? (/* @__PURE__ */ new Date()).toISOString();
    const totalBytes = sanitizeNonNegativeInt(input.totalBytes);
    const totalTokens = sanitizeNonNegativeInt(input.totalTokens);
    const band = this.bandForTotalBytes(totalBytes);
    const existing = this.sessions.get(input.sessionKey);
    if (!existing) {
      this.sessions.set(input.sessionKey, {
        sessionKey: input.sessionKey,
        cursorBytes: totalBytes,
        cursorTokens: totalTokens,
        lastObservedAt: nowIso
      });
      await this.enqueueSave();
      return {
        triggered: false,
        deltaBytes: 0,
        deltaTokens: 0,
        band,
        reason: "baseline"
      };
    }
    const session = { ...existing };
    if (totalBytes < session.cursorBytes || totalTokens < session.cursorTokens) {
      session.cursorBytes = totalBytes;
      session.cursorTokens = totalTokens;
      session.lastObservedAt = nowIso;
      session.lastResetAt = nowIso;
      this.sessions.set(input.sessionKey, session);
      await this.enqueueSave();
      return { triggered: false, deltaBytes: 0, deltaTokens: 0, band, reason: "baseline" };
    }
    const deltaBytes = totalBytes - session.cursorBytes;
    const deltaTokens = totalTokens - session.cursorTokens;
    const crossedThreshold = band.triggerDeltaBytes > 0 && deltaBytes >= band.triggerDeltaBytes || band.triggerDeltaTokens > 0 && deltaTokens >= band.triggerDeltaTokens;
    session.lastObservedAt = nowIso;
    if (!crossedThreshold) {
      const unchanged = deltaBytes === 0 && deltaTokens === 0;
      if (!unchanged) {
        this.sessions.set(input.sessionKey, session);
        await this.enqueueSave();
      }
      return {
        triggered: false,
        deltaBytes,
        deltaTokens,
        band
      };
    }
    const nowMs = Date.parse(nowIso);
    const lastTriggeredMs = session.lastTriggeredAt ? Date.parse(session.lastTriggeredAt) : NaN;
    const withinDebounce = Number.isFinite(lastTriggeredMs) && nowMs - lastTriggeredMs < this.debounceMs;
    if (withinDebounce) {
      this.sessions.set(input.sessionKey, session);
      await this.enqueueSave();
      return {
        triggered: false,
        deltaBytes,
        deltaTokens,
        band,
        reason: "debounced"
      };
    }
    session.lastTriggeredAt = nowIso;
    session.cursorBytes = totalBytes;
    session.cursorTokens = totalTokens;
    this.sessions.set(input.sessionKey, session);
    await this.enqueueSave();
    return {
      triggered: true,
      deltaBytes,
      deltaTokens,
      band,
      reason: "threshold"
    };
  }
};

export {
  normalizeObserverBands,
  SessionObserverState
};
//# sourceMappingURL=chunk-JR4ZC3G4.js.map