/** * Post-sync qmd reindex. * * Why this lives in the runner (not just an HQ-core script): the qmd search * index is a per-machine local store, and nothing re-indexed it after a sync * pulled new files in — so teammates saw divergent search results depending * on who ran `qmd update` most recently, and newly-synced knowledge folders * weren't searchable until someone manually registered them as a collection. * * The runner ships via `npx @indigoai-us/hq-cloud@latest` (both the AppBar * menubar and the `/hq-sync` CLI pull it at runtime), so putting the fix here * reaches every teammate on their next sync WITHOUT requiring them to update * their HQ core. It is therefore intentionally self-contained: it shells out * to the globally-installed `qmd` binary directly and does NOT depend on any * script inside the synced HQ tree (which may be stale). * * What it does, best-effort and idempotent: * 1. Auto-registers populated company knowledge, company project, and * personal knowledge dirs that aren't yet qmd collections (kills the * manual "map" step). Company knowledge detects path drift when the name * exists but points elsewhere; repairs only when * `HQ_QMD_REPAIR_PATH_DRIFT=1` (non-breaking default: detect-only). * 2. Runs an incremental lexical `qmd update` (fast — qmd skips unchanged * files by mtime). * 3. Rebuilds embeddings only when `embed: true` (slow on a multi-GB * index; meant for an idle pass, not every sync). Never embeds in the * same cycle as a path-drift repair (repair drops that collection's * vectors). * * The index itself is never synced — it is large, binary, and embeds absolute * local paths. Only its *freshness* is automated here. * * ## Corruption safety (feedback_b9a369ff + feedback_332c7ccc) * * The qmd store (sqlite-vec) was being corrupted on the vector side because * TWO uncoordinated writers raced on it. Mitigations: * - Raised exec timeout so legitimate long passes are not killed mid-write. * - Dual advisory lock: legacy `/.qmd/.reindex.lock` AND * `/.reindex.lock` (deduped when equal). Default index * is often `~/.cache/qmd/index.sqlite`, not `/.qmd`. * - Corruption quarantine targets the resolved index dir; reports success * only when files were actually moved. */ /** Result of a single `qmd` invocation. */ export interface QmdExecResult { status: number | null; stdout: string; /** Captured stderr (qmd prints SQLITE_CORRUPT errors here). Optional for fakes. */ stderr?: string; /** True when the process was killed because it exceeded the exec timeout. */ timedOut?: boolean; } /** Injectable command runner — real `spawnSync` in prod, a fake in tests. */ export interface QmdExec { (args: string[]): QmdExecResult; } /** * Default exec timeout for a single `qmd` invocation. The historical 120s bound * routinely killed `qmd update`/`qmd embed` mid-write on a large index (the HQ * root's `hq` collection alone spans the whole tree), which corrupted the vector * store. 15 minutes comfortably covers a cold or embed-heavy pass, so the bound * effectively stops firing on legitimate work; a genuinely wedged process is * still bounded, and a timed-out pass is treated as not-done (never written * further onto) — see {@link defaultExec} and {@link reindexAfterSync}. */ export declare const DEFAULT_QMD_EXEC_TIMEOUT_MS = 900000; /** Resolve the exec timeout, honoring `HQ_QMD_EXEC_TIMEOUT_MS` (ms) if valid. */ export declare function resolveExecTimeoutMs(env?: NodeJS.ProcessEnv): number; /** Opt-in destructive path-drift repair (remove + re-add). Default off. */ export declare function repairPathDriftEnabled(env?: NodeJS.ProcessEnv): boolean; /** * Parse the directory that holds `index.sqlite` from `qmd status` output. * Returns null when the Index line is missing/unparseable. */ export declare function parseIndexDirFromStatus(stdout: string): string | null; /** Parse `Path:` from `qmd collection show` human output. */ export declare function parseCollectionPathFromShow(stdout: string): string | null; /** Case-insensitive, trailing-separator-tolerant path equality after realpath. */ export declare function pathsEquivalent(a: string, b: string, realpathSync?: (p: string) => string): boolean; /** True if a qmd result reports SQLite corruption on stdout or stderr. */ export declare function looksCorrupt(r: Partial>): boolean; /** Handle returned by an acquired reindex lock. `release()` is idempotent. */ export interface ReindexLockHandle { release(): void; } /** * Acquire the shared reindex lock at `lockPath`. Returns a handle when acquired, * or `null` when another live writer already holds it (caller should skip the * cycle). Must never throw — coordination is advisory and must not fail a sync. */ export type AcquireReindexLock = (lockPath: string) => ReindexLockHandle | null; /** * Acquire unique lock paths (legacy + resolved index). If any is busy, release * what was acquired and return busy. Dedupes when both paths are the same so a * process never O_EXCL-locks itself out. */ export declare function acquireDualReindexLocks(acquireLock: AcquireReindexLock, lockPaths: string[]): { locks: ReindexLockHandle[]; busy: boolean; }; /** Move corrupt DB files aside (never delete) so a clean rebuild can follow. */ export type QuarantineCorruptIndex = (qmdDir: string, timestampSuffix: string) => string | null; export interface ReindexOptions { /** Rebuild embeddings too (slow). Default false — lexical-only. */ embed?: boolean; /** * HQ-relative paths changed by sync. When omitted, preserves the historical * behavior and runs QMD for any sync caller that invokes this function. */ changedPaths?: string[]; /** Force a collection registration refresh even when content is not dirty. */ forceCollectionRefresh?: boolean; /** Delay dirty updates by this many ms while keeping a persistent marker. */ debounceMs?: number; /** Clock override for deterministic debounce tests. */ nowMs?: number; /** State file override for tests. */ statePath?: string; /** Command runner override for tests. */ exec?: QmdExec; /** `existsSync` override for tests. */ existsSync?: (p: string) => boolean; /** `readdirSync` override for tests (returns subdir names of companies/). */ readCompanies?: (companiesDir: string) => string[]; /** Returns true if the knowledge dir has at least one indexable .md file. */ hasIndexableMarkdown?: (knowledgeDir: string) => boolean; /** Returns true if the projects dir has at least one indexable .md or .json file. */ hasIndexableProjectContent?: (projectsDir: string) => boolean; /** Reindex-lock acquirer override for tests. */ acquireReindexLock?: AcquireReindexLock; /** Corrupt-index quarantine override for tests. */ quarantineCorruptIndex?: QuarantineCorruptIndex; /** realpath override for path-drift comparison in tests. */ realpathSync?: (p: string) => string; /** Env override for tests (repair flag, etc.). */ env?: NodeJS.ProcessEnv; /** Optional diagnostic sink for unexpected swallowed failures. */ log?: (diagnostic: { event: string; message: string; err: unknown; context?: Record; }) => void; } export interface ReindexResult { qmdAvailable: boolean; collectionsAdded: string[]; /** HQ-convention collections whose registered path differs from desired. */ pathDriftDetected: string[]; /** Collections successfully remove+re-added when repair was enabled. */ collectionsRepaired: string[]; updated: boolean; embedded: boolean; pendingDirty: boolean; /** True when the cycle was skipped because another writer held the lock. */ lockBusy: boolean; /** True when a qmd command exceeded the exec timeout and was aborted. */ timedOut: boolean; /** True when a corrupt index was detected and files were quarantined. */ corruptionQuarantined: boolean; /** True when corruption was detected but quarantine moved nothing. */ corruptionQuarantineFailed: boolean; /** Directory used for quarantine / index-side lock (resolved or fallback). */ indexDir: string | null; } /** * Reindex qmd for an HQ tree after a sync. Never throws — all failures are * swallowed so a reindex problem can never mask or fail the sync result. * * @returns a small summary for logging/telemetry (collectionsAdded, updated). */ export declare function reindexAfterSync(hqRoot: string, opts?: ReindexOptions): ReindexResult; //# sourceMappingURL=qmd-reindex.d.ts.map