/** * BRAIN single-writer chokepoint — main-thread queue manager. * * All hot-path writes to `brain.db` MUST route through `enqueueBrainWrite`, * which 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). * * ## Serialization primitive * * Every write holds the cross-process `brain` writer lease (batch-granular, see * `enterBrainBatchLease`) and runs through one in-process async mutex * (`runInline`). That pair is the chokepoint in every process (T13126). * * ## Worker thread (explicit opt-in for long-lived hosts) * * A long-lived host (the sentient daemon, the gateway daemon, the HTTP/MCP * servers, agent harness hosts) calls {@link useBrainWriterThread} at startup, * or is launched with `CLEO_BRAIN_WRITER_THREAD=1`. Its writes then run on one * `worker_threads.Worker` that owns the only write handle. A one-shot CLI * command does not opt in: spawning a second isolate and opening the store a * second time cost ~300 MB per `cleo memory observe` for a single write. A host * that forgets to opt in still writes correctly, inline under the mutex. * * ## Worker 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 | BrainEmbedOp; /** 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; } /** * Upsert computed memory embeddings (T13218). The vectors are computed * BEFORE the op (model inference stays outside the chokepoint); only the * `brain_embeddings` writes travel through it, so a backfill never opens a * second write handle beside the worker's. */ export interface BrainEmbedOp { kind: 'embed'; projectRoot: string; /** Model tag of the provider that computed every vector in {@link rows} (T13740). */ model: string; /** One row per memory entry: its id and its embedding vector. */ rows: Array<{ id: string; vector: Float32Array; }>; } /** * 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; } | { kind: 'embed'; written: number; }; /** 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; }; /** * Ask the writer worker to finish its background work (scheduled embeddings) * before it is terminated (T14069). It answers {@link WriterDrainedMessage}. */ export interface WriterDrainMessage { drain: true; } /** The writer worker finished its background work and may be terminated (T14069). */ export interface WriterDrainedMessage { drained: true; } /** * How long {@link drainAndTerminateWriter} waits for the worker's background * work before it terminates the worker anyway (T14069). */ export declare const WRITER_DRAIN_TIMEOUT_MS = 10000; /** * Stop a brain writer worker without cutting through its background work * (T14069): ask it to drain, wait for its answer (at most `timeoutMs`), then * terminate it. The worker embeds new observations inline, which loads the * native onnxruntime addon; terminating the isolate mid-load or mid-inference * can abort the whole process, and vitest reports that as "Worker exited * unexpectedly". * * @param worker - The writer worker. * @param timeoutMs - Longest wait for the drain. @defaultValue {@link WRITER_DRAIN_TIMEOUT_MS} * * @task T14069 */ export declare function drainAndTerminateWriter(worker: import('node:worker_threads').Worker, timeoutMs?: number): Promise; /** * Opt this process in to the worker-thread brain writer. * * Call once at startup from a long-lived host (daemon, gateway server, agent * harness host) whose writes are frequent enough to amortise a second isolate. * One-shot CLI commands must not call it: without it, writes run inline under * the same writer lease and async mutex, which is equally serialized. The same * declaration makes `observeBrain` embed new observations as they are stored * (see `brain-host.ts`). * * @example * ```ts * import { useBrainWriterThread } from '@cleocode/core/memory/brain-writer-thread'; * useBrainWriterThread(); * ``` */ export declare function useBrainWriterThread(): void; /** * Whether brain writes in this process run on the worker thread. * * @returns `true` after {@link useBrainWriterThread} or with * `CLEO_BRAIN_WRITER_THREAD=1`; `false` otherwise (inline, serialized). */ export declare function brainWriterThreadEnabled(): boolean; /** * Whether the brain writer has been shut down for this process. * * @returns `true` once `shutdownBrainWriter()` has begun. */ export declare function isBrainWriterShuttingDown(): boolean; /** * Raised when a brain write is attempted after teardown has begun. * * Deliberately NOT a `CleoError`: it is always caught one frame up in * `enqueueBrainWrite`, which routes the op to `runInline`, so it must never * reach a LAFS envelope. (`scripts/lint-cleo-errors.mjs` scans only the `tasks` * and `validation` trees, so it does not apply here — stated so nobody * "corrects" this to a CleoError and turns a handled fallback into a * user-visible failure.) */ export declare class BrainWriterShutDownError extends Error { readonly codeName: "E_BRAIN_WRITER_SHUTDOWN"; constructor(kind: BrainWriteOp['kind']); } /** * Enqueue a brain.db write op through the single-writer chokepoint. * * Runs inline under the brain writer lease and the in-process mutex, or on the * worker thread when the host opted in ({@link useBrainWriterThread}). * * Resolves with the typed result for the op's `kind`. Rejects when: * - the op fails (inline, or inside the worker, which forwards the 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`). * @throws Error when the op fails inline or in the worker. * @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