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

// src/recall-state.ts
import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
import path from "path";
import { createHash, randomUUID } from "crypto";
function clampGraphRecallExpandedEntries(entries, maxEntries = 64) {
  const limit = Math.max(1, Math.floor(maxEntries));
  if (!Array.isArray(entries)) return [];
  return entries.filter((item) => !!item && typeof item === "object").map((item) => {
    const graphType = item.graphType === "entity" || item.graphType === "time" || item.graphType === "causal" ? item.graphType : "entity";
    const out = {
      path: typeof item.path === "string" ? item.path : "",
      score: typeof item.score === "number" && Number.isFinite(item.score) ? item.score : 0,
      namespace: typeof item.namespace === "string" ? item.namespace : "",
      seed: typeof item.seed === "string" ? item.seed : "",
      hopDepth: typeof item.hopDepth === "number" && Number.isFinite(item.hopDepth) ? Math.max(0, Math.floor(item.hopDepth)) : 0,
      decayedWeight: typeof item.decayedWeight === "number" && Number.isFinite(item.decayedWeight) ? Math.max(0, item.decayedWeight) : 0,
      graphType
    };
    if (typeof item.edgeConfidence === "number" && Number.isFinite(item.edgeConfidence)) {
      out.edgeConfidence = Math.min(1, Math.max(0, item.edgeConfidence));
    }
    return out;
  }).filter((item) => item.path.length > 0 && item.namespace.length > 0).slice(0, limit);
}
function cloneTierExplain(tierExplain) {
  if (!tierExplain) return void 0;
  return structuredClone(tierExplain);
}
function cloneLastRecallSnapshot(snapshot) {
  if (!snapshot) return null;
  return structuredClone(snapshot);
}
var DEFAULT_TIER_MIGRATION_STATUS = {
  updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
  lastCycle: null,
  totals: {
    cycles: 0,
    scanned: 0,
    migrated: 0,
    promoted: 0,
    demoted: 0,
    errors: 0
  }
};
var LastRecallStore = class {
  statePath;
  impressionsPath;
  state = {};
  constructor(memoryDir) {
    this.statePath = path.join(memoryDir, "state", "last_recall.json");
    this.impressionsPath = path.join(memoryDir, "state", "recall_impressions.jsonl");
  }
  async load() {
    try {
      const raw = await readFile(this.statePath, "utf-8");
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === "object") this.state = parsed;
    } catch {
      this.state = {};
    }
  }
  get(sessionKey) {
    return cloneLastRecallSnapshot(this.state[sessionKey] ?? null);
  }
  getMostRecent() {
    const snapshots = Object.values(this.state);
    if (snapshots.length === 0) return null;
    snapshots.sort((a, b) => {
      const byTime = b.recordedAt.localeCompare(a.recordedAt);
      if (byTime !== 0) return byTime;
      return a.sessionKey.localeCompare(b.sessionKey);
    });
    return cloneLastRecallSnapshot(snapshots[0] ?? null);
  }
  /**
   * Persist last-recall snapshot and append an impression log entry.
   * Does not store raw query text; uses a stable hash for correlation.
   */
  async record(opts) {
    const now = (/* @__PURE__ */ new Date()).toISOString();
    const queryHash = createHash("sha256").update(opts.query).digest("hex");
    const liveSnapshot = {
      sessionKey: opts.sessionKey,
      recordedAt: now,
      queryHash,
      writeNonce: randomUUID(),
      queryLen: opts.query.length,
      memoryIds: opts.memoryIds,
      namespace: opts.namespace,
      traceId: opts.traceId,
      plannerMode: opts.plannerMode,
      requestedMode: opts.requestedMode,
      source: opts.source,
      fallbackUsed: opts.fallbackUsed,
      sourcesUsed: opts.sourcesUsed,
      budgetsApplied: opts.budgetsApplied,
      latencyMs: opts.latencyMs,
      resultPaths: opts.resultPaths,
      policyVersion: opts.policyVersion,
      identityInjectionMode: opts.identityInjection?.mode,
      identityInjectedChars: opts.identityInjection?.injectedChars,
      identityInjectionTruncated: opts.identityInjection?.truncated,
      tierExplain: opts.tierExplain
    };
    const snapshot = cloneLastRecallSnapshot(liveSnapshot);
    this.state[opts.sessionKey] = snapshot;
    const keys = Object.keys(this.state);
    if (keys.length > 50) {
      const ordered = keys.map((k) => ({ k, at: this.state[k]?.recordedAt ?? "" })).sort((a, b) => b.at.localeCompare(a.at));
      for (const doomed of ordered.slice(50)) {
        delete this.state[doomed.k];
      }
    }
    try {
      await mkdir(path.dirname(this.statePath), { recursive: true });
      await writeFile(this.statePath, JSON.stringify(this.state, null, 2), "utf-8");
    } catch (err) {
      log.debug(`last recall store write failed: ${err}`);
    }
    if (opts.appendImpression !== false) {
      try {
        await mkdir(path.dirname(this.impressionsPath), { recursive: true });
        await appendFile(this.impressionsPath, JSON.stringify(snapshot) + "\n", "utf-8");
      } catch (err) {
        log.debug(`recall impressions append failed: ${err}`);
      }
    }
  }
  /**
   * Attach a RecallTierExplain block to the existing snapshot for a
   * session without rewriting the entire snapshot.  Used by the
   * post-recall direct-answer annotation path (issue #518 slice 3c):
   * recallInternal records the snapshot first, then the orchestrator
   * fires the direct-answer tier in observation mode and annotates
   * the stored snapshot with whichever tier served the query.
   *
   * No-op when no snapshot exists for the given session; callers do
   * not need to guard on existence.
   */
  async annotateTierExplain(sessionKey, tierExplain, expected) {
    const current = this.state[sessionKey];
    if (!current) return;
    if (expected) {
      if (typeof expected.writeNonce === "string" && expected.writeNonce.length > 0) {
        if (current.writeNonce !== expected.writeNonce) return;
      } else {
        const hasExpectedTraceId = typeof expected.traceId === "string" && expected.traceId.length > 0;
        const traceIdMatches = hasExpectedTraceId && current.traceId === expected.traceId;
        const recordedAtMatches = expected.recordedAt !== void 0 && current.recordedAt === expected.recordedAt;
        if (hasExpectedTraceId) {
          if (!traceIdMatches) return;
        } else if (expected.recordedAt !== void 0 && !recordedAtMatches) {
          return;
        }
      }
    }
    this.state[sessionKey] = {
      ...current,
      tierExplain: cloneTierExplain(tierExplain)
    };
    try {
      await mkdir(path.dirname(this.statePath), { recursive: true });
      await writeFile(this.statePath, JSON.stringify(this.state, null, 2), "utf-8");
    } catch (err) {
      log.debug(`last recall tier-explain annotate failed: ${err}`);
    }
  }
};
var TierMigrationStatusStore = class {
  statePath;
  state = structuredClone(DEFAULT_TIER_MIGRATION_STATUS);
  constructor(memoryDir) {
    this.statePath = path.join(memoryDir, "state", "tier-migration-status.json");
  }
  async load() {
    try {
      const raw = await readFile(this.statePath, "utf-8");
      const parsed = JSON.parse(raw);
      if (!parsed || typeof parsed !== "object") {
        this.state = structuredClone(DEFAULT_TIER_MIGRATION_STATUS);
        return;
      }
      const totals = parsed.totals && typeof parsed.totals === "object" ? parsed.totals : DEFAULT_TIER_MIGRATION_STATUS.totals;
      this.state = {
        updatedAt: typeof parsed.updatedAt === "string" && parsed.updatedAt.length > 0 ? parsed.updatedAt : DEFAULT_TIER_MIGRATION_STATUS.updatedAt,
        lastCycle: parsed.lastCycle && typeof parsed.lastCycle === "object" ? parsed.lastCycle : null,
        totals: {
          cycles: typeof totals.cycles === "number" && Number.isFinite(totals.cycles) ? totals.cycles : 0,
          scanned: typeof totals.scanned === "number" && Number.isFinite(totals.scanned) ? totals.scanned : 0,
          migrated: typeof totals.migrated === "number" && Number.isFinite(totals.migrated) ? totals.migrated : 0,
          promoted: typeof totals.promoted === "number" && Number.isFinite(totals.promoted) ? totals.promoted : 0,
          demoted: typeof totals.demoted === "number" && Number.isFinite(totals.demoted) ? totals.demoted : 0,
          errors: typeof totals.errors === "number" && Number.isFinite(totals.errors) ? totals.errors : 0
        }
      };
    } catch {
      this.state = structuredClone(DEFAULT_TIER_MIGRATION_STATUS);
    }
  }
  get() {
    return {
      updatedAt: this.state.updatedAt,
      lastCycle: this.state.lastCycle ? { ...this.state.lastCycle } : null,
      totals: { ...this.state.totals }
    };
  }
  async recordCycle(summary) {
    const now = (/* @__PURE__ */ new Date()).toISOString();
    const migratedDelta = summary.dryRun ? 0 : Math.max(0, summary.migrated);
    const promotedDelta = summary.dryRun ? 0 : Math.max(0, summary.promoted);
    const demotedDelta = summary.dryRun ? 0 : Math.max(0, summary.demoted);
    const next = {
      updatedAt: now,
      lastCycle: { ...summary },
      totals: {
        cycles: this.state.totals.cycles + 1,
        scanned: this.state.totals.scanned + Math.max(0, summary.scanned),
        migrated: this.state.totals.migrated + migratedDelta,
        promoted: this.state.totals.promoted + promotedDelta,
        demoted: this.state.totals.demoted + demotedDelta,
        errors: this.state.totals.errors + Math.max(0, summary.errorCount ?? 0)
      }
    };
    this.state = next;
    try {
      await mkdir(path.dirname(this.statePath), { recursive: true });
      await writeFile(this.statePath, JSON.stringify(next, null, 2), "utf-8");
    } catch (err) {
      log.debug(`tier migration status write failed: ${err}`);
    }
  }
};

export {
  clampGraphRecallExpandedEntries,
  LastRecallStore,
  TierMigrationStatusStore
};
//# sourceMappingURL=chunk-WPGJYVUH.js.map