import { t as typedAll } from './typed-query-l1D_QElt.js';

//#region ../core/dist/memory/brain-plasticity-class.js
/**
* Plasticity class and stability score utilities for STDP and Hebbian plasticity.
*
* Manages the plasticity_class column which governs which algorithms write to an edge:
* - 'static': Non-plastic (structural, semantic). Immune to decay.
* - 'hebbian': Written by strengthenCoRetrievedEdges. Subject to decay.
* - 'stdp': Written or refined by applyStdpPlasticity. Subject to decay + LTD.
*
* Also computes stability_score for decay filtering and plasticity event ordering.
*
* @task T693
* @epic T673
*/
/**
* Upgrade plasticity class to the most plastic variant that has touched it.
*
* Decision logic (T693 / synthesis decision #12):
* - If current = 'static' and event = 'hebbian' → 'hebbian' (first plasticity touch)
* - If current = 'static' and event = 'stdp' → 'stdp' (STDP direct touch)
* - If current = 'hebbian' and event = 'stdp' → 'stdp' (upgrade via STDP)
* - If current = 'stdp' → stays 'stdp' (no downgrade)
* - Otherwise return current unchanged
*
* @param currentClass - Current plasticity class value ('static', 'hebbian', 'stdp')
* @param event - Event type triggering upgrade ('hebbian' or 'stdp')
* @returns Upgraded plasticity class
*/
function upgradePlasticityClass(currentClass, event) {
	const current = currentClass ?? "static";
	if (current === "stdp") return "stdp";
	return "stdp";
}
/**
* Compute biological stability score for an edge based on reinforcement history.
*
* Formula (T673 spec §1 decision #13):
*   stability = tanh(reinforcement_count / 10) × exp(-(days_since_reinforced / 30))
*
* Parameters:
* - reinforcement_count: Number of LTP events applied lifetime (integer ≥ 0)
* - lastReinforcedAt: ISO 8601 timestamp of last LTP event, or null if never
*
* Returns: 0.0–1.0, or null if no history to compute from
*
* Examples:
* - rc=10, days=0 → tanh(1.0) × 1.0 ≈ 0.762
* - rc=5, days=0  → tanh(0.5) × 1.0 ≈ 0.462
* - rc=10, days=30 → tanh(1.0) × exp(-1) ≈ 0.280
* - rc=0, days=any → tanh(0) × ... = 0.0 (new edges)
*
* @param reinforcementCount - Lifetime count of LTP events applied to edge
* @param lastReinforcedAt - ISO 8601 timestamp of last LTP event, or null
* @param now - Current timestamp (epoch ms, for testing). Defaults to Date.now()
* @returns Stability score 0.0–1.0, or null if edge has no reinforcement history
*/
function computeStabilityScore(reinforcementCount, lastReinforcedAt, now = Date.now()) {
	if (!reinforcementCount || reinforcementCount <= 0) return null;
	if (!lastReinforcedAt) return null;
	const rcTerm = Math.tanh(reinforcementCount / 10);
	const ageDays = (now - new Date(lastReinforcedAt).getTime()) / (1440 * 60 * 1e3);
	const stability = rcTerm * Math.exp(-ageDays / 30);
	return Math.max(0, Math.min(1, stability));
}
//#endregion
//#region ../core/dist/memory/brain-stdp.js
/**
* STDP (Spike-Timing-Dependent Plasticity) for CLEO BRAIN.
*
* Implements a biologically-inspired Hebbian learning rule that modulates edge
* weights based on the *temporal order* of memory retrievals within a session
* window, not just co-occurrence frequency.
*
* ## Neuroscience basis
*
* Classical STDP: if neuron A fires before neuron B by Δt milliseconds, the
* synapse A→B is potentiated (LTP); if A fires after B, it is depressed (LTD).
* The magnitude of the change decays exponentially with |Δt|.
*
* ## Mapping to CLEO memory
*
* - Each entry retrieved from brain.db is a "spike".
* - Retrievals within the same session and within `sessionWindowMs` of each
*   other are treated as temporally related spikes.
* - If entry A was retrieved BEFORE entry B by Δt ms:
*     Δw = A_pre  × exp(−Δt / τ_pre)   → potentiation (LTP, positive Δw)
* - If entry A was retrieved AFTER entry B by Δt ms:
*     Δw = −A_post × exp(−Δt / τ_post)  → depression  (LTD, negative Δw)
* - Weights are clamped to [0.0, 1.0].
* - All events are logged to `brain_plasticity_events` for observability.
*
* ## Parameters (biologically reasonable defaults)
*
* | Symbol  | Value  | Meaning                                          |
* |---------|--------|--------------------------------------------------|
* | τ_pre   | 20 s   | Time constant for pre→post potentiation          |
* | τ_post  | 20 s   | Time constant for post→pre depression            |
* | A_pre   | 0.05   | Peak potentiation amplitude                      |
* | A_post  | 0.06   | Peak depression amplitude (asymmetric, per STDP) |
*
* The asymmetry A_post > A_pre models the biological fact that LTD is slightly
* stronger than LTP, preventing runaway weight growth.
*
* ## Relation to Hebbian co-retrieval
*
* Hebbian (`strengthenCoRetrievedEdges` in brain-lifecycle.ts) uses the
* `co_retrieved` edge type and does NOT track order; it fires on pairs that
* co-occur ≥ 3× regardless of timing. STDP is a *second plasticity pass* that
* runs after Hebbian and refines existing `co_retrieved` edges using order data.
*
* If no `co_retrieved` edge exists yet between a pair, STDP inserts one with the
* initial STDP-derived weight (potentiation pairs only — LTD does not create
* new edges, only weakens existing ones).
*
* ## Two-Window Architecture (T679 BUG-1 fix, T688 expansion)
*
* Prior code used `sessionWindowMs=5min` as BOTH the SQL lookback cutoff AND the
* spike-pair Δt gate, causing all live rows (>5min old) to produce zero plasticity
* events. This version separates the two concerns:
*
* | Parameter         | Default    | Purpose                                  |
* |-------------------|------------|------------------------------------------|
* | `lookbackDays`    | 30 days    | SQL cutoff for fetching retrieval rows   |
* | `pairingWindowMs` | 24 h       | Max Δt between two spikes for pairing    |
*
* T688: `pairingWindowMs` raised to 24 h — cross-session pairs now eligible.
* Session boundary is NOT a hard cutoff; τ tier determines decay magnitude.
*
* ## Tiered τ (T689)
*
* `computeTau(deltaT)` selects the decay time constant based on Δt:
*
* | Gap class     | Δt range       | τ       |
* |---------------|----------------|---------|
* | Intra-batch   | 0 — 30 s       | 20 s    |
* | Intra-session | 30 s — 2 h     | 30 min  |
* | Cross-session | 2 h — 24 h     | 12 h    |
*
* ## R-STDP reward modulation (T692)
*
* `reward_signal r` (from Step 9a backfill) gates Δw per spike:
*   Δw_ltp_effective = clamp(Δw_ltp × (1+r), 0, 2×A_pre)
*   Δw_ltd_effective = clamp(Δw_ltd × (1-r), -2×A_post, 0)
* null reward → no modulation (r treated as 0).
*
* ## Novelty boost (T691)
*
* On INSERT (first co-retrieval), initial_weight = clamp(Δw × 1.5, 0, A_pre×1.5).
* UPDATE path uses un-boosted Δw.
*
* @task T626
* @task T679
* @task T688
* @task T689
* @task T691
* @task T692
* @epic T673
* @see packages/core/src/memory/brain-lifecycle.ts#strengthenCoRetrievedEdges
* @see docs/specs/stdp-wire-up-spec.md §3.2 Two-Window Architecture
*/
/** Default SQL lookback window: fetch retrieval rows from the last N days. */
var DEFAULT_LOOKBACK_DAYS = 30;
/**
* Default spike-pair matching window in milliseconds.
* T688: raised from 5 min to 24 h — cross-session pairs are now eligible.
* Session boundary is NOT a hard cutoff; tiered τ provides smaller Δw for
* cross-session pairs (τ_episodic=12h vs τ_near=20s).
*/
var DEFAULT_PAIRING_WINDOW_MS = 1440 * 60 * 1e3;
/**
* Intra-batch time constant (ms).
* Used when Δt ≤ 30 s — both spikes are in the same retrieval batch.
* Biological analogue: classical STDP window (~20 ms–100 ms in neurons;
* scaled to 20 s for CLEO memory retrieval granularity).
*
* @task T689
*/
var TAU_NEAR_MS = 2e4;
/**
* Intra-session time constant (ms).
* Used when 30 s < Δt ≤ 2 h — spikes are in the same session but not same batch.
* Biological analogue: working-memory consolidation window.
*
* @task T689
*/
var TAU_SESSION_MS = 1800 * 1e3;
/**
* Cross-session (episodic) time constant (ms).
* Used when Δt > 2 h — spikes span different sessions.
* Biological analogue: episodic reconsolidation (~12 h per Walker & Stickgold 2004).
* Pairs 12 h apart contribute A × exp(-1) ≈ 0.37×A; pairs 36 h apart ≈ 0.05×A.
*
* @task T689
*/
var TAU_EPISODIC_MS = 720 * 60 * 1e3;
/**
* Δt boundary (ms) between intra-batch and intra-session τ tiers.
* Spikes within 30 s of each other use τ_near.
*/
var TAU_NEAR_THRESHOLD_MS = 3e4;
/**
* Δt boundary (ms) between intra-session and cross-session τ tiers.
* Spikes more than 2 h apart use τ_episodic.
*/
var TAU_SESSION_THRESHOLD_MS = 7200 * 1e3;
/** Peak potentiation amplitude (dimensionless weight delta). */
var A_PRE = .05;
/** Peak depression amplitude (slightly larger than A_pre — asymmetric STDP). */
var A_POST = .06;
/**
* Novelty boost multiplier for first-ever co-retrieval pair (INSERT path).
* Models dopamine-novelty literature: new associations are strengthened more.
* Applied only on INSERT; UPDATE path uses standard Δw.
*
* @task T691
*/
var K_NOVELTY = 1.5;
/** Minimum edge weight (floor). */
var WEIGHT_MIN = 0;
/** Maximum edge weight (ceiling). */
var WEIGHT_MAX = 1;
/**
* Select the decay time constant τ based on spike-pair temporal gap.
*
* Three tiers (per spec §3.3):
* - τ_near = 20 s   — Δt ≤ 30 s  (intra-batch, classical STDP window)
* - τ_session = 30 min — 30 s < Δt ≤ 2 h (intra-session, working memory)
* - τ_episodic = 12 h — Δt > 2 h  (cross-session, episodic reconsolidation)
*
* @param deltaT - Time gap between spikes in milliseconds (non-negative).
* @returns τ in milliseconds for use in exp(-Δt / τ).
*
* @task T689
*/
function computeTau(deltaT) {
	if (deltaT <= TAU_NEAR_THRESHOLD_MS) return TAU_NEAR_MS;
	if (deltaT <= TAU_SESSION_THRESHOLD_MS) return TAU_SESSION_MS;
	return TAU_EPISODIC_MS;
}
/**
* Check if a recent plasticity event exists for the given source→target pair
* within the last `withinHours` (default 1 hour).
*
* Used by T713 idempotency guard to prevent duplicate event insertion when
* consolidation runs multiple times against the same session.
*
* @param nativeDb - SQLite native database connection
* @param sourceNode - Source node ID (entry ID)
* @param targetNode - Target node ID (entry ID)
* @param kind - Event kind ('ltp' or 'ltd')
* @param sessionId - Session ID to match (null = all sessions)
* @param withinHours - Dedup window in hours. Default: 1
* @returns True if a matching recent event exists; false otherwise.
*/
function isPlasticityEventDuplicate(nativeDb, sourceNode, targetNode, kind, sessionId, withinHours = 1) {
	try {
		const cutoffIso = (/* @__PURE__ */ new Date(Date.now() - withinHours * 60 * 60 * 1e3)).toISOString().replace("T", " ").slice(0, 19);
		return nativeDb.prepare(`SELECT 1 FROM brain_plasticity_events
       WHERE source_node = ? AND target_node = ? AND kind = ?
         AND session_id = ?
         AND timestamp > ?
       LIMIT 1`).get(sourceNode, targetNode, kind, sessionId, cutoffIso) !== void 0;
	} catch {
		return false;
	}
}
/**
* Check if there are at least `minCount` retrieval log rows since the last
* plasticity event in the given session.
*
* Used by T714 minimum-pair gate to skip Step 9b when a session has too few
* retrievals to warrant plasticity processing.
*
* @param nativeDb - SQLite native database connection
* @param minCount - Minimum number of new retrievals required. Default: 2
* @param sessionId - Session ID to check (null = all sessions)
* @returns True if minCount or more new retrievals exist; false otherwise.
*/
function hasMinimumRetrievalsSinceLastPlasticity(nativeDb, minCount = 2, sessionId = null) {
	try {
		const db = nativeDb;
		const lastTime = (() => {
			if (sessionId !== null) return db.prepare(`SELECT MAX(timestamp) as last_time FROM brain_plasticity_events
             WHERE session_id = ?`).get(sessionId)?.last_time ?? null;
			return db.prepare(`SELECT MAX(timestamp) as last_time FROM brain_plasticity_events`).get()?.last_time ?? null;
		})();
		let newRetrievalCount;
		if (lastTime === null) if (sessionId !== null) newRetrievalCount = db.prepare(`SELECT COUNT(*) as cnt FROM brain_retrieval_log
             WHERE session_id = ?`).get(sessionId)?.cnt ?? 0;
		else newRetrievalCount = db.prepare(`SELECT COUNT(*) as cnt FROM brain_retrieval_log`).get()?.cnt ?? 0;
		else if (sessionId !== null) newRetrievalCount = db.prepare(`SELECT COUNT(*) as cnt FROM brain_retrieval_log
             WHERE session_id = ? AND created_at > ?`).get(sessionId, lastTime)?.cnt ?? 0;
		else newRetrievalCount = db.prepare(`SELECT COUNT(*) as cnt FROM brain_retrieval_log
             WHERE created_at > ?`).get(lastTime)?.cnt ?? 0;
		return newRetrievalCount >= minCount;
	} catch {
		return true;
	}
}
/**
* T714: Check whether Step 9b (STDP plasticity) should run based on retrieval volume.
*
* Per spec §4.2, skip plasticity processing if fewer than `minRetrievalsForPlasticity`
* new retrieval rows exist since the last `brain_plasticity_events` timestamp.
* This prevents wasted compute on early-session edge cases where no meaningful pairs exist.
*
* @param projectRoot - Project root for brain.db resolution
* @param sessionId - Session ID to check (null = all sessions)
* @param minRetrievalsForPlasticity - Minimum row count required. Default: 2.
* @returns True if plasticity should run; false if gate blocks it.
*/
async function shouldRunPlasticity(projectRoot, sessionId = null, minRetrievalsForPlasticity = 2) {
	const { getBrainDb, getBrainNativeDb } = await import('./memory-sqlite-Dg5uGJCo.js');
	await getBrainDb(projectRoot);
	const nativeDb = getBrainNativeDb();
	if (!nativeDb) return true;
	const hasMinimum = hasMinimumRetrievalsSinceLastPlasticity(nativeDb, minRetrievalsForPlasticity, sessionId);
	if (!hasMinimum) {
		const count = (() => {
			try {
				const db = nativeDb;
				const lastTime = (() => {
					if (sessionId !== null) return db.prepare(`SELECT MAX(timestamp) as last_time FROM brain_plasticity_events WHERE session_id = ?`).get(sessionId)?.last_time ?? null;
					return db.prepare(`SELECT MAX(timestamp) as last_time FROM brain_plasticity_events`).get()?.last_time ?? null;
				})();
				if (lastTime === null) return (sessionId !== null ? db.prepare(`SELECT COUNT(*) as cnt FROM brain_retrieval_log WHERE session_id = ?`).get(sessionId) : db.prepare(`SELECT COUNT(*) as cnt FROM brain_retrieval_log`).get())?.cnt ?? 0;
				else return (sessionId !== null ? db.prepare(`SELECT COUNT(*) as cnt FROM brain_retrieval_log WHERE session_id = ? AND created_at > ?`).get(sessionId, lastTime) : db.prepare(`SELECT COUNT(*) as cnt FROM brain_retrieval_log WHERE created_at > ?`).get(lastTime))?.cnt ?? 0;
			} catch {
				return 0;
			}
		})();
		console.warn(`[plasticity] Minimum-pair gate: skipped STDP Step 9b (${count} retrievals, need >=${minRetrievalsForPlasticity})`);
	}
	return hasMinimum;
}
/**
* Apply Spike-Timing-Dependent Plasticity to brain_page_edges.
*
* Reads `brain_retrieval_log` for rows within the past `lookbackDays` days
* (default 30), reconstructs the temporal spike sequence, and applies the
* STDP rule to every ordered pair within `pairingWindowMs` (default 24 h).
*
* All weight changes are logged to `brain_plasticity_events` (with
* `session_id`, `retrieval_log_id`, `weight_before`, `weight_after`,
* `delta_t_ms`) and to `brain_weight_history` for the full audit trail.
*
* ### T679 fixes (root cause: BUG-1 from stdp-wire-up-spec.md §1.4)
*
* - Separated `lookbackDays` (SQL fetch window, default 30d) from
*   `pairingWindowMs` (spike-pair Δt gate, default 5min). Previously both
*   used `sessionWindowMs=5min`, causing all live rows (>5min old) to produce
*   zero events.
* - Writer now populates `session_id` + `retrieval_log_id` + `weight_before` /
*   `weight_after` on `brain_plasticity_events` INSERT.
* - Writer now inserts into `brain_weight_history` per weight delta.
* - Fixed bug where `prepareUpdateEdge.run()` was missing 4 of 9 SQL params.
*
* ### Backward compatibility
*
* The legacy `applyStdpPlasticity(root, number)` signature is still accepted.
* The number is mapped to `pairingWindowMs`; `lookbackDays` stays at 30d.
* A deprecation warning is emitted to steer callers to `StdpPlasticityOptions`.
*
* ### T10351 — process-local serializer
*
* `brain_plasticity_events` and `brain_weight_history` are the highest-volume
* write tables in brain.db (~94% of the 1.7 GB blow-up identified in the
* T10301 RCA). Two concurrent `applyStdpPlasticity` runs in the same process
* (possible via the dialectic-hook + propose-tick reflex + manual
* `cleo memory consolidate` racing each other) would interleave their inserts
* and torn frame writes from one transaction can clobber the schema-root
* B-tree page — the documented corruption signature. The exported entry
* point now serializes passes via an async mutex; one pass at a time per
* process. See `brain-writer-thread.ts` for the per-row chokepoint that
* covers low-volume callers.
*
* @param projectRoot - Project root directory for brain.db resolution
* @param options - `StdpPlasticityOptions` or a legacy number (deprecated `sessionWindowMs`).
* @returns Counts of LTP/LTD events applied and edges created/updated.
*/
var _stdpInFlight = null;
async function applyStdpPlasticity(projectRoot, options) {
	while (_stdpInFlight) try {
		await _stdpInFlight;
	} catch {}
	const passPromise = applyStdpPlasticityImpl(projectRoot, options);
	_stdpInFlight = passPromise.catch(() => void 0);
	try {
		return await passPromise;
	} finally {
		_stdpInFlight = null;
	}
}
async function applyStdpPlasticityImpl(projectRoot, options) {
	let lookbackDays = DEFAULT_LOOKBACK_DAYS;
	let pairingWindowMs = DEFAULT_PAIRING_WINDOW_MS;
	if (typeof options === "number") {
		console.warn("[brain-stdp] Deprecated: passing sessionWindowMs as a number. Use StdpPlasticityOptions { lookbackDays, pairingWindowMs } instead. (T679)");
		pairingWindowMs = options;
	} else if (options !== void 0) {
		lookbackDays = options.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
		pairingWindowMs = options.pairingWindowMs ?? DEFAULT_PAIRING_WINDOW_MS;
	}
	const { getBrainDb, getBrainNativeDb } = await import('./memory-sqlite-Dg5uGJCo.js');
	await getBrainDb(projectRoot);
	const nativeDb = getBrainNativeDb();
	const result = {
		ltpEvents: 0,
		ltdEvents: 0,
		edgesCreated: 0,
		pairsExamined: 0,
		rewardModulatedEvents: 0
	};
	if (!nativeDb) return result;
	try {
		nativeDb.prepare("SELECT 1 FROM brain_retrieval_log LIMIT 1").get();
	} catch {
		return result;
	}
	try {
		nativeDb.prepare("SELECT 1 FROM brain_plasticity_events LIMIT 1").get();
	} catch {
		return result;
	}
	const now = Date.now();
	const cutoffMs = now - lookbackDays * 24 * 60 * 60 * 1e3;
	const cutoffIso = new Date(cutoffMs).toISOString().replace("T", " ").slice(0, 19);
	const nowIso = new Date(now).toISOString().replace("T", " ").slice(0, 19);
	let logRows = [];
	try {
		logRows = typedAll(nativeDb.prepare(`SELECT id, entry_ids, created_at, retrieval_order, delta_ms, session_id, reward_signal
         FROM brain_retrieval_log
         WHERE created_at >= ?
         ORDER BY created_at ASC, id ASC
         LIMIT 2000`), cutoffIso);
	} catch {
		return result;
	}
	if (logRows.length === 0) return result;
	const spikes = [];
	let globalOrder = 0;
	for (const row of logRows) {
		let ids;
		try {
			ids = JSON.parse(row.entry_ids);
		} catch {
			continue;
		}
		if (!Array.isArray(ids)) continue;
		const rowTime = (/* @__PURE__ */ new Date(row.created_at.replace(" ", "T") + "Z")).getTime();
		for (const rawId of ids) {
			if (typeof rawId !== "string" || rawId.length === 0) continue;
			const entryId = rawId.includes(":") ? rawId : `observation:${rawId}`;
			spikes.push({
				entryId,
				rowId: row.id,
				retrievedAt: rowTime,
				order: row.retrieval_order ?? globalOrder,
				sessionId: row.session_id ?? null,
				rewardSignal: row.reward_signal ?? null
			});
			globalOrder++;
		}
	}
	if (spikes.length < 2) return result;
	spikes.sort((a, b) => a.retrievedAt - b.retrievedAt || a.order - b.order);
	/** T695: Maximum spikes per session contributed to cross-session pair checks. */
	const MAX_PAIRS_PER_SESSION = 50;
	const sessionBucketMap = /* @__PURE__ */ new Map();
	for (const spike of spikes) {
		const key = spike.sessionId ?? "null";
		const bucket = sessionBucketMap.get(key);
		if (bucket !== void 0) bucket.push(spike);
		else sessionBucketMap.set(key, [spike]);
	}
	const orderedBuckets = Array.from(sessionBucketMap.entries()).sort(([, a], [, b]) => (a[0]?.retrievedAt ?? 0) - (b[0]?.retrievedAt ?? 0));
	const workingSpikes = [];
	for (let bi = 0; bi < orderedBuckets.length; bi++) {
		const [, bucketSpikes] = orderedBuckets[bi];
		workingSpikes.push(...bucketSpikes);
	}
	const withinSessionIndex = /* @__PURE__ */ new Map();
	const sessionSizeMap = /* @__PURE__ */ new Map();
	for (const [, bucketSpikes] of orderedBuckets) for (let idx = 0; idx < bucketSpikes.length; idx++) {
		const spike = bucketSpikes[idx];
		withinSessionIndex.set(spike, idx);
		sessionSizeMap.set(spike.sessionId ?? "null", bucketSpikes.length);
	}
	const prepareGetEdge = nativeDb.prepare(`SELECT weight, reinforcement_count, last_reinforced_at, plasticity_class, depression_count, last_depressed_at
     FROM brain_page_edges
     WHERE from_id = ? AND to_id = ? AND edge_type = 'co_retrieved'`);
	const prepareUpdateEdgeLtp = nativeDb.prepare(`UPDATE brain_page_edges
     SET weight = MAX(?, MIN(?, weight + ?)),
         reinforcement_count = reinforcement_count + 1,
         last_reinforced_at = ?,
         plasticity_class = ?,
         stability_score = ?
     WHERE from_id = ? AND to_id = ? AND edge_type = 'co_retrieved'`);
	const prepareUpdateEdgeLtd = nativeDb.prepare(`UPDATE brain_page_edges
     SET weight = MAX(?, MIN(?, weight + ?)),
         depression_count = depression_count + 1,
         last_depressed_at = ?,
         plasticity_class = ?,
         stability_score = ?
     WHERE from_id = ? AND to_id = ? AND edge_type = 'co_retrieved'`);
	const prepareInsertEdge = nativeDb.prepare(`INSERT OR IGNORE INTO brain_page_edges
       (from_id, to_id, edge_type, weight, provenance, reinforcement_count, last_reinforced_at, plasticity_class, stability_score, created_at)
     VALUES (?, ?, 'co_retrieved', ?, 'plasticity:stdp-ltp', 1, ?, 'stdp', ?, ?)`);
	const prepareLogEvent = nativeDb.prepare(`INSERT INTO brain_plasticity_events
       (source_node, target_node, delta_w, kind, timestamp,
        session_id, retrieval_log_id, weight_before, weight_after, delta_t_ms)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
	let prepareLogWeightHistory = null;
	try {
		nativeDb.prepare("SELECT 1 FROM brain_weight_history LIMIT 1").get();
		prepareLogWeightHistory = nativeDb.prepare(`INSERT INTO brain_weight_history
         (edge_from_id, edge_to_id, edge_type, weight_before, weight_after,
          delta_weight, event_kind, source_plasticity_event_id, retrieval_log_id,
          reward_signal, changed_at)
       VALUES (?, ?, 'co_retrieved', ?, ?, ?, ?, ?, ?, ?, ?)`);
	} catch {}
	for (let i = 0; i < workingSpikes.length; i++) {
		const spikeA = workingSpikes[i];
		const sessionKeyA = spikeA.sessionId ?? "null";
		const sessionSizeA = sessionSizeMap.get(sessionKeyA) ?? 1;
		const canCrossSession = (withinSessionIndex.get(spikeA) ?? 0) >= sessionSizeA - MAX_PAIRS_PER_SESSION;
		for (let j = i + 1; j < workingSpikes.length; j++) {
			const spikeB = workingSpikes[j];
			const deltaT = spikeB.retrievedAt - spikeA.retrievedAt;
			if (deltaT > pairingWindowMs) break;
			if (spikeA.entryId === spikeB.entryId) continue;
			if (sessionKeyA !== (spikeB.sessionId ?? "null") && !canCrossSession) continue;
			result.pairsExamined++;
			const tau = computeTau(deltaT);
			let deltaW = A_PRE * Math.exp(-deltaT / tau);
			if (deltaW < 1e-6) continue;
			const eventSessionId = spikeA.sessionId ?? null;
			const eventRewardSignal = spikeA.rewardSignal ?? null;
			let wasRewardModulated = false;
			if (eventRewardSignal !== null) {
				deltaW = Math.min(deltaW * (1 + eventRewardSignal), 2 * A_PRE);
				deltaW = Math.max(deltaW, 0);
				wasRewardModulated = true;
			}
			if (deltaW < 1e-6) {
				if (wasRewardModulated) result.rewardModulatedEvents++;
				continue;
			}
			const existingEdge = prepareGetEdge.get(spikeA.entryId, spikeB.entryId);
			let ltpEventId = null;
			try {
				if (existingEdge !== void 0) {
					const currentWeight = existingEdge.weight;
					const newWeight = Math.max(WEIGHT_MIN, Math.min(WEIGHT_MAX, currentWeight + deltaW));
					const upgradedClass = upgradePlasticityClass(existingEdge.plasticity_class, "stdp");
					const stability = computeStabilityScore((existingEdge.reinforcement_count ?? 0) + 1, nowIso, now);
					prepareUpdateEdgeLtp.run(WEIGHT_MIN, WEIGHT_MAX, deltaW, nowIso, upgradedClass, stability, spikeA.entryId, spikeB.entryId);
					if (isPlasticityEventDuplicate(nativeDb, spikeA.entryId, spikeB.entryId, "ltp", eventSessionId, 1)) {
						result.ltpEvents++;
						if (wasRewardModulated) result.rewardModulatedEvents++;
						continue;
					}
					const evtStmt = prepareLogEvent.run(spikeA.entryId, spikeB.entryId, deltaW, "ltp", nowIso, eventSessionId, spikeA.rowId, currentWeight, newWeight, deltaT);
					ltpEventId = evtStmt.lastInsertRowid != null ? Number(evtStmt.lastInsertRowid) : null;
					if (prepareLogWeightHistory) prepareLogWeightHistory.run(spikeA.entryId, spikeB.entryId, currentWeight, newWeight, deltaW, "ltp", ltpEventId, spikeA.rowId, eventRewardSignal, nowIso);
				} else {
					const noveltyBoostedWeight = deltaW * K_NOVELTY;
					const initialWeight = Math.min(WEIGHT_MAX, Math.min(A_PRE * K_NOVELTY, noveltyBoostedWeight));
					const stability = computeStabilityScore(1, nowIso, now);
					prepareInsertEdge.run(spikeA.entryId, spikeB.entryId, initialWeight, nowIso, stability, nowIso);
					result.edgesCreated++;
					const evtStmt = prepareLogEvent.run(spikeA.entryId, spikeB.entryId, initialWeight, "ltp", nowIso, eventSessionId, spikeA.rowId, null, initialWeight, deltaT);
					ltpEventId = evtStmt.lastInsertRowid != null ? Number(evtStmt.lastInsertRowid) : null;
					if (prepareLogWeightHistory) prepareLogWeightHistory.run(spikeA.entryId, spikeB.entryId, null, initialWeight, initialWeight, "ltp", ltpEventId, spikeA.rowId, eventRewardSignal, nowIso);
				}
				result.ltpEvents++;
				if (wasRewardModulated) result.rewardModulatedEvents++;
			} catch {}
			let deltaWNeg = -(A_POST * Math.exp(-deltaT / tau));
			let ltdWasRewardModulated = false;
			if (eventRewardSignal !== null) {
				deltaWNeg = Math.max(deltaWNeg * (1 - eventRewardSignal), -2 * A_POST);
				ltdWasRewardModulated = true;
			}
			const existingReverseEdge = prepareGetEdge.get(spikeB.entryId, spikeA.entryId);
			if (existingReverseEdge !== void 0 && Math.abs(deltaWNeg) >= 1e-6) try {
				const currentReverseWeight = existingReverseEdge.weight;
				const newReverseWeight = Math.max(WEIGHT_MIN, Math.min(WEIGHT_MAX, currentReverseWeight + deltaWNeg));
				const upgradedClass = upgradePlasticityClass(existingReverseEdge.plasticity_class, "stdp");
				const stability = computeStabilityScore(existingReverseEdge.reinforcement_count, existingReverseEdge.last_reinforced_at, now);
				prepareUpdateEdgeLtd.run(WEIGHT_MIN, WEIGHT_MAX, deltaWNeg, nowIso, upgradedClass, stability, spikeB.entryId, spikeA.entryId);
				if (isPlasticityEventDuplicate(nativeDb, spikeB.entryId, spikeA.entryId, "ltd", eventSessionId, 1)) {
					result.ltdEvents++;
					if (ltdWasRewardModulated) result.rewardModulatedEvents++;
					continue;
				}
				const ltdEvtStmt = prepareLogEvent.run(spikeB.entryId, spikeA.entryId, deltaWNeg, "ltd", nowIso, eventSessionId, spikeB.rowId, currentReverseWeight, newReverseWeight, deltaT);
				const ltdEventId = ltdEvtStmt.lastInsertRowid != null ? Number(ltdEvtStmt.lastInsertRowid) : null;
				if (prepareLogWeightHistory) prepareLogWeightHistory.run(spikeB.entryId, spikeA.entryId, currentReverseWeight, newReverseWeight, deltaWNeg, "ltd", ltdEventId, spikeB.rowId, eventRewardSignal, nowIso);
				result.ltdEvents++;
				if (ltdWasRewardModulated) result.rewardModulatedEvents++;
			} catch {}
		}
	}
	return result;
}
/**
* Backfill reward_signal values on brain_retrieval_log rows for a session.
*
* Step 9a of the `runConsolidation` pipeline — runs BEFORE `applyStdpPlasticity`
* (Step 9b) so reward signals are present when STDP reads them.
*
* ## Signal derivation
*
* Queries tasks.db for tasks attributed to `sessionId` within `lookbackDays`.
* Maps task outcomes to reward scalars:
*
* | Task state | Reward |
* |-----------|--------|
* | `status='done'`, `verification.passed=true` | +1.0 (verified correct) |
* | `status='done'`, verification not passed | +0.5 (completed, unverified) |
* | `status='cancelled'` | -0.5 (cancelled) |
*
* All brain_retrieval_log rows for the session receive the derived scalar as
* a session-level reward (the entire session's retrieval pattern is rated by
* the session's overall task outcome). If multiple tasks exist in the session,
* the MAXIMUM reward takes precedence (positive outcome wins).
*
* ## Skipped sessions
*
* Sessions with `session_id LIKE 'ses_backfill_%'` are synthetic (date-bucketed
* historical rows per M1). These have no real task correlation and MUST be skipped.
*
* ## Idempotency
*
* Already-labeled rows (reward_signal IS NOT NULL) are not overwritten.
* Running twice on the same session is safe.
*
* ## Transaction pattern
*
* Two separate SQLite connections (no ATTACH): reads tasks.db → computes reward
* map → writes brain.db in separate transactions. Matches `cross-db-cleanup.ts`.
*
* @param projectRoot - Project root directory for database resolution
* @param sessionId - The session ID to backfill. Pass null/undefined for no-op.
* @param lookbackDays - Days of tasks.db history to scan (default 30)
* @returns Counts of rows labeled and skipped
*
* @task T681
* @epic T673
*/
async function backfillRewardSignals(projectRoot, sessionId, lookbackDays = 30) {
	const result = {
		rowsLabeled: 0,
		rowsSkipped: 0
	};
	if (!sessionId) return result;
	if (sessionId.startsWith("ses_backfill_")) return result;
	let taskRows = [];
	try {
		const { getDb } = await import('./sqlite2-UG45Z53q.js');
		const tasksDb = await getDb(projectRoot);
		const { tasks } = await import('./tasks-schema2-DJGD-IKf.js');
		const { and, eq, inArray, gte, or, isNotNull } = await import('./index-DIWrtHjY.js');
		const cutoffTs = (/* @__PURE__ */ new Date(Date.now() - lookbackDays * 24 * 60 * 60 * 1e3)).toISOString().replace("T", " ").slice(0, 19);
		taskRows = (await tasksDb.select({
			id: tasks.id,
			status: tasks.status,
			verificationJson: tasks.verificationJson,
			completedAt: tasks.completedAt,
			cancelledAt: tasks.cancelledAt
		}).from(tasks).where(and(eq(tasks.sessionId, sessionId), inArray(tasks.status, ["done", "cancelled"]), or(and(isNotNull(tasks.completedAt), gte(tasks.completedAt, cutoffTs)), and(isNotNull(tasks.cancelledAt), gte(tasks.cancelledAt, cutoffTs))))).all()).map((r) => ({
			id: r.id,
			status: r.status,
			verificationJson: r.verificationJson ?? null,
			completedAt: r.completedAt ?? null,
			cancelledAt: r.cancelledAt ?? null
		}));
	} catch {
		return result;
	}
	if (taskRows.length === 0) return result;
	let sessionReward = null;
	/** Derive the scalar reward for one task row. */
	function deriveTaskReward(task) {
		if (task.status === "cancelled") return -0.5;
		let verificationPassed = false;
		if (task.verificationJson) try {
			verificationPassed = JSON.parse(task.verificationJson).passed === true;
		} catch {}
		return verificationPassed ? 1 : .5;
	}
	for (const task of taskRows) {
		const taskReward = deriveTaskReward(task);
		if (sessionReward === null || taskReward > sessionReward) sessionReward = taskReward;
	}
	if (sessionReward === null) return result;
	try {
		const { getBrainDb, getBrainNativeDb } = await import('./memory-sqlite-Dg5uGJCo.js');
		await getBrainDb(projectRoot);
		const nativeDb = getBrainNativeDb();
		if (!nativeDb) return result;
		try {
			nativeDb.prepare("SELECT 1 FROM brain_retrieval_log LIMIT 1").get();
		} catch {
			return result;
		}
		const updateResult = nativeDb.prepare(`UPDATE brain_retrieval_log
         SET reward_signal = ?
         WHERE session_id = ?
           AND reward_signal IS NULL`).run(sessionReward, sessionId);
		const updatedCount = typeof updateResult.changes === "number" ? updateResult.changes : 0;
		result.rowsLabeled = updatedCount;
		result.rowsSkipped = nativeDb.prepare(`SELECT COUNT(*) AS cnt FROM brain_retrieval_log
         WHERE session_id = ? AND reward_signal IS NOT NULL`).get(sessionId)?.cnt ?? 0;
		let modulatorsExist = false;
		try {
			nativeDb.prepare("SELECT 1 FROM brain_modulators LIMIT 1").get();
			modulatorsExist = true;
		} catch {}
		if (modulatorsExist && updatedCount > 0) {
			const insertModulator = nativeDb.prepare(`INSERT INTO brain_modulators
           (modulator_type, valence, magnitude, source_event_id, session_id, description)
         VALUES (?, ?, 1.0, ?, ?, ?)`);
			for (const task of taskRows) {
				const taskReward = deriveTaskReward(task);
				let modulatorType;
				let description;
				if (task.status === "cancelled") {
					modulatorType = "task_cancelled";
					description = `Task ${task.id} cancelled`;
				} else if (taskReward >= 1) {
					modulatorType = "task_verified";
					description = `Task ${task.id} completed and verified`;
				} else {
					modulatorType = "task_completed";
					description = `Task ${task.id} completed (unverified)`;
				}
				try {
					insertModulator.run(modulatorType, taskReward, task.id, sessionId, description);
				} catch {}
			}
		}
	} catch {}
	return result;
}
/**
* Apply homeostatic weight decay to `hebbian` and `stdp` brain edges (Step 9c).
*
* Runs after `applyStdpPlasticity` (Step 9b) in the consolidation pipeline.
* For each `co_retrieved` edge with `plasticity_class IN ('hebbian', 'stdp')` and
* `last_reinforced_at` older than `gracePeriodDays`, applies:
*
* ```
* new_weight = current_weight × (1 - decayRatePerDay) ^ days_idle
* ```
*
* where `days_idle = (now − last_reinforced_at) − gracePeriodDays`.
*
* If `new_weight < pruneThreshold`, the edge is **deleted** and a
* `brain_weight_history` row with `event_kind='prune'` is written.
* If `new_weight >= pruneThreshold`, the weight is updated **without**
* writing to `brain_weight_history` (routine decay is not logged per
* spec §1 decision #11).
*
* **Protected edge classes** (never touched):
* - `plasticity_class = 'static'` — structural edges
* - `plasticity_class = 'external'` — externally owned edges
* - Edges with `last_reinforced_at IS NULL` — never reinforced, skip decay
*
* @param projectRoot - Project root directory for brain.db resolution
* @param options - Decay configuration (see `HomeostaticDecayOptions`)
* @returns Counts of edges decayed and pruned
*
* @task T690
* @epic T673
* @see docs/specs/stdp-wire-up-spec.md §3.9
*/
async function applyHomeostaticDecay(projectRoot, options) {
	const decayRatePerDay = options?.decayRatePerDay ?? .02;
	const gracePeriodDays = options?.gracePeriodDays ?? 7;
	const pruneThreshold = options?.pruneThreshold ?? .05;
	const result = {
		edgesDecayed: 0,
		edgesPruned: 0
	};
	const { getBrainDb, getBrainNativeDb } = await import('./memory-sqlite-Dg5uGJCo.js');
	await getBrainDb(projectRoot);
	const nativeDb = getBrainNativeDb();
	if (!nativeDb) return result;
	try {
		nativeDb.prepare("SELECT 1 FROM brain_page_edges LIMIT 1").get();
	} catch {
		return result;
	}
	const nowIso = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
	let weightHistoryExists = false;
	try {
		nativeDb.prepare("SELECT 1 FROM brain_weight_history LIMIT 1").get();
		weightHistoryExists = true;
	} catch {}
	let candidates = [];
	try {
		candidates = typedAll(nativeDb.prepare(`SELECT from_id, to_id, edge_type, weight, plasticity_class, last_reinforced_at
         FROM brain_page_edges
         WHERE plasticity_class IN ('hebbian', 'stdp')
           AND last_reinforced_at IS NOT NULL
           AND (julianday('now') - julianday(last_reinforced_at)) > ?`), gracePeriodDays);
	} catch {
		return result;
	}
	if (candidates.length === 0) return result;
	const prepareUpdateWeight = nativeDb.prepare(`UPDATE brain_page_edges
     SET weight = ?
     WHERE from_id = ? AND to_id = ? AND edge_type = ?`);
	const prepareDeleteEdge = nativeDb.prepare(`DELETE FROM brain_page_edges
     WHERE from_id = ? AND to_id = ? AND edge_type = ?`);
	let prepareInsertHistory = null;
	if (weightHistoryExists) try {
		prepareInsertHistory = nativeDb.prepare(`INSERT INTO brain_weight_history
           (edge_from_id, edge_to_id, edge_type, weight_before, weight_after,
            delta_weight, event_kind, changed_at)
         VALUES (?, ?, ?, ?, 0.0, ?, 'prune', ?)`);
	} catch {}
	const nowMs = Date.now();
	for (const edge of candidates) try {
		const daysIdle = (nowMs - (/* @__PURE__ */ new Date(edge.last_reinforced_at.replace(" ", "T") + "Z")).getTime()) / (1440 * 60 * 1e3);
		const decayDays = Math.max(0, daysIdle - gracePeriodDays);
		const newWeight = edge.weight * (1 - decayRatePerDay) ** decayDays;
		if (newWeight < pruneThreshold) {
			prepareDeleteEdge.run(edge.from_id, edge.to_id, edge.edge_type);
			result.edgesPruned++;
			if (prepareInsertHistory) {
				const deltaW = -edge.weight;
				try {
					prepareInsertHistory.run(edge.from_id, edge.to_id, edge.edge_type, edge.weight, deltaW, nowIso);
				} catch {}
			}
		} else {
			prepareUpdateWeight.run(newWeight, edge.from_id, edge.to_id, edge.edge_type);
			result.edgesDecayed++;
		}
	} catch {}
	return result;
}
/**
* Prune stale rows from `brain_plasticity_events` and `brain_weight_history`.
*
* The two tables are STDP-driven append-only event logs. Per the T10301 RCA,
* unbounded growth (19K → 3.57M rows, 446 MB + 458 MB across 19 days) caused
* `brain.db` to reach 1.7 GB and contributed to the malformation incident
* (Saga T10281). T10348 wires retention discipline behind this single chokepoint.
*
* ### Two-stage prune
*
* 1. **Age-based**: delete rows with `timestamp` / `changed_at` older than
*    `retentionDays` days (default 30 — matches STDP `lookbackDays`).
* 2. **Row-cap fallback**: after step 1, if the table is still over its row
*    cap, delete the oldest remaining rows until under the cap.
*
* ### Operator override
*
* `CLEO_BRAIN_HISTORY_RETENTION_DAYS=0` disables both stages entirely. Any
* positive integer value REPLACES `retentionDays` (the option still wins if
* passed explicitly — env is only consulted when no option is passed).
*
* ### Best-effort semantics
*
* Every SQL call is wrapped — missing tables, partial-migration installs,
* and per-row errors are all logged silently and never throw. This function
* MUST NEVER block `runConsolidation` even when retention storage is wedged.
*
* ### Logging
*
* When `plasticityEventsDeleted + weightHistoryDeleted >= PRUNE_HISTORY_LOG_THRESHOLD`
* (default 10_000), a single `[pruneStaleHistory]` info line is emitted via
* `console.info` so operators can see large prunes in session output. Smaller
* prunes are silent. VACUUM is NEVER triggered automatically — heavyweight
* compaction stays operator-driven (`cleo doctor --vacuum`).
*
* @param projectRoot - Project root directory for brain.db resolution
* @param options - Retention configuration (see `PruneStaleHistoryOptions`)
* @returns Counts of rows deleted from each table
*
* @task T10348
* @epic T10286
*/
async function pruneStaleHistory(projectRoot, options) {
	const result = {
		plasticityEventsDeleted: 0,
		weightHistoryDeleted: 0,
		skipped: false
	};
	let retentionDays;
	if (options?.retentionDays !== void 0) retentionDays = options.retentionDays;
	else {
		const envRaw = process.env["CLEO_BRAIN_HISTORY_RETENTION_DAYS"];
		if (envRaw !== void 0 && envRaw !== "") {
			const parsed = Number.parseInt(envRaw, 10);
			retentionDays = Number.isFinite(parsed) && parsed >= 0 ? parsed : 30;
		} else retentionDays = 30;
	}
	if (retentionDays === 0) {
		result.skipped = true;
		return result;
	}
	const plasticityRowCap = options?.plasticityEventsRowCap ?? 5e4;
	const weightHistoryRowCap = options?.weightHistoryRowCap ?? 5e4;
	const { getBrainDb, getBrainNativeDb } = await import('./memory-sqlite-Dg5uGJCo.js');
	await getBrainDb(projectRoot);
	const nativeDb = getBrainNativeDb();
	if (!nativeDb) return result;
	const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
	const cutoffIso = new Date(cutoffMs).toISOString().replace("T", " ").slice(0, 19);
	try {
		nativeDb.prepare("SELECT 1 FROM brain_plasticity_events LIMIT 1").get();
		const changes = nativeDb.prepare(`DELETE FROM brain_plasticity_events WHERE timestamp < ?`).run(cutoffIso).changes;
		result.plasticityEventsDeleted += Number(changes ?? 0);
	} catch {}
	try {
		nativeDb.prepare("SELECT 1 FROM brain_weight_history LIMIT 1").get();
		const changes = nativeDb.prepare(`DELETE FROM brain_weight_history WHERE changed_at < ?`).run(cutoffIso).changes;
		result.weightHistoryDeleted += Number(changes ?? 0);
	} catch {}
	try {
		const row = nativeDb.prepare(`SELECT COUNT(*) AS cnt FROM brain_plasticity_events`).get();
		const count = Number(row?.cnt ?? 0);
		if (count > plasticityRowCap) {
			const overage = count - plasticityRowCap;
			const info = nativeDb.prepare(`DELETE FROM brain_plasticity_events
         WHERE id IN (
           SELECT id FROM brain_plasticity_events
           ORDER BY timestamp ASC, id ASC
           LIMIT ?
         )`).run(overage);
			result.plasticityEventsDeleted += Number(info.changes ?? 0);
		}
	} catch {}
	try {
		const row = nativeDb.prepare(`SELECT COUNT(*) AS cnt FROM brain_weight_history`).get();
		const count = Number(row?.cnt ?? 0);
		if (count > weightHistoryRowCap) {
			const overage = count - weightHistoryRowCap;
			const info = nativeDb.prepare(`DELETE FROM brain_weight_history
         WHERE id IN (
           SELECT id FROM brain_weight_history
           ORDER BY changed_at ASC, id ASC
           LIMIT ?
         )`).run(overage);
			result.weightHistoryDeleted += Number(info.changes ?? 0);
		}
	} catch {}
	if (result.plasticityEventsDeleted + result.weightHistoryDeleted >= 1e4) console.info(`[pruneStaleHistory] Pruned ${result.plasticityEventsDeleted} plasticity_events + ${result.weightHistoryDeleted} weight_history rows (retention=${retentionDays}d). Consider running 'cleo doctor --vacuum' to reclaim disk space.`);
	return result;
}

export { applyHomeostaticDecay, applyStdpPlasticity, backfillRewardSignals, pruneStaleHistory, shouldRunPlasticity };
//# sourceMappingURL=brain-stdp2-CIsmzhz3.js.map
