/** * BRAIN single-writer chokepoint — main-thread queue manager. * * All hot-path writes to `brain.db` MUST route through `enqueueBrainWrite` so * that a single Node.js `worker_threads.Worker` owns the only write handle and * serializes every INSERT/UPDATE through one consumer. This eliminates the * within-process race documented in the T10301 RCA (page-1 sqlite_schema * B-tree corruption caused by concurrent setImmediate writers + dialectic-hook * + propose-tick reconciler + STDP plasticity loop all opening their own * `getBrainDb` singletons). * * ## Architecture * * Main thread Worker thread * ─────────── ───────────── * enqueueBrainWrite(op) (owns getBrainDb handle) * │ ▲ * ▼ │ MessagePort * pendingRequests[seq] = {resolve, reject} │ * │ │ * └──── postMessage({seq, op}) ────────┘ * │ * ▼ * handleWriteOp(op) * (SQL INSERT/UPDATE) * │ * ┌──── postMessage({seq, ok, ...})────┘ * ▼ * resolve(...) → caller's await * * Reads continue to use `getBrainDb` / `getBrainNativeDb` directly. SQLite WAL * permits concurrent readers; only writes must be funneled. * * ## Bypass mechanism * * Forensic / one-shot scripts that need a direct write handle can set * `CLEO_BRAIN_BYPASS_WRITER_THREAD=1`. Each call to `enqueueBrainWrite` then * executes the op inline on the main thread. **A Pino warn is emitted on every * bypass** (audit trail per AC #5). * * @task T10351 * @epic T10286 * @saga T10281 */ import type { ObserveBrainParams, ObserveBrainResult } from '@cleocode/contracts'; import type { NewBrainDecisionRow, NewBrainLearningRow } from '../store/schema/memory-schema.js'; /** * Discriminated union of all hot-path brain.db write operations. * * Adding a new op kind: * 1. Add the variant here. * 2. Implement the corresponding handler in `brain-writer-worker.ts`. * 3. Update the BrainWriteResult union to mirror the return shape. */ export type BrainWriteOp = BrainObserveOp | BrainDecisionOp | BrainLearningOp | BrainPlasticityEventOp | BrainWeightUpdateOp | BrainDialecticOp; /** Insert a new observation row via the canonical `observeBrain` pipeline. */ export interface BrainObserveOp { kind: 'observe'; projectRoot: string; params: ObserveBrainParams; } /** Insert a new decision row. */ export interface BrainDecisionOp { kind: 'decision'; projectRoot: string; /** Fully-prepared NewBrainDecisionRow shape (serializable). */ row: NewBrainDecisionRow; } /** Insert a new learning row. */ export interface BrainLearningOp { kind: 'learning'; projectRoot: string; /** Fully-prepared NewBrainLearningRow shape (serializable). */ row: NewBrainLearningRow; } /** Insert a brain_plasticity_events row (raw SQL params, T679 ordering). */ export interface BrainPlasticityEventOp { kind: 'plasticity_event'; projectRoot: string; sourceNode: string; targetNode: string; deltaW: number; eventKind: 'ltp' | 'ltd' | 'hebbian'; timestamp: string; sessionId: string | null; retrievalLogId: number | null; weightBefore: number | null; weightAfter: number; deltaTms: number; } /** Insert a brain_weight_history row (T679 spec §2.1.4). */ export interface BrainWeightUpdateOp { kind: 'weight_update'; projectRoot: string; edgeFromId: string; edgeToId: string; edgeType: 'co_retrieved' | 'related' | 'depends_on' | 'caused_by'; weightBefore: number | null; weightAfter: number; deltaWeight: number; eventKind: 'ltp' | 'ltd' | 'hebbian'; sourcePlasticityEventId: number | null; retrievalLogId: number | null; rewardSignal: number | null; changedAt: string; } /** * Dialectic composite op — fires the full `applyInsights` pipeline on the * worker (calls into `observeBrain` and `appendNarrativeDelta`). */ export interface BrainDialecticOp { kind: 'dialectic'; projectRoot: string; sessionId: string; activePeerId: string; insights: SerializedDialecticInsights; } /** * Serializable copy of `DialecticInsights` (the `applyInsights` callers pass * objects already shaped by the evaluator; we re-declare the shape here so the * writer-thread layer does not depend on the evaluator module). */ export interface SerializedDialecticInsights { globalTraits: Array<{ key: string; value: string; confidence: number; }>; peerInsights: Array<{ key: string; value: string; confidence: number; }>; sessionNarrativeDelta: string | null; } /** Successful result of a write op (shape varies by op kind). */ export type BrainWriteResult = { kind: 'observe'; result: ObserveBrainResult; } | { kind: 'decision'; id: string; } | { kind: 'learning'; id: string; } | { kind: 'plasticity_event'; lastInsertRowid: number | null; } | { kind: 'weight_update'; ok: true; } | { kind: 'dialectic'; ok: true; }; /** Outbound message envelope (main → worker). */ export interface WriterRequestEnvelope { seq: number; op: BrainWriteOp; } /** Inbound message envelope (worker → main). */ export type WriterResponseEnvelope = { seq: number; ok: true; result: BrainWriteResult; } | { seq: number; ok: false; error: string; }; /** * Enqueue a brain.db write op through the single-writer chokepoint. * * Resolves with the typed result for the op's `kind`. Rejects when: * - the op fails inside the worker (worker forwards the error message) * - the worker crashes (the in-flight promise rejects with the worker error) * - bypass mode is on AND the inline execution fails * * @example * ```ts * const result = await enqueueBrainWrite({ * kind: 'observe', * projectRoot: '/path/to/project', * params: { text: 'Decided X over Y', sourceType: 'manual' }, * }); * if (result.kind === 'observe') { * console.log('Stored observation', result.result.id); * } * ``` * * @param op - The write op (discriminated by `kind`). * @returns Worker result for the op. */ export declare function enqueueBrainWrite(op: BrainWriteOp): Promise; /** * Shutdown the writer-thread singleton (mostly used by tests). * @internal */ export declare function shutdownBrainWriter(): Promise; /** * Test helper — reset internal state without shutting the worker down. * @internal */ export declare function _resetBrainWriterForTests(): void; //# sourceMappingURL=brain-writer-thread.d.ts.map