/** * Data accessor for brain.db — CRUD operations for decisions, patterns, * learnings, and memory links. * * Wraps drizzle ORM queries over the brain.db singleton. All methods are * async (sqlite-proxy) and return typed rows from memory-schema.ts. * * @epic T5149 * @task T5128 */ import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite'; import type { BrainAttentionRow, BrainConsolidationEventRow, BrainDecisionRow, BrainLearningRow, BrainMemoryLinkRow, BrainModulatorInsert, BrainModulatorRow, BrainObservationRow, BrainPageEdgeRow, BrainPageNodeRow, BrainPatternRow, BrainStickyNoteRow, BrainWeightHistoryInsert, BrainWeightHistoryRow, NewBrainAttentionRow, NewBrainDecisionRow, NewBrainLearningRow, NewBrainMemoryLinkRow, NewBrainObservationRow, NewBrainPageEdgeRow, NewBrainPageNodeRow, NewBrainPatternRow, NewBrainStickyNoteRow } from './schema/memory-schema.js'; import * as brainSchema from './schema/memory-schema.js'; export declare class BrainDataAccessor { private db; constructor(db: NodeSQLiteDatabase); addDecision(row: NewBrainDecisionRow): Promise; /** * Insert a decision while allocating the next sequential `Dnnn` id * **atomically inside the INSERT statement** (T11552). * * The `id` value is computed by an in-SQL subquery * (`MAX(numeric suffix) + 1`) evaluated as part of the same INSERT, so the * id read and the row write are a single indivisible operation. Two * concurrent callers therefore can never read the same `MAX(id)` and collide * on the primary key — unlike a read-then-write in application code, which * yields the event loop between the `SELECT MAX(id)` and the `INSERT` and so * races (the original defect). * * Properties: * - **Race-free under concurrency** — node:sqlite executes the statement * synchronously and atomically; no JS `await` boundary splits read from write. * - **Numeric ordering** — `CAST(substr(id,2) AS INTEGER)` so the sequence * advances correctly past `D999 → D1000` (lexical ordering would regress). * - **Legacy-id tolerant** — `GLOB 'D[0-9]*'` restricts the max scan to * canonical `Dnnn` ids, ignoring foreign shapes like `D-abcd`. * - **Never silently drops** — uses a real INSERT (not `INSERT OR IGNORE`) * and returns the persisted row. * * The caller MUST NOT pre-set a meaningful `row.id`; any value present is * overridden by the computed id. * * @param row - The decision row to insert (its `id` field is ignored). * @returns The persisted decision row, including the allocated `id`. * * @task T11552 */ addDecisionWithSequentialId(row: NewBrainDecisionRow): Promise; getDecision(id: string): Promise; findDecisions(params?: { type?: (typeof brainSchema.BRAIN_DECISION_TYPES)[number]; confidence?: (typeof brainSchema.BRAIN_CONFIDENCE_LEVELS)[number]; outcome?: (typeof brainSchema.BRAIN_OUTCOME_TYPES)[number]; contextTaskId?: string; limit?: number; /** * T1830: when false (default), AGT-* agent dispatch rows * (`decision_category = 'agent_dispatch'`) are excluded from results. * Pass true to include all categories. */ includeAgentDispatch?: boolean; }): Promise; updateDecision(id: string, updates: Partial): Promise; addPattern(row: NewBrainPatternRow): Promise; getPattern(id: string): Promise; findPatterns(params?: { type?: (typeof brainSchema.BRAIN_PATTERN_TYPES)[number]; impact?: (typeof brainSchema.BRAIN_IMPACT_LEVELS)[number]; minFrequency?: number; limit?: number; }): Promise; updatePattern(id: string, updates: Partial): Promise; addLearning(row: NewBrainLearningRow): Promise; getLearning(id: string): Promise; findLearnings(params?: { minConfidence?: number; actionable?: boolean; limit?: number; }): Promise; updateLearning(id: string, updates: Partial): Promise; addObservation(row: NewBrainObservationRow): Promise; getObservation(id: string): Promise; findObservations(params?: { type?: (typeof brainSchema.BRAIN_OBSERVATION_TYPES)[number]; project?: string; sourceType?: (typeof brainSchema.BRAIN_OBSERVATION_SOURCE_TYPES)[number]; sourceSessionId?: string; /** T417: filter by agent provenance name (Wave 8 mental models). */ agent?: string; limit?: number; }): Promise; updateObservation(id: string, updates: Partial): Promise; addLink(row: NewBrainMemoryLinkRow): Promise; getLinksForMemory(memoryType: (typeof brainSchema.BRAIN_MEMORY_TYPES)[number], memoryId: string): Promise; getLinksForTask(taskId: string): Promise; removeLink(memoryType: (typeof brainSchema.BRAIN_MEMORY_TYPES)[number], memoryId: string, taskId: string, linkType: (typeof brainSchema.BRAIN_LINK_TYPES)[number]): Promise; /** * Parse a `tags_json` text column into a deduplicated string-array. * * Used to keep the {@link brainSchema.stickyTags} junction in sync with the * legacy whole-array column. Invalid / non-array JSON yields an empty list. */ private static parseStickyTags; /** * Replace the junction rows for one sticky so they exactly mirror its tags. * * Delete-then-insert keeps `sticky_tags` authoritative without RMW races: * the prior tag set is dropped and the supplied set re-inserted. Called on * every sticky create/update (T11355). * * @param stickyId - The owning sticky note id. * @param tags - The full tag set the junction should reflect. */ private syncStickyTags; addStickyNote(row: NewBrainStickyNoteRow): Promise; getStickyNote(id: string): Promise; /** * Find sticky notes with optional column filters and an index-backed, * SQL-side tag filter via the {@link brainSchema.stickyTags} junction. * * When `tags` is supplied the query keeps only notes that contain ALL of the * requested tags (membership runs through a junction subquery, never a * load-all-then-JS-filter). The `limit` is applied at the SQL layer in every * case — including with a tag filter — so callers never over-fetch. * * @param params - Status/color/priority equality filters, an all-of `tags` * membership filter, and an optional row `limit`. * @returns Matching sticky-note rows, newest first. */ findStickyNotes(params?: { status?: (typeof brainSchema.BRAIN_STICKY_STATUSES)[number]; color?: (typeof brainSchema.BRAIN_STICKY_COLORS)[number]; priority?: (typeof brainSchema.BRAIN_STICKY_PRIORITIES)[number]; tags?: string[]; limit?: number; }): Promise; updateStickyNote(id: string, updates: Partial): Promise; deleteStickyNote(id: string): Promise; /** * Insert one attention (jot) row. * * Row-per-item: each jot is its own row so it decays and scopes * independently. `tags` is a JSONB BLOB written via the {@link jsonb} helper * (`toDriver` wraps in `jsonb()`), never serialized TEXT. Reads MUST use * `json_each` / `json(col)` — never the raw BLOB. * * @param row - New attention row (scope already resolved by the caller). * @returns The persisted row (tags read back via `json(col)` to satisfy the * JSONB read rule). * @task T11372 */ addAttention(row: NewBrainAttentionRow): Promise; /** * Fetch one attention row by id, reading `tags` whole-value via `json(col)`. * * A plain `select()` of the JSONB column would hit the {@link jsonb} * `fromDriver` guard and throw, so `tags` is projected with `jsonbText`. * * @param id - Attention item id. * @returns The row, or `null` when absent. * @task T11372 */ getAttention(id: string): Promise; /** * List attention items for a scope set, applying live-item + tag filters * entirely in SQL (never load-all-then-JS-filter). * * Scope filtering is by exact `(scope_kind, scope_id)` pairs — the visible * scope chain the caller resolved (narrowest → broader ancestors). Because * visibility is the scope key, items outside the chain are never read, so * cross-agent leakage is impossible by construction. * * Tag filtering uses `json_each(tags)` membership ("contains ALL" requested * tags) — a GROUP BY + HAVING COUNT(DISTINCT) subquery, exactly mirroring the * index-backed sticky-tags pattern but over the JSONB column directly. * * The default (`openOnly`, the common path) excludes items that are * `consolidated`/`discarded`, past `expires_at`, or below `decayThreshold`. * * @param params - Scope chain, optional tag filter, liveness controls, limit. * @returns Matching rows, newest first, `tags` read via `json(col)`. * @task T11372 */ findAttention(params: { /** Visible scope chain as `(kind, id)` pairs. Empty ⇒ no scope restriction. */ scopes?: Array<{ kind: (typeof brainSchema.BRAIN_ATTENTION_SCOPE_KINDS)[number]; id: string; }>; /** "Contains ALL" tag membership filter via `json_each`. */ tags?: string[]; /** When true (default) only live `open` items are returned. */ openOnly?: boolean; /** Decay floor; items with `decay_score < threshold` are excluded. */ decayThreshold?: number; /** Reference time (unix ms) for the TTL predicate. Defaults to `Date.now()`. */ now?: number; /** Max rows (SQL LIMIT — applied even with a tag filter). */ limit?: number; }): Promise; /** * Sweep expired / decayed open items to `status = 'discarded'`. * * Idempotent: only flips rows that are currently `open` AND past * `expires_at` (or, when `decayThreshold` is given, below the floor). Returns * the number of rows discarded so callers can log the sweep. * * @param params - Reference time + optional decay floor. * @returns Count of rows transitioned to `discarded`. * @task T11372 */ expireAttention(params?: { now?: number; decayThreshold?: number; }): Promise; /** * Set the lifecycle status of one attention item (e.g. `consolidated`). * * @param id - Attention item id. * @param status - New status. * @task T11372 */ setAttentionStatus(id: string, status: (typeof brainSchema.BRAIN_ATTENTION_STATUSES)[number]): Promise; /** * Shared SELECT that projects `tags` whole-value via `json(col)` so the * JSONB `fromDriver` guard is never hit. Newest-first ordering. * * @internal */ private selectAttention; addPageNode(node: NewBrainPageNodeRow): Promise; getPageNode(id: string): Promise; findPageNodes(params?: { nodeType?: (typeof brainSchema.BRAIN_NODE_TYPES)[number]; minQualityScore?: number; limit?: number; }): Promise; updatePageNode(id: string, updates: Partial): Promise; removePageNode(id: string): Promise; addPageEdge(edge: NewBrainPageEdgeRow): Promise; findPageEdges(params?: { edgeType?: (typeof brainSchema.BRAIN_EDGE_TYPES)[number]; provenance?: string; limit?: number; }): Promise; getPageEdges(nodeId: string, direction?: 'in' | 'out' | 'both'): Promise; getNeighbors(nodeId: string, edgeType?: (typeof brainSchema.BRAIN_EDGE_TYPES)[number]): Promise; removePageEdge(fromId: string, toId: string, edgeType: (typeof brainSchema.BRAIN_EDGE_TYPES)[number]): Promise; } /** * Factory: get a BrainDataAccessor backed by the brain.db singleton. */ export declare function getBrainAccessor(cwd?: string): Promise; /** * Insert one row into brain_weight_history. * Called by writeWeightHistory() in brain-stdp.ts for each LTP, LTD, Hebbian, * or prune event that crosses the 1e-6 negligibility threshold. * * @param cwd - Project root (locates brain.db). Defaults to process.cwd(). * @param input - Row data. `changedAt` defaults to SQLite datetime('now'). * @returns The inserted row with its generated id. * * @task T697 * @epic T673 */ export declare function insertWeightHistoryRow(cwd: string | undefined, input: BrainWeightHistoryInsert): Promise; /** * Insert one row into brain_modulators. * Called by backfillRewardSignals() for each task outcome it processes. * Both this INSERT and the retrieval_log UPDATE MUST run in the same logical * pass but in separate transactions (no ATTACH) — see spec §4.3. * * @param cwd - Project root (locates brain.db). Defaults to process.cwd(). * @param input - Row data. `createdAt` defaults to SQLite datetime('now'). * @returns The inserted row with its generated id. * * @task T699 * @epic T673 */ export declare function insertModulatorRow(cwd: string | undefined, input: BrainModulatorInsert): Promise; /** * Open a consolidation event row in brain_consolidation_events. * Call this at the START of runConsolidation before any steps execute. * Returns the new row id — pass it to logConsolidationComplete() when done. * * @param cwd - Project root (locates brain.db). * @param trigger - What initiated this consolidation run. * @param sessionId - Active session ID, if any. * @returns The id of the newly inserted row. * * @task T701 * @epic T673 */ export declare function logConsolidationStart(cwd: string | undefined, trigger: string, sessionId?: string): Promise; /** * Complete a consolidation event row by updating it with final results. * Call this at the END of runConsolidation after all steps complete. * * @param cwd - Project root (locates brain.db). * @param id - Row id returned by logConsolidationStart. * @param stats - JSON-serializable step results object. * @param durationMs - Total wall-clock duration in milliseconds. * @param succeeded - Whether the run completed without error. * @returns The updated row. * * @task T701 * @epic T673 */ export declare function logConsolidationComplete(cwd: string | undefined, id: number, stats: Record, durationMs: number, succeeded?: boolean): Promise; //# sourceMappingURL=memory-accessor.d.ts.map