memorix 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/cli/index.js +5273 -4730
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +506 -99
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.js +176 -53
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +33 -0
- package/dist/sdk.d.ts +4 -2
- package/dist/sdk.js +507 -99
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +8 -1
- package/dist/types.js.map +1 -1
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +86 -0
- package/docs/1.2.5-RETRIEVAL-PERFORMANCE.md +85 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +13 -1
- package/docs/API_REFERENCE.md +13 -3
- package/docs/PERFORMANCE.md +22 -1
- package/docs/dev-log/progress.txt +73 -7
- package/package.json +2 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/capability-map.ts +1 -1
- package/src/cli/command-guide.ts +4 -1
- package/src/cli/commands/agent-integrations.ts +5 -1
- package/src/cli/commands/codegraph.ts +1 -1
- package/src/cli/commands/context.ts +9 -1
- package/src/cli/commands/memory.ts +12 -3
- package/src/cli/commands/operator-shared.ts +74 -2
- package/src/cli/commands/resume.ts +31 -0
- package/src/cli/index.ts +5 -1
- package/src/codegraph/auto-context.ts +54 -1
- package/src/codegraph/task-lens.ts +29 -0
- package/src/compact/token-budget.ts +16 -1
- package/src/config/resolved-config.ts +29 -4
- package/src/config/toml-loader.ts +9 -5
- package/src/hooks/handler.ts +127 -66
- package/src/hooks/installers/index.ts +5 -4
- package/src/hooks/official-skills.ts +6 -4
- package/src/hooks/rules/memorix-agent-rules.md +9 -7
- package/src/knowledge/context-assembly.ts +4 -1
- package/src/knowledge/workset.ts +89 -1
- package/src/memory/session.ts +144 -10
- package/src/runtime/project-maintenance.ts +1 -14
- package/src/sdk.ts +4 -0
- package/src/server.ts +144 -25
- package/src/store/bun-sqlite-compat.ts +118 -15
- package/src/store/orama-store.ts +39 -18
- package/src/store/sqlite-db.ts +3 -3
- package/src/timeout.ts +23 -0
- package/src/types.ts +8 -0
|
@@ -1,21 +1,120 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bun SQLite Compatibility Layer
|
|
3
3
|
*
|
|
4
|
-
* Provides a better-sqlite3
|
|
5
|
-
*
|
|
4
|
+
* Provides a small better-sqlite3 compatibility layer for the available local
|
|
5
|
+
* SQLite implementation. Node 22 ships node:sqlite, which keeps Memorix
|
|
6
|
+
* usable when an optional better-sqlite3 native binary is unavailable.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { createRequire } from 'node:module';
|
|
9
10
|
|
|
10
11
|
let Database: any;
|
|
12
|
+
let driver: 'better-sqlite3' | 'node:sqlite' | 'bun:sqlite' | undefined;
|
|
11
13
|
const requireFromHere = createRequire(import.meta.url);
|
|
12
14
|
|
|
15
|
+
function loadNodeSqlite(): any {
|
|
16
|
+
const nodeSqlite = requireFromHere('node:sqlite');
|
|
17
|
+
return nodeSqlite.DatabaseSync;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isNativeBindingFailure(error: unknown): boolean {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
return /could not locate the bindings file|better_sqlite3\.node|module did not self-register/i.test(message);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function addCompatibility(db: any): any {
|
|
26
|
+
// Bun and node:sqlite do not expose better-sqlite3's pragma helper.
|
|
27
|
+
if (!db.pragma) {
|
|
28
|
+
db.pragma = function (pragma: string, options?: { simple?: boolean }) {
|
|
29
|
+
const statement = db.prepare(`PRAGMA ${pragma}`);
|
|
30
|
+
if (options?.simple) {
|
|
31
|
+
const result = statement.get();
|
|
32
|
+
return result ? Object.values(result)[0] : undefined;
|
|
33
|
+
}
|
|
34
|
+
return statement.all();
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Stores use better-sqlite3's synchronous transaction factory. Keep nested
|
|
39
|
+
// calls correct with savepoints instead of silently committing inner work.
|
|
40
|
+
if (!db.transaction) {
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let sequence = 0;
|
|
43
|
+
db.transaction = function <T>(fn: (...args: any[]) => T) {
|
|
44
|
+
return (...args: any[]): T => {
|
|
45
|
+
const outermost = depth === 0;
|
|
46
|
+
const savepoint = `memorix_tx_${++sequence}`;
|
|
47
|
+
if (outermost) {
|
|
48
|
+
db.exec('BEGIN IMMEDIATE');
|
|
49
|
+
} else {
|
|
50
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
51
|
+
}
|
|
52
|
+
depth += 1;
|
|
53
|
+
try {
|
|
54
|
+
const result = fn(...args);
|
|
55
|
+
if (result && typeof (result as any).then === 'function') {
|
|
56
|
+
throw new Error('[memorix] SQLite transactions must be synchronous');
|
|
57
|
+
}
|
|
58
|
+
depth -= 1;
|
|
59
|
+
if (outermost) {
|
|
60
|
+
db.exec('COMMIT');
|
|
61
|
+
} else {
|
|
62
|
+
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
depth -= 1;
|
|
67
|
+
try {
|
|
68
|
+
if (outermost) {
|
|
69
|
+
db.exec('ROLLBACK');
|
|
70
|
+
} else {
|
|
71
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
72
|
+
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Preserve the original application failure when rollback itself fails.
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return db;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function instantiateDatabase(Sqlite: any, filePath: string, options?: any): any {
|
|
87
|
+
// DatabaseSync validates that an explicitly supplied second argument is an
|
|
88
|
+
// object, unlike better-sqlite3 which accepts undefined.
|
|
89
|
+
return driver === 'node:sqlite' && options === undefined
|
|
90
|
+
? new Sqlite(filePath)
|
|
91
|
+
: new Sqlite(filePath, options);
|
|
92
|
+
}
|
|
93
|
+
|
|
13
94
|
export function loadSqlite(): any {
|
|
14
95
|
if (Database) return Database;
|
|
15
96
|
|
|
97
|
+
// Kept as an operational escape hatch and a regression-test seam. Node 22
|
|
98
|
+
// is the supported minimum runtime, so this does not expand the platform
|
|
99
|
+
// contract beyond package.json's existing engine requirement.
|
|
100
|
+
if (process.env.MEMORIX_SQLITE_DRIVER === 'node') {
|
|
101
|
+
Database = loadNodeSqlite();
|
|
102
|
+
driver = 'node:sqlite';
|
|
103
|
+
return Database;
|
|
104
|
+
}
|
|
105
|
+
|
|
16
106
|
// Try better-sqlite3 first (Node.js)
|
|
17
107
|
try {
|
|
18
108
|
Database = requireFromHere('better-sqlite3');
|
|
109
|
+
driver = 'better-sqlite3';
|
|
110
|
+
return Database;
|
|
111
|
+
} catch {
|
|
112
|
+
// Fall through to node:sqlite, then Bun's runtime implementation.
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
Database = loadNodeSqlite();
|
|
117
|
+
driver = 'node:sqlite';
|
|
19
118
|
return Database;
|
|
20
119
|
} catch {
|
|
21
120
|
// Fall through to bun:sqlite
|
|
@@ -26,9 +125,10 @@ export function loadSqlite(): any {
|
|
|
26
125
|
// bun:sqlite is a Bun built-in
|
|
27
126
|
const bunSqlite = requireFromHere('bun:sqlite');
|
|
28
127
|
Database = bunSqlite.Database;
|
|
128
|
+
driver = 'bun:sqlite';
|
|
29
129
|
return Database;
|
|
30
130
|
} catch {
|
|
31
|
-
throw new Error('[memorix]
|
|
131
|
+
throw new Error('[memorix] SQLite is unavailable (better-sqlite3, node:sqlite, and bun:sqlite failed)');
|
|
32
132
|
}
|
|
33
133
|
}
|
|
34
134
|
|
|
@@ -38,18 +138,21 @@ export function loadSqlite(): any {
|
|
|
38
138
|
*/
|
|
39
139
|
export function createDatabase(path: string, options?: any): any {
|
|
40
140
|
const Sqlite = loadSqlite();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
141
|
+
try {
|
|
142
|
+
return addCompatibility(instantiateDatabase(Sqlite, path, options));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// require('better-sqlite3') can succeed even when npm had no prebuild and
|
|
145
|
+
// no local compiler. Fall back to Node's built-in SQLite for that one
|
|
146
|
+
// recoverable case; real open/corruption errors still surface unchanged.
|
|
147
|
+
if (driver !== 'better-sqlite3' || !isNativeBindingFailure(error)) throw error;
|
|
148
|
+
Database = loadNodeSqlite();
|
|
149
|
+
driver = 'node:sqlite';
|
|
150
|
+
return addCompatibility(instantiateDatabase(Database, path, options));
|
|
52
151
|
}
|
|
152
|
+
}
|
|
53
153
|
|
|
54
|
-
|
|
154
|
+
/** Test-only reset for the driver cache used by the forced node:sqlite path. */
|
|
155
|
+
export function resetSqliteDriverForTests(): void {
|
|
156
|
+
Database = undefined;
|
|
157
|
+
driver = undefined;
|
|
55
158
|
}
|
package/src/store/orama-store.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { getEmbeddingProvider, type EmbeddingProvider } from '../embedding/provi
|
|
|
17
17
|
import { calculateProjectAffinity, extractProjectKeywords, type AffinityContext, type MemoryContent } from './project-affinity.js';
|
|
18
18
|
import { detectQueryIntent, applyIntentBoost } from '../search/intent-detector.js';
|
|
19
19
|
import { maybeExpandSearchQuery } from '../search/query-expansion.js';
|
|
20
|
+
import { withTimeout } from '../timeout.js';
|
|
20
21
|
|
|
21
22
|
let db: AnyOrama | null = null;
|
|
22
23
|
let dbInitPromise: Promise<AnyOrama> | null = null;
|
|
@@ -501,8 +502,12 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
501
502
|
const t0 = perf ? performance.now() : 0;
|
|
502
503
|
const mark = (label: string) => { if (perf) { const now = performance.now(); process.stderr.write(` [search-perf] ${label}: ${(now - t0).toFixed(0)}ms\n`); } };
|
|
503
504
|
const modeKey = options.projectId ?? SEARCH_MODE_DEFAULT_KEY;
|
|
504
|
-
|
|
505
|
+
const quality = options.quality ?? 'balanced';
|
|
505
506
|
const database = await getDb();
|
|
507
|
+
lastSearchModeByProject.set(
|
|
508
|
+
modeKey,
|
|
509
|
+
quality === 'fast' ? 'fulltext (fast profile)' : embeddingEnabled ? 'hybrid' : 'fulltext',
|
|
510
|
+
);
|
|
506
511
|
|
|
507
512
|
// Resolve project aliases — safety net for observations not yet migrated to canonical ID.
|
|
508
513
|
// After migration, this is typically a single-element array matching options.projectId.
|
|
@@ -531,17 +536,28 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
531
536
|
// Determine search mode: hybrid (with vector) or fulltext (default)
|
|
532
537
|
const hasQuery = options.query && options.query.trim().length > 0;
|
|
533
538
|
const originalQuery = options.query;
|
|
534
|
-
|
|
539
|
+
// The fast profile is a bounded local probe, not a lower-quality spelling
|
|
540
|
+
// corrector. Skip query classification and the expensive recall enrichments
|
|
541
|
+
// it would otherwise inherit from the balanced path.
|
|
542
|
+
const tier = quality === 'fast' ? 'fast' as QueryTier : hasQuery ? classifyQueryTier(originalQuery!) : 'fast' as QueryTier;
|
|
535
543
|
mark(`tier=${tier}`);
|
|
536
544
|
|
|
537
|
-
//
|
|
538
|
-
|
|
545
|
+
// LLM quality work is explicit. It never runs on the default search path.
|
|
546
|
+
if (quality === 'thorough' && hasQuery) {
|
|
547
|
+
const { initLLM, isLLMEnabled } = await import('../llm/provider.js');
|
|
548
|
+
if (!isLLMEnabled()) initLLM();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Query expansion: only for thorough heavy-tier (CJK) searches.
|
|
552
|
+
const expandedEmbeddingQuery = quality === 'thorough' && tier === 'heavy'
|
|
553
|
+
? await maybeExpandSearchQuery(options.query!)
|
|
554
|
+
: options.query;
|
|
539
555
|
mark('queryExpansion');
|
|
540
556
|
|
|
541
557
|
// ── Intent-Aware Recall ──────────────────────────────────────
|
|
542
558
|
// Detect query intent (why/when/how/what/problem) and adjust
|
|
543
559
|
// field weights and type boosting accordingly.
|
|
544
|
-
const intentResult = hasQuery ? detectQueryIntent(originalQuery!)
|
|
560
|
+
const intentResult = quality === 'fast' || !hasQuery ? null : detectQueryIntent(originalQuery!);
|
|
545
561
|
|
|
546
562
|
// Orama's vector/hybrid search can leak cross-project hits even when `where`
|
|
547
563
|
// is present, so always keep enough headroom for a deterministic post-filter.
|
|
@@ -565,20 +581,24 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
565
581
|
let searchParams: Record<string, unknown> = {
|
|
566
582
|
term: originalQuery,
|
|
567
583
|
limit: requestLimit,
|
|
568
|
-
|
|
584
|
+
// Fast search never uses vectors, so returning them only adds work and
|
|
585
|
+
// payload pressure to an explicitly latency-sensitive path.
|
|
586
|
+
includeVectors: quality !== 'fast',
|
|
569
587
|
...(Object.keys(filters).length > 0 ? { where: filters } : {}),
|
|
570
588
|
// Search specific fields (not tokens, accessCount, etc.)
|
|
571
589
|
properties: ['title', 'entityName', 'narrative', 'facts', 'concepts', 'filesModified'],
|
|
572
590
|
// Field boosting: intent-aware or default
|
|
573
591
|
boost: fieldBoost,
|
|
574
592
|
// Fuzzy tolerance: allow 1-char typos for short queries, 2 for longer
|
|
575
|
-
|
|
593
|
+
// Fuzzy matching is useful for ordinary recall, but grows sharply with
|
|
594
|
+
// corpus size. The fast profile deliberately uses exact lexical matching.
|
|
595
|
+
...(hasQuery && quality !== 'fast' ? { tolerance: originalQuery!.length > 6 ? 2 : 1 } : {}),
|
|
576
596
|
};
|
|
577
597
|
|
|
578
598
|
// If embedding provider is available and query tier warrants it, use hybrid search
|
|
579
599
|
// Fast-tier queries skip embedding entirely (fulltext is sufficient)
|
|
580
600
|
let queryVector: number[] | null = null;
|
|
581
|
-
if (embeddingEnabled && hasQuery && tier !== 'fast') {
|
|
601
|
+
if (quality !== 'fast' && embeddingEnabled && hasQuery && tier !== 'fast') {
|
|
582
602
|
try {
|
|
583
603
|
const provider = await getEmbeddingProvider();
|
|
584
604
|
if (provider) {
|
|
@@ -594,11 +614,11 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
594
614
|
} else {
|
|
595
615
|
// Embedding timeout: 15 seconds
|
|
596
616
|
const EMBEDDING_TIMEOUT_MS = 15000;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
617
|
+
queryVector = await withTimeout(
|
|
618
|
+
provider.embed(expandedEmbeddingQuery!),
|
|
619
|
+
EMBEDDING_TIMEOUT_MS,
|
|
620
|
+
'Embedding',
|
|
600
621
|
);
|
|
601
|
-
queryVector = await Promise.race([embedPromise, timeoutPromise]);
|
|
602
622
|
mark('embedding');
|
|
603
623
|
// Detect CJK-heavy queries: BM25 can't tokenize Chinese/Japanese/Korean well
|
|
604
624
|
const cjkRatio = (originalQuery!.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery!.length;
|
|
@@ -946,9 +966,10 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
946
966
|
|
|
947
967
|
// ── LLM Reranking (heavy-tier only) ────────────────────────────
|
|
948
968
|
// Only triggered for heavy-tier queries with ambiguous top results.
|
|
949
|
-
//
|
|
969
|
+
// Non-thorough profiles, fast, and standard tiers skip entirely.
|
|
950
970
|
// Ambiguity check: top-2 scores within 30% → results are uncertain.
|
|
951
|
-
const shouldRerank =
|
|
971
|
+
const shouldRerank = quality === 'thorough'
|
|
972
|
+
&& tier === 'heavy'
|
|
952
973
|
&& hasQuery
|
|
953
974
|
&& intermediate.length > 2
|
|
954
975
|
&& (() => {
|
|
@@ -979,11 +1000,11 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
979
1000
|
// LLM rerank timeout: configurable via MEMORIX_RERANK_TIMEOUT_MS, default 5s
|
|
980
1001
|
const _parsedRerank = parseInt(process.env.MEMORIX_RERANK_TIMEOUT_MS || '', 10);
|
|
981
1002
|
const RERANK_TIMEOUT_MS = Number.isFinite(_parsedRerank) && _parsedRerank > 0 ? _parsedRerank : 5000;
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
1003
|
+
const { reranked, usedLLM } = await withTimeout(
|
|
1004
|
+
rerankResults(originalQuery!, candidates),
|
|
1005
|
+
RERANK_TIMEOUT_MS,
|
|
1006
|
+
'LLM rerank',
|
|
985
1007
|
);
|
|
986
|
-
const { reranked, usedLLM } = await Promise.race([rerankPromise, timeoutPromise]);
|
|
987
1008
|
mark(`rerank(usedLLM=${usedLLM})`);
|
|
988
1009
|
|
|
989
1010
|
if (usedLLM) {
|
package/src/store/sqlite-db.ts
CHANGED
|
@@ -17,7 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import { createDatabase, loadSqlite } from './bun-sqlite-compat.js';
|
|
19
19
|
|
|
20
|
-
// Dynamic require for SQLite (better-sqlite3 or bun:sqlite)
|
|
20
|
+
// Dynamic require for SQLite (better-sqlite3, node:sqlite, or bun:sqlite)
|
|
21
21
|
let BetterSqlite3: any;
|
|
22
22
|
|
|
23
23
|
export function loadBetterSqlite3(): any {
|
|
@@ -26,7 +26,7 @@ export function loadBetterSqlite3(): any {
|
|
|
26
26
|
BetterSqlite3 = loadSqlite();
|
|
27
27
|
return BetterSqlite3;
|
|
28
28
|
} catch {
|
|
29
|
-
throw new Error('[memorix] SQLite is not available (
|
|
29
|
+
throw new Error('[memorix] SQLite is not available (better-sqlite3, node:sqlite, and bun:sqlite all failed)');
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -688,7 +688,7 @@ function applySchemaMigrations(db: any): void {
|
|
|
688
688
|
const _dbCache = new Map<string, any>();
|
|
689
689
|
|
|
690
690
|
/**
|
|
691
|
-
* Get or create a shared
|
|
691
|
+
* Get or create a shared SQLite database handle for the given data directory.
|
|
692
692
|
*
|
|
693
693
|
* The handle is cached per normalized dataDir path. All stores (observations,
|
|
694
694
|
* mini-skills, sessions) share the same connection and the same DB file.
|
package/src/timeout.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run an operation with a bounded wait without leaving its watchdog alive.
|
|
3
|
+
*
|
|
4
|
+
* The underlying operation is not force-cancelled because many existing
|
|
5
|
+
* callers do not expose an AbortSignal. Its settlement is still observed so a
|
|
6
|
+
* late rejection is handled, while the caller receives the timeout promptly.
|
|
7
|
+
*/
|
|
8
|
+
export function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
9
|
+
return new Promise<T>((resolve, reject) => {
|
|
10
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
11
|
+
|
|
12
|
+
promise.then(
|
|
13
|
+
(value) => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
resolve(value);
|
|
16
|
+
},
|
|
17
|
+
(error) => {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
reject(error);
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -239,6 +239,9 @@ export interface TimelineContext {
|
|
|
239
239
|
after: IndexEntry[];
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
/** Retrieval work allowed for a search request. */
|
|
243
|
+
export type RetrievalQuality = 'fast' | 'balanced' | 'thorough';
|
|
244
|
+
|
|
242
245
|
/** Search options for the compact engine */
|
|
243
246
|
export interface SearchOptions {
|
|
244
247
|
query: string;
|
|
@@ -257,6 +260,11 @@ export interface SearchOptions {
|
|
|
257
260
|
trackAccess?: boolean;
|
|
258
261
|
/** Internal reader context for agent-facing visibility filtering. */
|
|
259
262
|
reader?: ObservationReader;
|
|
263
|
+
/**
|
|
264
|
+
* `fast` stays fully local, `balanced` (default) permits embeddings, and
|
|
265
|
+
* `thorough` explicitly permits optional LLM query rewrite and reranking.
|
|
266
|
+
*/
|
|
267
|
+
quality?: RetrievalQuality;
|
|
260
268
|
}
|
|
261
269
|
|
|
262
270
|
/** Topic key family heuristics for suggesting stable topic keys */
|