/** * 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 retention window (days) for `brain_plasticity_events`. * * Rows older than this many days are deleted by `pruneStaleHistory()` after * every `runConsolidation` pass. T10301 RCA established that this table grew * from 19K → 3.57M rows in 19 days (182×) under heavy consolidation traffic * because the table is append-only with no DELETE path. 30 days matches the * STDP `lookbackDays` window — events older than the STDP read horizon have * no further influence on plasticity computations, so they are safe to prune. * * @task T10348 * @epic T10286 */ export declare const MAX_PLASTICITY_EVENTS_RETENTION_DAYS = 30; /** * Default retention window (days) for `brain_weight_history`. * * Rows older than this many days are deleted by `pruneStaleHistory()` after * every `runConsolidation` pass. The schema-level comment on * `brainWeightHistory` (see `packages/core/src/store/memory-schema.ts`) states * "Retention policy: rolling 90 days" but that pruning was never wired — * T10348 wires it at 30 days (matching plasticity_events) to bound growth on * the larger of the two append-only logs. * * @task T10348 * @epic T10286 */ export declare const MAX_WEIGHT_HISTORY_RETENTION_DAYS = 30; /** * Hard row-count cap for `brain_plasticity_events`. * * Acts as a defensive upper bound when retention-by-days alone is insufficient * (e.g. a single day producing > MAX rows under unusually heavy consolidation * traffic, as happened on 2026-05-12 when 1.17M rows were inserted in 24 h). * When the table exceeds this cap, `pruneStaleHistory()` deletes the oldest * rows by `timestamp ASC` until the count is back under the cap. * * @task T10348 * @epic T10286 */ export declare const MAX_PLASTICITY_EVENTS_ROW_CAP = 50000; /** * Hard row-count cap for `brain_weight_history` — companion to * `MAX_PLASTICITY_EVENTS_ROW_CAP`. Same semantics. * * @task T10348 * @epic T10286 */ export declare const MAX_WEIGHT_HISTORY_ROW_CAP = 50000; /** * Row-deletion threshold above which `pruneStaleHistory()` emits a * `[pruneStaleHistory]` Pino info-level message. Below this threshold the * prune is silent. The threshold matches the T10301 RCA recommendation * ("optionally VACUUM if rows-deleted > 10_000") but `pruneStaleHistory()` * does NOT auto-run VACUUM — VACUUM under WAL is a heavyweight blocking * operation that should be operator-driven (see `cleo doctor --vacuum`). * * @task T10348 * @epic T10286 */ export declare const PRUNE_HISTORY_LOG_THRESHOLD = 10000; /** * Result returned by `backfillRewardSignals`. * * @task T681 * @epic T673 */ export interface RewardBackfillResult { /** Number of brain_retrieval_log rows updated with a reward_signal. */ rowsLabeled: number; /** Number of rows skipped (already labeled, backfill session, or no task match). */ rowsSkipped: number; } /** * Options for `applyStdpPlasticity`. * * ### Migration note — legacy number signature deprecated (T679) * * The old single-parameter signature `applyStdpPlasticity(root, sessionWindowMs)` * used the same value for both SQL lookback cutoff and spike-pair Δt gate. * All 38 live retrieval rows were older than 5 min, so zero events ever fired. * * Pass `lookbackDays` (how far back to fetch rows) and `pairingWindowMs` * (max Δt between two spikes for pair formation) as separate parameters. */ export interface StdpPlasticityOptions { /** * SQL lookback window: only retrieve log rows from the last N days. * Default: 30. A 30-day window captures all meaningful retrieval history * without unbounded growth. */ lookbackDays?: number; /** * Maximum Δt (ms) between two spikes for them to form a STDP pair. * T688 default: 24 h (86,400,000 ms) — cross-session pairs are eligible. * Tiered τ (T689) applies different decay constants based on session * relationship and Δt magnitude, so cross-session pairs still get * significantly smaller Δw than intra-batch pairs. */ pairingWindowMs?: number; } /** * 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 */ export declare function computeTau(deltaT: number): number; /** Result returned by `applyStdpPlasticity`. */ export interface StdpPlasticityResult { /** Number of LTP (potentiation) events applied. */ ltpEvents: number; /** Number of LTD (depression) events applied. */ ltdEvents: number; /** Number of new edges inserted (LTP on pairs without an existing edge). */ edgesCreated: number; /** Number of retrieval pairs examined. */ pairsExamined: number; /** * Number of pairs where reward_signal was non-null and modulated Δw. * Incremented for both LTP and LTD events that had a non-null reward. * * @task T692 */ rewardModulatedEvents: number; } /** Summary row from `getPlasticityStats`. */ export interface PlasticityStatsSummary { /** Total number of plasticity events ever recorded. */ totalEvents: number; /** Count of LTP events. */ ltpCount: number; /** Count of LTD events. */ ltdCount: number; /** Net weight delta summed across all events (positive = net strengthening). */ netDeltaW: number; /** Most recent event timestamp (ISO 8601), or null if no events. */ lastEventAt: string | null; /** Recent events (up to `limit`, newest first). */ recentEvents: RecentPlasticityEvent[]; } /** A single recent plasticity event for display. */ export interface RecentPlasticityEvent { /** Auto-increment event ID. */ id: number; /** Source node identifier. */ sourceNode: string; /** Target node identifier. */ targetNode: string; /** Signed weight delta. */ deltaW: number; /** 'ltp' or 'ltd'. */ kind: 'ltp' | 'ltd'; /** ISO 8601 timestamp. */ timestamp: string; /** Session ID, if available. */ sessionId: string | null; } /** * 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. */ export declare function shouldRunPlasticity(projectRoot: string, sessionId?: string | null, minRetrievalsForPlasticity?: number): Promise; export declare function applyStdpPlasticity(projectRoot: string, options?: StdpPlasticityOptions | number): Promise; /** * Retrieve a summary of recent STDP plasticity events from `brain_plasticity_events`. * * Used by `cleo brain plasticity stats`. * * @param projectRoot - Project root directory for brain.db resolution * @param limit - Maximum number of recent events to include. Defaults to 20. * @returns Aggregated plasticity statistics and the most recent events. */ export declare function getPlasticityStats(projectRoot: string, limit?: number): Promise; /** * 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 */ export declare function backfillRewardSignals(projectRoot: string, sessionId: string | null | undefined, lookbackDays?: number): Promise; /** * Options for `applyHomeostaticDecay`. * * @task T690 * @epic T673 */ export interface HomeostaticDecayOptions { /** * Fractional weight loss per day for idle edges. * Default: 0.02 (2%/day → weight halves in ~35 days). * Spec §3.9 default: decay_rate=0.02. */ decayRatePerDay?: number; /** * Days of idle time before decay begins. * Edges reinforced more recently than this are left untouched. * Default: 7 days — keeps weekly-session edges alive. */ gracePeriodDays?: number; /** * Edges whose post-decay weight falls below this threshold are pruned (deleted). * Default: 0.05 (5%) — no meaningful signal below this floor. */ pruneThreshold?: number; } /** * Result returned by `applyHomeostaticDecay`. * * @task T690 * @epic T673 */ export interface HomeostaticDecayResult { /** Number of edges whose weight was reduced by the decay pass (still above pruneThreshold). */ edgesDecayed: number; /** Number of edges deleted because their post-decay weight fell below pruneThreshold. */ edgesPruned: number; } /** * 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 */ export declare function applyHomeostaticDecay(projectRoot: string, options?: HomeostaticDecayOptions): Promise; /** * Options for `pruneStaleHistory`. * * @task T10348 * @epic T10286 */ export interface PruneStaleHistoryOptions { /** * Retention window (days) applied to BOTH tables. Rows older than this * many days are deleted. Default: 30 (see * `MAX_PLASTICITY_EVENTS_RETENTION_DAYS`). * * Operators can disable retention entirely by setting * `CLEO_BRAIN_HISTORY_RETENTION_DAYS=0` in the environment — when that env * var is `0`, `pruneStaleHistory()` is a no-op regardless of this option. */ retentionDays?: number; /** * Hard row cap for `brain_plasticity_events`. After the age-based prune, * if the row count still exceeds this number, the oldest rows are deleted * by `timestamp ASC` until the count is back under the cap. Default: * `MAX_PLASTICITY_EVENTS_ROW_CAP`. */ plasticityEventsRowCap?: number; /** * Hard row cap for `brain_weight_history`. Same semantics as * `plasticityEventsRowCap`. Default: `MAX_WEIGHT_HISTORY_ROW_CAP`. */ weightHistoryRowCap?: number; } /** * Result returned by `pruneStaleHistory`. * * @task T10348 * @epic T10286 */ export interface PruneStaleHistoryResult { /** Number of rows deleted from `brain_plasticity_events`. */ plasticityEventsDeleted: number; /** Number of rows deleted from `brain_weight_history`. */ weightHistoryDeleted: number; /** Whether retention was skipped (env override set to 0). */ skipped: boolean; } /** * 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 */ export declare function pruneStaleHistory(projectRoot: string, options?: PruneStaleHistoryOptions): Promise; //# sourceMappingURL=brain-stdp.d.ts.map