/** * Per-corpus serialization lock. * * The corpus JSON (.json) and manifest JSON (.manifest.json) * are two separate files written under a single indexCorpus/refreshCorpus * call. Two concurrent mutators racing on the same corpus name can * interleave those writes — one saves corpus, the other saves manifest, * the pair no longer describes the same state. * * The fix is to serialize every mutating operation per corpus name. This * in-process lock is enough: the corpus files live under a single * ~/.ollama-intern/corpora/ directory shared by a single running server * process. Multi-process access (two servers, same corpus dir) is not a * supported configuration. * * Implementation: one Promise per corpus name. Each caller awaits * the prior promise, then registers its own. Released in a `finally` so a * throw inside `fn` doesn't strand the lock. * * H1-res (2026-07 health pass) — this queued wait is deliberately NOT * abort-aware, unlike the Ollama semaphore (semaphore.ts, H1). Two reasons * it's acceptable: (1) corpus mutations (index / refresh / amend) are NOT * wrapped in the tier-timeout guardrail — they're bounded corpus operations, * not generative tier calls — so no outer AbortSignal reaches this call path * to cancel a wait (every caller — indexCorpus, refreshCorpus, corpusAmend — * passes none). (2) A holder's runtime is bounded: its inner embed batches are * tier-bounded (guardrails/embedTimeout.ts) and index/refresh/amend are finite * passes over a finite corpus, so a waiter blocks at most one holder's bounded * runtime, never indefinitely. Single-process, same-corpus concurrent mutation * is also rare (one server process is the only supported config). Making the * queued turn itself interruptible would break the serialization guarantee this * lock exists to provide (torn corpus/manifest writes) for no real-world gain. * * Phase 7 / FT-001 event-emission policy: `withCorpusLock` is silent * by default. A future wave can wire an optional `onWait` callback for * operators who want lock-contention visibility; today the only * structured surface is `buildCorpusLockWaitEvent` below for consumers * that detect contention through other means (timing on a wrapped * call). Tagged `op: 'pack_step'` to keep the closed CorrelationOp * enum tight; corpus mutation is pack-step-adjacent. */ /** * Run `fn` while serialized against other calls for the same `name`. * Resolves (or rejects) with whatever `fn` returns. Other callers queued * on the same name wait until this one finishes. */ export declare function withCorpusLock(name: string, fn: () => Promise): Promise; /** * Detail payload for an operator-facing structured event emitted when * a corpus lock acquire had to wait (a prior mutator was in flight on * the same name). Lock contention is rare in single-process operation * but nonzero — concurrent index/refresh on the same corpus serializes * here, and an operator debugging "why was this slow?" benefits from * a wait_ms readout. * * Phase 7 / FT-001: tagged `op: 'pack_step'` (corpus mutation is * pack-step-adjacent; a separate corpus_lock op would inflate the * closed CorrelationOp enum). The NDJSON logger auto-merges `run_id` * from ALS at write time. * * `withCorpusLock` does NOT call this helper itself — wiring is the * caller's responsibility (a tool handler that times the lock and * emits the event when wait_ms exceeds its threshold). */ export interface CorpusLockWaitEventDetail { /** Closed-enum op tag from observability.CorrelationOp. */ op: "pack_step"; /** Stable rule identifier — greppable. */ rule: "corpus_lock_wait"; /** Corpus name being acquired. */ name: string; /** Wait duration in milliseconds. */ wait_ms: number; } /** * Build the structured-event detail for a corpus lock wait. Pure * shaping — does NOT call the logger itself. */ export declare function buildCorpusLockWaitEvent(args: { name: string; wait_ms: number; }): CorpusLockWaitEventDetail; //# sourceMappingURL=lock.d.ts.map