import { d as getBrainNativeDb } from './sqlite-Dsbe0wec.js';
import { isEmbeddingAvailable, embedText } from './brain-embedding-Djw9ekXu.js';
import './chunk-DDP5xfzh.js';
import 'node:module';
import './paths-2M_UxD_H.js';
import 'node:fs';
import 'node:path';
import 'node:os';
import 'node:events';
import 'node:diagnostics_channel';
import 'fs';
import 'events';
import 'util';
import 'path';
import 'assert';
import 'worker_threads';
import 'module';
import 'node:url';
import 'url';
import 'buffer';
import 'node:child_process';
import 'node:crypto';
import 'node:process';
import 'node:async_hooks';
import './dual-scope-db-ATrwq0LH.js';
import './indexes-aYlULWG3.js';
import './utils-BvHPJOVk.js';
import './conduit-Ca_OFWR_.js';
import './unique-constraint-BM-Apx1H.js';
import './memory-schema-Bi7cChwo.js';
import './agent-registry-BtE5uhMO.js';
import './nexus-sqlite2-DgTuv9BQ.js';
import './nexus-schema2-BEd-ol6e.js';
import './logger-ChCwlV5k.js';
import 'node:sqlite';
import './lock-BoHF25mx.js';
import './index-IVoIz4D5.js';
import 'constants';
import 'stream';

//#region ../core/dist/memory/session-narrative.js
/**
* Rolling Session Narrative — Per-session conversation summary store.
*
* Maintains a compact, rolling prose summary of what has happened in a CLEO
* session.  Each dialectic turn can contribute a `sessionNarrativeDelta` that
* is appended and compressed into the rolling `narrative` field.
*
* ## Storage
*
* Data is persisted to the `session_narrative` table in brain.db.  The table
* is created on first write via `CREATE TABLE IF NOT EXISTS` so the module
* works safely before the formal ADR-054 Drizzle migration is applied.
* The Drizzle migration (`20260423000002_t1089-add-session-narrative-table`)
* generated by `pnpm db:new -- --db brain --task T1089 --name add-session-narrative-table`
* is the canonical source for the schema; the inline DDL here is a verbatim copy
* kept for zero-dep startup resilience.
*
* ## Schema
*
* ```
* session_narrative (
*   session_id     TEXT PRIMARY KEY,
*   narrative      TEXT NOT NULL,       -- Rolling summary, max 2000 chars
*   turn_count     INTEGER NOT NULL DEFAULT 0,
*   last_updated_at INTEGER NOT NULL,   -- Unix epoch milliseconds
*   pivot_count    INTEGER NOT NULL DEFAULT 0
* )
* ```
*
* ## Pivot Detection
*
* `detectPivot(delta, existingNarrative)` uses cosine similarity over text
* embeddings when the sqlite-vec extension and an embedding provider are both
* available.  A pivot is signalled when the cosine similarity between the two
* texts falls below `PIVOT_COSINE_THRESHOLD`.
*
* When embeddings are unavailable the function falls back to a keyword-overlap
* heuristic: a topic shift is detected when the new delta contains fewer than
* `PIVOT_KEYWORD_OVERLAP_THRESHOLD` of its tokens in the existing narrative.
*
* ## PSYCHE Reference
*
* Inspired by `upstream psyche-lineage · deriver/deriver.py` (session state
* derivation) — PSYCHE maintains a rolling representation of what a session
* "is about" that informs future retrieval and sigil generation.
*
* @task T1531
* @epic T1056
*/
/** Maximum character length for the rolling narrative field. */
var MAX_NARRATIVE_CHARS = 2e3;
/**
* Cosine similarity threshold below which a turn is classified as a pivot
* when embedding-based detection is available (sqlite-vec + embedding provider
* both loaded).
*
* A score of 0.0 means orthogonal vectors (completely unrelated topics);
* 1.0 means identical direction (same topic).  Empirically, topic shifts
* tend to produce similarity < 0.3.
*
* **Tradeoffs vs keyword fallback**:
* - Higher accuracy on paraphrase and synonym pivots the keyword heuristic
*   misses (e.g. "switch to Rust" vs "rewrite in a systems language").
* - Requires the embedding model to be loaded (~50–80 MB), which adds latency
*   on first use and is unavailable in cold environments.
* - The fallback keyword heuristic is zero-cost and always available but
*   cannot detect pivots expressed through semantically related but
*   lexically different phrasing.
*
* @task T1531
*/
var PIVOT_COSINE_THRESHOLD = .3;
/**
* Keyword-overlap threshold used by the fallback heuristic when embeddings
* are unavailable.  When fewer than this fraction of delta keywords appear in
* the existing narrative, the topic is considered to have shifted.
*
* Kept alongside `PIVOT_COSINE_THRESHOLD` so callers and tests can reference
* both strategies explicitly.
*/
var PIVOT_KEYWORD_OVERLAP_THRESHOLD = .5;
/** Whether the `session_narrative` table has been verified / created. */
var _tableEnsured = false;
/**
* Ensure the `session_narrative` table exists in brain.db.
*
* Uses `CREATE TABLE IF NOT EXISTS` so this is idempotent and safe to call
* multiple times.  After the formal Drizzle migration lands the DDL is
* redundant but harmless.
*/
function ensureTable() {
	const db = getBrainNativeDb();
	if (!db) throw new Error("session-narrative: brain.db native handle not available. Call getBrainDb() at startup to initialise the database.");
	if (!_tableEnsured) {
		db.exec(`
      CREATE TABLE IF NOT EXISTS session_narrative (
        session_id      TEXT PRIMARY KEY,
        narrative       TEXT NOT NULL DEFAULT '',
        turn_count      INTEGER NOT NULL DEFAULT 0,
        last_updated_at INTEGER NOT NULL DEFAULT 0,
        pivot_count     INTEGER NOT NULL DEFAULT 0
      );
    `);
		_tableEnsured = true;
	}
	return db;
}
/**
* Compute the cosine similarity between two Float32 vectors.
*
* Returns a value in [-1, 1] where 1 means identical direction and 0 means
* orthogonal.  Returns 0 when either vector has zero magnitude to avoid
* division by zero.
*
* @param a - First embedding vector.
* @param b - Second embedding vector (must be same length as `a`).
* @returns Cosine similarity in [-1, 1].
*
* @task T1531
*/
function cosineSimilarity(a, b) {
	let dot = 0;
	let magA = 0;
	let magB = 0;
	for (let i = 0; i < a.length; i++) {
		dot += (a[i] ?? 0) * (b[i] ?? 0);
		magA += (a[i] ?? 0) ** 2;
		magB += (b[i] ?? 0) ** 2;
	}
	const denom = Math.sqrt(magA) * Math.sqrt(magB);
	return denom === 0 ? 0 : dot / denom;
}
/**
* Retrieve the current narrative record for a session.
*
* Returns `null` when the session has no narrative entry yet.
*
* @param sessionId - The session identifier to look up.
* @returns The narrative record, or `null` if not found.
*
* @example
* ```ts
* const record = await getSessionNarrative('ses_20260422131135_5149eb');
* console.log(record?.narrative ?? '(no narrative yet)');
* ```
*
* @task T1089
*/
async function getSessionNarrative(sessionId) {
	const row = ensureTable().prepare("SELECT session_id, narrative, turn_count, last_updated_at, pivot_count FROM session_narrative WHERE session_id = ?").get(sessionId);
	if (!row) return null;
	return {
		sessionId: row.session_id,
		narrative: row.narrative,
		turnCount: row.turn_count,
		lastUpdatedAt: row.last_updated_at,
		pivotCount: row.pivot_count
	};
}
/**
* Append a narrative delta to a session's rolling summary.
*
* The delta is prepended with a space separator if the existing narrative is
* non-empty.  When the combined result exceeds `MAX_NARRATIVE_CHARS` the oldest
* content is trimmed from the left (keeping the most recent context).
*
* Side effect: increments `turn_count` by one and updates `pivot_count` when
* `detectPivot(delta)` returns `true` against the existing narrative.
*
* @param sessionId   - The session to update.
* @param delta       - New narrative content from the current turn.
* @param _projectRoot - Project root (reserved for future use; unused now).
*
* @example
* ```ts
* await appendNarrativeDelta(
*   'ses_20260422131135_5149eb',
*   'User requested zero-dep implementation; agent proposed using existing buildRetrievalBundle.',
*   '/mnt/projects/cleocode',
* );
* ```
*
* @task T1089
*/
async function appendNarrativeDelta(sessionId, delta, _projectRoot) {
	if (!delta.trim()) return;
	const db = ensureTable();
	const now = Date.now();
	const existing = await getSessionNarrative(sessionId);
	let newNarrative;
	let newPivotCount;
	if (!existing) {
		newNarrative = delta.slice(0, MAX_NARRATIVE_CHARS);
		newPivotCount = 0;
	} else {
		const isPivot = await detectPivot(delta, existing.narrative);
		newPivotCount = existing.pivotCount + (isPivot ? 1 : 0);
		const combined = existing.narrative ? `${existing.narrative} ${delta}` : delta;
		newNarrative = combined.length <= MAX_NARRATIVE_CHARS ? combined : combined.slice(combined.length - MAX_NARRATIVE_CHARS);
	}
	const newTurnCount = (existing?.turnCount ?? 0) + 1;
	db.prepare(`
    INSERT INTO session_narrative (session_id, narrative, turn_count, last_updated_at, pivot_count)
    VALUES (?, ?, ?, ?, ?)
    ON CONFLICT(session_id) DO UPDATE SET
      narrative       = excluded.narrative,
      turn_count      = excluded.turn_count,
      last_updated_at = excluded.last_updated_at,
      pivot_count     = excluded.pivot_count
  `).run(sessionId, newNarrative, newTurnCount, now, newPivotCount);
}
/**
* Detect whether a new narrative delta represents a significant topic pivot.
*
* ## Strategy selection
*
* **Embedding path** (preferred): when an embedding provider is available
* (via `isEmbeddingAvailable()`), both `delta` and `existingNarrative` are
* embedded into float vectors and compared with cosine similarity.  A score
* below `PIVOT_COSINE_THRESHOLD` (default 0.3) indicates a pivot.  This path
* catches paraphrase pivots and synonym-based topic changes that the keyword
* heuristic misses, at the cost of requiring the embedding model to be loaded.
*
* **Keyword fallback** (no embedding provider / vec extension): if
* `embedText()` returns `null` for either input, the function falls back to a
* keyword-overlap heuristic.  A pivot is declared when fewer than
* `PIVOT_KEYWORD_OVERLAP_THRESHOLD` (0.5) of the delta's content tokens appear
* in the existing narrative.
*
* Short deltas (< 5 tokens in the keyword path) never trigger a pivot to avoid
* false positives from acknowledgement messages.  The embedding path has no
* minimum-length guard because the model handles short inputs gracefully.
*
* @param delta            - Incoming narrative text from the new turn.
* @param existingNarrative - Current rolling narrative content.
* @returns Promise resolving to `true` when a topic pivot is detected.
*
* @example
* ```ts
* await detectPivot("switching to Rust implementation", "TypeScript type system discussion"); // true
* await detectPivot("continuing the TypeScript work", "TypeScript type system discussion");   // false
* ```
*
* @task T1531
*/
async function detectPivot(delta, existingNarrative) {
	if (!existingNarrative.trim()) return false;
	if (isEmbeddingAvailable()) {
		const [deltaVec, narrativeVec] = await Promise.all([embedText(delta), embedText(existingNarrative)]);
		if (deltaVec !== null && narrativeVec !== null) return cosineSimilarity(deltaVec, narrativeVec) < PIVOT_COSINE_THRESHOLD;
	}
	const tokenise = (text) => {
		const tokens = text.toLowerCase().split(/\W+/).filter((t) => t.length >= 4);
		return new Set(tokens);
	};
	const deltaTokens = tokenise(delta);
	if (deltaTokens.size < 5) return false;
	const narrativeTokens = tokenise(existingNarrative);
	let overlap = 0;
	for (const token of deltaTokens) if (narrativeTokens.has(token)) overlap++;
	return overlap / deltaTokens.size < PIVOT_KEYWORD_OVERLAP_THRESHOLD;
}

export { appendNarrativeDelta, getSessionNarrative };
//# sourceMappingURL=session-narrative-B6H2uYEO.js.map
