ghagga-core 3.0.0 → 3.2.0
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/dist/agents/audit.js +1 -1
- package/dist/agents/audit.js.map +1 -1
- package/dist/agents/diagnostic.d.ts.map +1 -1
- package/dist/agents/diagnostic.js +1 -1
- package/dist/agents/diagnostic.js.map +1 -1
- package/dist/agents/fan-out-lenses.js +1 -1
- package/dist/agents/fan-out-lenses.js.map +1 -1
- package/dist/agents/prompts.js +1 -1
- package/dist/agents/prompts.js.map +1 -1
- package/dist/agents/simple.d.ts.map +1 -1
- package/dist/agents/simple.js +1 -1
- package/dist/agents/simple.js.map +1 -1
- package/dist/embed.d.ts +140 -0
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.js +288 -0
- package/dist/embed.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/memory/backfill.d.ts +58 -0
- package/dist/memory/backfill.d.ts.map +1 -0
- package/dist/memory/backfill.js +85 -0
- package/dist/memory/backfill.js.map +1 -0
- package/dist/memory/sqlite.d.ts +78 -12
- package/dist/memory/sqlite.d.ts.map +1 -1
- package/dist/memory/sqlite.js +265 -85
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/pipeline/gather-context.d.ts.map +1 -1
- package/dist/pipeline/gather-context.js +4 -2
- package/dist/pipeline/gather-context.js.map +1 -1
- package/dist/pipeline/gather-safe.d.ts +24 -4
- package/dist/pipeline/gather-safe.d.ts.map +1 -1
- package/dist/pipeline/gather-safe.js +42 -6
- package/dist/pipeline/gather-safe.js.map +1 -1
- package/dist/pipeline/prepare.d.ts.map +1 -1
- package/dist/pipeline/prepare.js +26 -0
- package/dist/pipeline/prepare.js.map +1 -1
- package/dist/pipeline/providers.d.ts.map +1 -1
- package/dist/pipeline/providers.js +1 -0
- package/dist/pipeline/providers.js.map +1 -1
- package/dist/pipeline/state.d.ts +3 -1
- package/dist/pipeline/state.d.ts.map +1 -1
- package/dist/providers/cli-bridge.d.ts +5 -0
- package/dist/providers/cli-bridge.d.ts.map +1 -1
- package/dist/providers/cli-bridge.js +65 -5
- package/dist/providers/cli-bridge.js.map +1 -1
- package/dist/providers/gateway-discovery.d.ts +60 -0
- package/dist/providers/gateway-discovery.d.ts.map +1 -0
- package/dist/providers/gateway-discovery.js +118 -0
- package/dist/providers/gateway-discovery.js.map +1 -0
- package/dist/providers/gateway.d.ts +95 -0
- package/dist/providers/gateway.d.ts.map +1 -1
- package/dist/providers/gateway.js +391 -9
- package/dist/providers/gateway.js.map +1 -1
- package/dist/providers/generate-fn.d.ts +1 -0
- package/dist/providers/generate-fn.d.ts.map +1 -1
- package/dist/providers/generate-fn.js.map +1 -1
- package/dist/tools/cpd.js +2 -2
- package/dist/tools/cpd.js.map +1 -1
- package/dist/tools/execution.d.ts +3 -1
- package/dist/tools/execution.d.ts.map +1 -1
- package/dist/tools/execution.js +10 -2
- package/dist/tools/execution.js.map +1 -1
- package/dist/tools/gitleaks-config.toml +23 -0
- package/dist/tools/runner.d.ts.map +1 -1
- package/dist/tools/runner.js +2 -1
- package/dist/tools/runner.js.map +1 -1
- package/dist/tools/semgrep.js +2 -2
- package/dist/tools/semgrep.js.map +1 -1
- package/dist/types.d.ts +47 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/concurrency.d.ts.map +1 -1
- package/dist/utils/concurrency.js +37 -1
- package/dist/utils/concurrency.js.map +1 -1
- package/dist/utils/llm-timeout.d.ts +1 -1
- package/dist/utils/llm-timeout.d.ts.map +1 -1
- package/package.json +9 -9
package/dist/memory/sqlite.d.ts
CHANGED
|
@@ -20,6 +20,23 @@ export interface SqliteMemoryStorageOptions {
|
|
|
20
20
|
* is used for search. When undefined, falls back to keyword-only search.
|
|
21
21
|
*/
|
|
22
22
|
embeddingProvider?: EmbeddingProvider;
|
|
23
|
+
/**
|
|
24
|
+
* Identifier for the active embedding model (e.g. "text-embedding-3-small").
|
|
25
|
+
* Persisted per-row alongside the vector and compared on read (design D3).
|
|
26
|
+
*
|
|
27
|
+
* NOTE: `EmbeddingProvider` (packages/core/src/embed.ts) does not currently
|
|
28
|
+
* expose a model name, so this is supplied by the caller out-of-band. When
|
|
29
|
+
* omitted, the read guard only enforces dimension compatibility (never
|
|
30
|
+
* throws either way) — the model-mismatch check is a no-op until callers
|
|
31
|
+
* start passing it (wired in PR5).
|
|
32
|
+
*/
|
|
33
|
+
embeddingModel?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Bounded cosine candidate set size (design D4). Defaults to 200.
|
|
36
|
+
* Callers should pass `resolveEmbeddingConfig(env).candidateK` (PR1) rather
|
|
37
|
+
* than relying on this default once wired (PR5).
|
|
38
|
+
*/
|
|
39
|
+
embeddingCandidateK?: number;
|
|
23
40
|
}
|
|
24
41
|
export declare class SqliteMemoryStorage implements MemoryStorage {
|
|
25
42
|
private db;
|
|
@@ -27,6 +44,8 @@ export declare class SqliteMemoryStorage implements MemoryStorage {
|
|
|
27
44
|
private dedupWindowMinutes;
|
|
28
45
|
private decayConfig;
|
|
29
46
|
private embeddingProvider;
|
|
47
|
+
private embeddingModel;
|
|
48
|
+
private embeddingCandidateK;
|
|
30
49
|
private pageIndex;
|
|
31
50
|
private constructor();
|
|
32
51
|
/**
|
|
@@ -50,16 +69,26 @@ export declare class SqliteMemoryStorage implements MemoryStorage {
|
|
|
50
69
|
type?: string;
|
|
51
70
|
}): Promise<MemoryObservationRow[]>;
|
|
52
71
|
/**
|
|
53
|
-
* Hybrid
|
|
54
|
-
*
|
|
72
|
+
* Hybrid keyword + semantic search: unions a bounded cosine candidate set
|
|
73
|
+
* with the keyword (FTS5) candidate set before scoring (spec R5.7-R5.10,
|
|
74
|
+
* design D4/D5). Only called when embeddingProvider is set.
|
|
55
75
|
*
|
|
56
76
|
* Strategy:
|
|
57
|
-
* 1. Run FTS5 to get keyword candidates (up to limit * 5
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
77
|
+
* 1. Run FTS5 to get keyword candidates (up to limit * 5), preserving
|
|
78
|
+
* native bm25() order (best match first).
|
|
79
|
+
* 2. Run a bounded, project(+type)-scoped cosine candidate query
|
|
80
|
+
* (embedding IS NOT NULL, ORDER BY last_accessed_at DESC LIMIT K).
|
|
81
|
+
* 3. Compute cosine similarity in JS for candidates that pass the
|
|
82
|
+
* dimension/model read guard (mismatches are excluded, never thrown).
|
|
83
|
+
* 4. Union both sets by id, dedup: an overlapping candidate keeps its
|
|
84
|
+
* real keyword score; a cosine-only candidate gets keyword-score 0.
|
|
85
|
+
* 5. Keyword score uses the unified positional-rank convention
|
|
86
|
+
* `1 - i/(n-1)` over the keyword-ordered list (design D5) — shared
|
|
87
|
+
* with the PostgreSQL backend so both engines score identically
|
|
88
|
+
* post-union.
|
|
89
|
+
* 6. Combine: finalScore = 0.7 * cosineSim + 0.3 * keywordScore.
|
|
90
|
+
* 7. Apply the decay filter, sort descending, cap at limit, update
|
|
91
|
+
* last_accessed_at only for the rows actually returned (R5.10).
|
|
63
92
|
*/
|
|
64
93
|
private _hybridSearch;
|
|
65
94
|
saveObservation(data: {
|
|
@@ -73,11 +102,14 @@ export declare class SqliteMemoryStorage implements MemoryStorage {
|
|
|
73
102
|
severity?: string;
|
|
74
103
|
}): Promise<MemoryObservationRow>;
|
|
75
104
|
/**
|
|
76
|
-
* Compute embedding buffer for storage
|
|
77
|
-
*
|
|
78
|
-
*
|
|
105
|
+
* Compute the embedding buffer plus its provider metadata for storage
|
|
106
|
+
* (design D3 — per-row `embedding_model`/`embedding_dim`, task 3.6).
|
|
107
|
+
* Returns all-null when no embeddingProvider is configured (no-op) or when
|
|
108
|
+
* embedding generation fails — a failed embed is non-fatal: the row is
|
|
109
|
+
* still saved with a NULL embedding rather than throwing (spec: Graceful
|
|
110
|
+
* Degradation on Provider/API Failure).
|
|
79
111
|
*/
|
|
80
|
-
private
|
|
112
|
+
private _computeEmbeddingMeta;
|
|
81
113
|
createSession(data: {
|
|
82
114
|
project: string;
|
|
83
115
|
prNumber?: number;
|
|
@@ -125,6 +157,40 @@ export declare class SqliteMemoryStorage implements MemoryStorage {
|
|
|
125
157
|
* Returns true if a row was deleted, false otherwise.
|
|
126
158
|
*/
|
|
127
159
|
deleteNegativeExample(findingHash: string): boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Export the in-memory WASM database and write it to disk WITHOUT closing
|
|
162
|
+
* the underlying handle — shared by close() and flush() (design D6).
|
|
163
|
+
*/
|
|
164
|
+
private _writeToDisk;
|
|
165
|
+
/**
|
|
166
|
+
* Durability hook for the backfill script (design D6, task 6.1): flushes
|
|
167
|
+
* the in-memory WASM database to disk after each batch so a mid-run crash
|
|
168
|
+
* loses at most the in-flight batch, not prior progress. Unlike close(),
|
|
169
|
+
* the database handle stays open and usable.
|
|
170
|
+
*/
|
|
171
|
+
flush(): Promise<void>;
|
|
128
172
|
close(): Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* List up to `limit` observations needing an embedding for the active
|
|
175
|
+
* provider/model, ordered by id ascending starting after `afterId`.
|
|
176
|
+
* Always matches NULL-embedding rows; with `includeMismatched` also
|
|
177
|
+
* matches rows whose stored `embedding_model`/`embedding_dim` disagree
|
|
178
|
+
* with the active provider (backfill `--re-embed`).
|
|
179
|
+
*/
|
|
180
|
+
listObservationsNeedingEmbedding(options: {
|
|
181
|
+
afterId: number;
|
|
182
|
+
limit: number;
|
|
183
|
+
activeModel: string;
|
|
184
|
+
activeDim: number;
|
|
185
|
+
includeMismatched: boolean;
|
|
186
|
+
}): Promise<{
|
|
187
|
+
id: number;
|
|
188
|
+
text: string;
|
|
189
|
+
}[]>;
|
|
190
|
+
/**
|
|
191
|
+
* Persist a backfilled embedding for a single observation (design D6).
|
|
192
|
+
* Metadata-only write — content/updated_at/last_accessed_at are untouched.
|
|
193
|
+
*/
|
|
194
|
+
updateObservationEmbedding(id: number, embedding: number[], model: string, dim: number): Promise<void>;
|
|
129
195
|
}
|
|
130
196
|
//# sourceMappingURL=sqlite.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/memory/sqlite.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,OAAO,EAGL,KAAK,iBAAiB,EAEvB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACvB,uBAAuB,EACvB,oBAAoB,EACpB,WAAW,EACX,aAAa,EACb,eAAe,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAwB,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAMrE,OAAO,EAAE,KAAK,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/memory/sqlite.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,OAAO,EAGL,KAAK,iBAAiB,EAEvB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACvB,uBAAuB,EACvB,oBAAoB,EACpB,WAAW,EACX,aAAa,EACb,eAAe,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAwB,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAMrE,OAAO,EAAE,KAAK,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AA6G1F,MAAM,WAAW,0BAA0B;IACzC,4HAA4H;IAC5H,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B,gFAAgF;IAChF,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAEtC;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAqBD,qBAAa,mBAAoB,YAAW,aAAa;IASrD,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,QAAQ;IATlB,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,iBAAiB,CAAgC;IACzD,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,SAAS,CAA0B;IAE3C,OAAO;IAaP;;;OAGG;IACH,WAAW,IAAI,kBAAkB;IAIjC;;;;OAIG;IACH,YAAY,IAAI,uBAAuB;IAIvC;;;OAGG;WACU,MAAM,CACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,0BAA0B,GACnC,OAAO,CAAC,mBAAmB,CAAC;IA0CzB,kBAAkB,CACtB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO,GAC9C,OAAO,CAAC,oBAAoB,EAAE,CAAC;IA0FlC;;;;;;;;;;;;;;;;;;;;;OAqBG;YACW,aAAa;IAyOrB,eAAe,CAAC,IAAI,EAAE;QAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAqJjC;;;;;;;OAOG;YACW,qBAAqB;IAsB7B,aAAa,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAapF,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAanE,OAAO,CAAC,WAAW;IAgBb,gBAAgB,CACpB,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,uBAAuB,EAAE,CAAC;IAmC/B,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC;IAmBnE,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK/C,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC;IAmChC,iBAAiB,CAAC,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAW5E;;;OAGG;IACH,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI;IAwBtD;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAuB/C;;;OAGG;IACH,mBAAmB,CAAC,OAAO,EAAE,eAAe,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAe3E;;;OAGG;IACH,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE;IAyB/D;;OAEG;IACH,sBAAsB,IAAI,CAAC,eAAe,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE;IAuBrE;;;OAGG;IACH,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAKnD;;;OAGG;IACH,OAAO,CAAC,YAAY;IAUpB;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B;;;;;;OAMG;IACG,gCAAgC,CAAC,OAAO,EAAE;QAC9C,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,iBAAiB,EAAE,OAAO,CAAC;KAC5B,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IA0C3C;;;OAGG;IACG,0BAA0B,CAC9B,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EAAE,EACnB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC;CAMjB"}
|
package/dist/memory/sqlite.js
CHANGED
|
@@ -45,7 +45,9 @@ const SCHEMA_SQL = `
|
|
|
45
45
|
revision_count INTEGER NOT NULL DEFAULT 1,
|
|
46
46
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
47
47
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
48
|
-
last_accessed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
48
|
+
last_accessed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
49
|
+
embedding_model TEXT,
|
|
50
|
+
embedding_dim INTEGER
|
|
49
51
|
);
|
|
50
52
|
|
|
51
53
|
CREATE INDEX IF NOT EXISTS idx_obs_project ON memory_observations(project);
|
|
@@ -124,6 +126,8 @@ const SCHEMA_SQL = `
|
|
|
124
126
|
CREATE INDEX IF NOT EXISTS idx_neg_context_hash ON negative_examples(context_hash);
|
|
125
127
|
`;
|
|
126
128
|
const DEFAULT_DEDUP_WINDOW_MINUTES = 15;
|
|
129
|
+
/** Default bounded cosine candidate set size (design D4) when not configured. */
|
|
130
|
+
const DEFAULT_EMBEDDING_CANDIDATE_K = 200;
|
|
127
131
|
/**
|
|
128
132
|
* Runs an ADD COLUMN migration idempotently.
|
|
129
133
|
* Swallows 'duplicate column name' (migration already ran) and rethrows
|
|
@@ -146,6 +150,8 @@ export class SqliteMemoryStorage {
|
|
|
146
150
|
dedupWindowMinutes;
|
|
147
151
|
decayConfig;
|
|
148
152
|
embeddingProvider;
|
|
153
|
+
embeddingModel;
|
|
154
|
+
embeddingCandidateK;
|
|
149
155
|
pageIndex;
|
|
150
156
|
constructor(db, filePath, options = {}) {
|
|
151
157
|
this.db = db;
|
|
@@ -153,6 +159,8 @@ export class SqliteMemoryStorage {
|
|
|
153
159
|
this.dedupWindowMinutes = options.dedupWindowMinutes ?? DEFAULT_DEDUP_WINDOW_MINUTES;
|
|
154
160
|
this.decayConfig = options.decayConfig ?? DEFAULT_DECAY_CONFIG;
|
|
155
161
|
this.embeddingProvider = options.embeddingProvider;
|
|
162
|
+
this.embeddingModel = options.embeddingModel;
|
|
163
|
+
this.embeddingCandidateK = options.embeddingCandidateK ?? DEFAULT_EMBEDDING_CANDIDATE_K;
|
|
156
164
|
this.pageIndex = new ProjectPageIndexService(this.db);
|
|
157
165
|
}
|
|
158
166
|
/**
|
|
@@ -199,11 +207,22 @@ export class SqliteMemoryStorage {
|
|
|
199
207
|
db.run('CREATE INDEX IF NOT EXISTS idx_obs_last_accessed ON memory_observations(last_accessed_at)');
|
|
200
208
|
// Migration: add embedding column for hybrid search (NULL when no embedding provider)
|
|
201
209
|
runIdempotentAlter(db, 'ALTER TABLE memory_observations ADD COLUMN embedding BLOB');
|
|
210
|
+
// Migration: add per-row embedding provider metadata (NULL when embedding is NULL).
|
|
211
|
+
// Used by the read guard to detect provider/dimension mismatches (design D3).
|
|
212
|
+
runIdempotentAlter(db, 'ALTER TABLE memory_observations ADD COLUMN embedding_model TEXT');
|
|
213
|
+
runIdempotentAlter(db, 'ALTER TABLE memory_observations ADD COLUMN embedding_dim INTEGER');
|
|
202
214
|
return new SqliteMemoryStorage(db, filePath, options);
|
|
203
215
|
}
|
|
204
216
|
async searchObservations(project, query, options = {}) {
|
|
205
217
|
const { limit = 10, type } = options;
|
|
206
|
-
// ── Hybrid search (BM25 + semantic)
|
|
218
|
+
// ── Hybrid search (BM25 + semantic RE-RANKING) ───────────────
|
|
219
|
+
// NOTE (MEM-HYBRID-006): this is semantic *re-ranking* of a keyword
|
|
220
|
+
// candidate set, NOT pure semantic retrieval. Even with an embedding
|
|
221
|
+
// provider, candidates are first gated by the FTS5 MATCH below, so an
|
|
222
|
+
// observation with no lexical overlap with the query is never a candidate
|
|
223
|
+
// and cannot be surfaced by embedding similarity alone (e.g. "secret
|
|
224
|
+
// leakage" will not recall a "credential exposure" note). Closing this
|
|
225
|
+
// would require unioning an ANN/vector candidate set with the keyword set.
|
|
207
226
|
if (this.embeddingProvider) {
|
|
208
227
|
return this._hybridSearch(project, query, { limit, type });
|
|
209
228
|
}
|
|
@@ -268,19 +287,32 @@ export class SqliteMemoryStorage {
|
|
|
268
287
|
return rows;
|
|
269
288
|
}
|
|
270
289
|
/**
|
|
271
|
-
* Hybrid
|
|
272
|
-
*
|
|
290
|
+
* Hybrid keyword + semantic search: unions a bounded cosine candidate set
|
|
291
|
+
* with the keyword (FTS5) candidate set before scoring (spec R5.7-R5.10,
|
|
292
|
+
* design D4/D5). Only called when embeddingProvider is set.
|
|
273
293
|
*
|
|
274
294
|
* Strategy:
|
|
275
|
-
* 1. Run FTS5 to get keyword candidates (up to limit * 5
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
295
|
+
* 1. Run FTS5 to get keyword candidates (up to limit * 5), preserving
|
|
296
|
+
* native bm25() order (best match first).
|
|
297
|
+
* 2. Run a bounded, project(+type)-scoped cosine candidate query
|
|
298
|
+
* (embedding IS NOT NULL, ORDER BY last_accessed_at DESC LIMIT K).
|
|
299
|
+
* 3. Compute cosine similarity in JS for candidates that pass the
|
|
300
|
+
* dimension/model read guard (mismatches are excluded, never thrown).
|
|
301
|
+
* 4. Union both sets by id, dedup: an overlapping candidate keeps its
|
|
302
|
+
* real keyword score; a cosine-only candidate gets keyword-score 0.
|
|
303
|
+
* 5. Keyword score uses the unified positional-rank convention
|
|
304
|
+
* `1 - i/(n-1)` over the keyword-ordered list (design D5) — shared
|
|
305
|
+
* with the PostgreSQL backend so both engines score identically
|
|
306
|
+
* post-union.
|
|
307
|
+
* 6. Combine: finalScore = 0.7 * cosineSim + 0.3 * keywordScore.
|
|
308
|
+
* 7. Apply the decay filter, sort descending, cap at limit, update
|
|
309
|
+
* last_accessed_at only for the rows actually returned (R5.10).
|
|
281
310
|
*/
|
|
282
311
|
async _hybridSearch(project, query, options) {
|
|
283
312
|
const { limit, type } = options;
|
|
313
|
+
const provider = this.embeddingProvider;
|
|
314
|
+
if (!provider)
|
|
315
|
+
return [];
|
|
284
316
|
const ftsQuery = query
|
|
285
317
|
.trim()
|
|
286
318
|
.split(/\s+/)
|
|
@@ -291,80 +323,149 @@ export class SqliteMemoryStorage {
|
|
|
291
323
|
return [];
|
|
292
324
|
// Fetch a larger candidate set for re-ranking
|
|
293
325
|
const fetchLimit = limit * 5;
|
|
294
|
-
|
|
326
|
+
const mapCandidateRow = (row) => ({
|
|
327
|
+
id: row.id,
|
|
328
|
+
type: row.type,
|
|
329
|
+
title: row.title,
|
|
330
|
+
content: row.content,
|
|
331
|
+
filePaths: row.file_paths ? JSON.parse(row.file_paths) : null,
|
|
332
|
+
severity: row.severity ?? null,
|
|
333
|
+
lastAccessed: row.last_accessed_at ? new Date(row.last_accessed_at) : new Date(),
|
|
334
|
+
embedding: row.embedding ? row.embedding : null,
|
|
335
|
+
embeddingModel: row.embedding_model ?? null,
|
|
336
|
+
embeddingDim: row.embedding_dim ?? null,
|
|
337
|
+
});
|
|
338
|
+
// ── Step 1: keyword candidates (FTS5, unchanged gate) ────────────
|
|
339
|
+
let keywordSql = `
|
|
295
340
|
SELECT o.id, o.type, o.title, o.content, o.file_paths, o.severity,
|
|
296
|
-
o.last_accessed_at, o.embedding,
|
|
297
|
-
bm25(memory_observations_fts) AS bm25_score
|
|
341
|
+
o.last_accessed_at, o.embedding, o.embedding_model, o.embedding_dim
|
|
298
342
|
FROM memory_observations o
|
|
299
343
|
JOIN memory_observations_fts fts ON fts.rowid = o.id
|
|
300
344
|
WHERE memory_observations_fts MATCH ?
|
|
301
345
|
AND o.project = ?
|
|
302
346
|
`;
|
|
303
|
-
const
|
|
347
|
+
const keywordParams = [ftsQuery, project];
|
|
304
348
|
if (type) {
|
|
305
|
-
|
|
306
|
-
|
|
349
|
+
keywordSql += ' AND o.type = ?';
|
|
350
|
+
keywordParams.push(type);
|
|
307
351
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
while (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
352
|
+
keywordSql += ' ORDER BY bm25(memory_observations_fts) LIMIT ?';
|
|
353
|
+
keywordParams.push(fetchLimit);
|
|
354
|
+
const keywordStmt = this.db.prepare(keywordSql);
|
|
355
|
+
keywordStmt.bind(keywordParams);
|
|
356
|
+
const keywordCandidates = [];
|
|
357
|
+
while (keywordStmt.step()) {
|
|
358
|
+
keywordCandidates.push(mapCandidateRow(keywordStmt.getAsObject()));
|
|
359
|
+
}
|
|
360
|
+
keywordStmt.free();
|
|
361
|
+
// ── Step 2: bounded cosine candidates (spec R5.7, design D4) ─────
|
|
362
|
+
let cosineSql = `
|
|
363
|
+
SELECT o.id, o.type, o.title, o.content, o.file_paths, o.severity,
|
|
364
|
+
o.last_accessed_at, o.embedding, o.embedding_model, o.embedding_dim
|
|
365
|
+
FROM memory_observations o
|
|
366
|
+
WHERE o.project = ?
|
|
367
|
+
AND o.embedding IS NOT NULL
|
|
368
|
+
`;
|
|
369
|
+
const cosineParams = [project];
|
|
370
|
+
if (type) {
|
|
371
|
+
cosineSql += ' AND o.type = ?';
|
|
372
|
+
cosineParams.push(type);
|
|
373
|
+
}
|
|
374
|
+
cosineSql += ' ORDER BY o.last_accessed_at DESC LIMIT ?';
|
|
375
|
+
cosineParams.push(this.embeddingCandidateK);
|
|
376
|
+
const cosineStmt = this.db.prepare(cosineSql);
|
|
377
|
+
cosineStmt.bind(cosineParams);
|
|
378
|
+
const cosineBoundedCandidates = [];
|
|
379
|
+
while (cosineStmt.step()) {
|
|
380
|
+
cosineBoundedCandidates.push(mapCandidateRow(cosineStmt.getAsObject()));
|
|
381
|
+
}
|
|
382
|
+
cosineStmt.free();
|
|
383
|
+
if (keywordCandidates.length === 0 && cosineBoundedCandidates.length === 0)
|
|
384
|
+
return [];
|
|
385
|
+
// Compute query embedding once. A query-time embed failure is non-fatal
|
|
386
|
+
// (spec: Graceful Degradation on Provider/API Failure): warn once and fall
|
|
387
|
+
// back to keyword-only for this query. `queryVec === null` makes every
|
|
388
|
+
// cosine 0 and empties the cosine-only candidate set, so the union/decay/cap
|
|
389
|
+
// pipeline below runs over the keyword candidates alone — unchanged.
|
|
390
|
+
let queryVec;
|
|
391
|
+
try {
|
|
392
|
+
queryVec = await provider.embed(query);
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
console.warn(`[ghagga] query embedding failed — degrading to keyword-only for this search: ${error instanceof Error ? error.message : String(error)}`);
|
|
396
|
+
queryVec = null;
|
|
397
|
+
}
|
|
398
|
+
// Dimension/model read guard (design D3): a row whose stored dimension or
|
|
399
|
+
// model doesn't match the active provider is excluded from the cosine set
|
|
400
|
+
// (task 3.2) — mirrors the pre-existing "no embedding → cosine 0" rule.
|
|
401
|
+
// `this.embeddingModel` may be undefined (EmbeddingProvider does not yet
|
|
402
|
+
// expose a model name) — when undefined, only dimension is enforced.
|
|
403
|
+
const isEmbeddingUsable = (row) => {
|
|
404
|
+
if (!row.embedding)
|
|
405
|
+
return false;
|
|
406
|
+
if (row.embeddingDim !== null && row.embeddingDim !== provider.dimension)
|
|
407
|
+
return false;
|
|
408
|
+
if (this.embeddingModel !== undefined && row.embeddingModel !== this.embeddingModel) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
return true;
|
|
412
|
+
};
|
|
413
|
+
const computeCosine = (row) => {
|
|
414
|
+
if (queryVec === null || !row.embedding || !isEmbeddingUsable(row))
|
|
415
|
+
return 0;
|
|
416
|
+
try {
|
|
417
|
+
const storedVec = deserializeEmbedding(row.embedding);
|
|
418
|
+
if (storedVec.length !== queryVec.length)
|
|
419
|
+
return 0;
|
|
420
|
+
return cosineSimilarity(queryVec, storedVec);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// Malformed embedding — treat as 0
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
// ── Step 3: unified positional-rank keyword score (design D5) ────
|
|
428
|
+
// keywordCandidates is already ordered by bm25() ASC (best match first).
|
|
429
|
+
const n = keywordCandidates.length;
|
|
430
|
+
const keywordScoreById = new Map();
|
|
431
|
+
keywordCandidates.forEach((candidate, i) => {
|
|
432
|
+
keywordScoreById.set(candidate.id, n > 1 ? 1 - i / (n - 1) : 1);
|
|
433
|
+
});
|
|
434
|
+
// ── Step 4: cosine similarity, top limit*5 of the GUARDED set ────
|
|
435
|
+
// When the query embed failed (queryVec === null), there are no cosine
|
|
436
|
+
// candidates — the union below sees keyword candidates only.
|
|
437
|
+
const cosineTop = queryVec === null
|
|
438
|
+
? []
|
|
439
|
+
: cosineBoundedCandidates
|
|
440
|
+
.filter(isEmbeddingUsable)
|
|
441
|
+
.map((candidate) => ({ candidate, cosineSim: computeCosine(candidate) }))
|
|
442
|
+
.sort((a, b) => b.cosineSim - a.cosineSim)
|
|
443
|
+
.slice(0, fetchLimit);
|
|
444
|
+
const merged = new Map();
|
|
445
|
+
for (const candidate of keywordCandidates) {
|
|
446
|
+
merged.set(candidate.id, {
|
|
447
|
+
candidate,
|
|
448
|
+
cosineSim: computeCosine(candidate),
|
|
449
|
+
keywordScore: keywordScoreById.get(candidate.id) ?? 0,
|
|
325
450
|
});
|
|
326
451
|
}
|
|
327
|
-
|
|
328
|
-
|
|
452
|
+
for (const { candidate, cosineSim } of cosineTop) {
|
|
453
|
+
if (merged.has(candidate.id))
|
|
454
|
+
continue; // keep the real keyword score (R5.8)
|
|
455
|
+
merged.set(candidate.id, { candidate, cosineSim, keywordScore: 0 }); // vector-only (R5.9)
|
|
456
|
+
}
|
|
457
|
+
if (merged.size === 0)
|
|
329
458
|
return [];
|
|
330
|
-
//
|
|
331
|
-
// biome-ignore lint/style/noNonNullAssertion: embeddingProvider is checked by the caller (_hybridSearch is only called when it's set)
|
|
332
|
-
const queryVec = await this.embeddingProvider.embed(query);
|
|
333
|
-
// BM25 scores from FTS5 are negative (lower = better match).
|
|
334
|
-
// Normalize to [0, 1]: higher is better.
|
|
335
|
-
const bm25Scores = candidates.map((c) => c.bm25Score);
|
|
336
|
-
const minBm25 = Math.min(...bm25Scores);
|
|
337
|
-
const maxBm25 = Math.max(...bm25Scores);
|
|
338
|
-
const bm25Range = maxBm25 - minBm25;
|
|
459
|
+
// ── Step 6: score, decay filter, sort, cap (spec R5.5/R5.8) ──────
|
|
339
460
|
const now = new Date();
|
|
340
461
|
const scored = [];
|
|
341
|
-
for (const candidate of
|
|
462
|
+
for (const { candidate, cosineSim, keywordScore } of merged.values()) {
|
|
342
463
|
const strength = computeStrength(candidate.lastAccessed, now, this.decayConfig);
|
|
343
464
|
if (strength < this.decayConfig.minStrength)
|
|
344
465
|
continue;
|
|
345
|
-
|
|
346
|
-
// so the best candidate (minBm25) must map to 1 and the worst (maxBm25) to 0.
|
|
347
|
-
const normalizedBm25 = bm25Range === 0 ? 1 : (maxBm25 - candidate.bm25Score) / bm25Range;
|
|
348
|
-
// Cosine similarity from stored embedding (0 if not present)
|
|
349
|
-
let cosineSim = 0;
|
|
350
|
-
if (candidate.embedding) {
|
|
351
|
-
try {
|
|
352
|
-
const storedVec = deserializeEmbedding(candidate.embedding);
|
|
353
|
-
// Dimension guard: a stored vector from a different embedding model
|
|
354
|
-
// is meaningless against this query — skip cosine (mirror queries.ts)
|
|
355
|
-
if (storedVec.length === queryVec.length) {
|
|
356
|
-
cosineSim = cosineSimilarity(queryVec, storedVec);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
// Malformed embedding — treat as 0
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
// 70/30 weighted combination
|
|
364
|
-
const finalScore = 0.7 * cosineSim + 0.3 * normalizedBm25;
|
|
466
|
+
const finalScore = 0.7 * cosineSim + 0.3 * keywordScore;
|
|
365
467
|
scored.push({ candidate, finalScore, strength });
|
|
366
468
|
}
|
|
367
|
-
// Sort by final score descending
|
|
368
469
|
scored.sort((a, b) => b.finalScore - a.finalScore);
|
|
369
470
|
const results = [];
|
|
370
471
|
const accessedIds = [];
|
|
@@ -382,7 +483,7 @@ export class SqliteMemoryStorage {
|
|
|
382
483
|
strength,
|
|
383
484
|
});
|
|
384
485
|
}
|
|
385
|
-
// Update last_accessed_at for returned
|
|
486
|
+
// Update last_accessed_at only for the final returned rows (R5.10)
|
|
386
487
|
if (accessedIds.length > 0) {
|
|
387
488
|
const placeholders = accessedIds.map(() => '?').join(',');
|
|
388
489
|
this.db.run(`UPDATE memory_observations SET last_accessed_at = datetime('now') WHERE id IN (${placeholders})`, accessedIds);
|
|
@@ -432,12 +533,12 @@ export class SqliteMemoryStorage {
|
|
|
432
533
|
if (existingByTopic.length > 0 && existingByTopic[0]?.values.length > 0) {
|
|
433
534
|
const existingId = existingByTopic[0]?.values[0]?.[0];
|
|
434
535
|
const filePathsJson = JSON.stringify(data.filePaths ?? []);
|
|
435
|
-
// Compute and store embedding if provider available
|
|
436
|
-
const
|
|
536
|
+
// Compute and store embedding + its provider metadata if provider available
|
|
537
|
+
const embeddingMeta = await this._computeEmbeddingMeta(`${data.title} ${data.content}`);
|
|
437
538
|
const updated = this.db.exec(`
|
|
438
539
|
UPDATE memory_observations
|
|
439
540
|
SET content = ?, title = ?, content_hash = ?, file_paths = ?,
|
|
440
|
-
severity = ?, embedding = ?,
|
|
541
|
+
severity = ?, embedding = ?, embedding_model = ?, embedding_dim = ?,
|
|
441
542
|
revision_count = revision_count + 1,
|
|
442
543
|
updated_at = datetime('now'),
|
|
443
544
|
last_accessed_at = datetime('now')
|
|
@@ -449,7 +550,9 @@ export class SqliteMemoryStorage {
|
|
|
449
550
|
contentHash,
|
|
450
551
|
filePathsJson,
|
|
451
552
|
data.severity ?? null,
|
|
452
|
-
|
|
553
|
+
embeddingMeta.buffer,
|
|
554
|
+
embeddingMeta.model,
|
|
555
|
+
embeddingMeta.dim,
|
|
453
556
|
existingId,
|
|
454
557
|
]);
|
|
455
558
|
const row = updated[0]?.values[0];
|
|
@@ -465,13 +568,14 @@ export class SqliteMemoryStorage {
|
|
|
465
568
|
};
|
|
466
569
|
}
|
|
467
570
|
}
|
|
468
|
-
// New observation — compute embedding if provider available
|
|
469
|
-
const
|
|
571
|
+
// New observation — compute embedding + its provider metadata if provider available
|
|
572
|
+
const embeddingMeta = await this._computeEmbeddingMeta(`${data.title} ${data.content}`);
|
|
470
573
|
const filePathsJson = JSON.stringify(data.filePaths ?? []);
|
|
471
574
|
const inserted = this.db.exec(`
|
|
472
575
|
INSERT INTO memory_observations
|
|
473
|
-
(session_id, project, type, title, content, severity, topic_key, file_paths, content_hash,
|
|
474
|
-
|
|
576
|
+
(session_id, project, type, title, content, severity, topic_key, file_paths, content_hash,
|
|
577
|
+
embedding, embedding_model, embedding_dim, last_accessed_at)
|
|
578
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
475
579
|
RETURNING id, type, title, content, file_paths, severity
|
|
476
580
|
`, [
|
|
477
581
|
data.sessionId ?? null,
|
|
@@ -483,7 +587,9 @@ export class SqliteMemoryStorage {
|
|
|
483
587
|
data.topicKey ?? null,
|
|
484
588
|
filePathsJson,
|
|
485
589
|
contentHash,
|
|
486
|
-
|
|
590
|
+
embeddingMeta.buffer,
|
|
591
|
+
embeddingMeta.model,
|
|
592
|
+
embeddingMeta.dim,
|
|
487
593
|
]);
|
|
488
594
|
const row = inserted[0]?.values[0];
|
|
489
595
|
if (!row)
|
|
@@ -501,20 +607,28 @@ export class SqliteMemoryStorage {
|
|
|
501
607
|
};
|
|
502
608
|
}
|
|
503
609
|
/**
|
|
504
|
-
* Compute embedding buffer for storage
|
|
505
|
-
*
|
|
506
|
-
*
|
|
610
|
+
* Compute the embedding buffer plus its provider metadata for storage
|
|
611
|
+
* (design D3 — per-row `embedding_model`/`embedding_dim`, task 3.6).
|
|
612
|
+
* Returns all-null when no embeddingProvider is configured (no-op) or when
|
|
613
|
+
* embedding generation fails — a failed embed is non-fatal: the row is
|
|
614
|
+
* still saved with a NULL embedding rather than throwing (spec: Graceful
|
|
615
|
+
* Degradation on Provider/API Failure).
|
|
507
616
|
*/
|
|
508
|
-
async
|
|
617
|
+
async _computeEmbeddingMeta(text) {
|
|
509
618
|
if (!this.embeddingProvider)
|
|
510
|
-
return null;
|
|
619
|
+
return { buffer: null, model: null, dim: null };
|
|
511
620
|
try {
|
|
512
621
|
const vec = await this.embeddingProvider.embed(text);
|
|
513
|
-
return
|
|
622
|
+
return {
|
|
623
|
+
buffer: serializeEmbedding(vec),
|
|
624
|
+
model: this.embeddingModel ?? null,
|
|
625
|
+
dim: this.embeddingProvider.dimension,
|
|
626
|
+
};
|
|
514
627
|
}
|
|
515
|
-
catch {
|
|
628
|
+
catch (error) {
|
|
516
629
|
// Embedding failure is non-fatal — store NULL and continue
|
|
517
|
-
|
|
630
|
+
console.warn(`[ghagga] embedding computation failed during save — persisting NULL embedding: ${error instanceof Error ? error.message : String(error)}`);
|
|
631
|
+
return { buffer: null, model: null, dim: null };
|
|
518
632
|
}
|
|
519
633
|
}
|
|
520
634
|
async createSession(data) {
|
|
@@ -744,14 +858,80 @@ export class SqliteMemoryStorage {
|
|
|
744
858
|
this.db.run('DELETE FROM negative_examples WHERE finding_hash = ?', [findingHash]);
|
|
745
859
|
return this.db.getRowsModified() > 0;
|
|
746
860
|
}
|
|
747
|
-
|
|
861
|
+
/**
|
|
862
|
+
* Export the in-memory WASM database and write it to disk WITHOUT closing
|
|
863
|
+
* the underlying handle — shared by close() and flush() (design D6).
|
|
864
|
+
*/
|
|
865
|
+
_writeToDisk() {
|
|
748
866
|
const dir = dirname(this.filePath);
|
|
749
867
|
if (!existsSync(dir)) {
|
|
750
868
|
mkdirSync(dir, { recursive: true });
|
|
751
869
|
}
|
|
752
870
|
const data = this.db.export();
|
|
753
871
|
writeFileSync(this.filePath, Buffer.from(data));
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Durability hook for the backfill script (design D6, task 6.1): flushes
|
|
875
|
+
* the in-memory WASM database to disk after each batch so a mid-run crash
|
|
876
|
+
* loses at most the in-flight batch, not prior progress. Unlike close(),
|
|
877
|
+
* the database handle stays open and usable.
|
|
878
|
+
*/
|
|
879
|
+
async flush() {
|
|
880
|
+
this._writeToDisk();
|
|
881
|
+
}
|
|
882
|
+
async close() {
|
|
883
|
+
this._writeToDisk();
|
|
754
884
|
this.db.close();
|
|
755
885
|
}
|
|
886
|
+
// ── Backfill (design D6) ────────────────────────────────────────
|
|
887
|
+
/**
|
|
888
|
+
* List up to `limit` observations needing an embedding for the active
|
|
889
|
+
* provider/model, ordered by id ascending starting after `afterId`.
|
|
890
|
+
* Always matches NULL-embedding rows; with `includeMismatched` also
|
|
891
|
+
* matches rows whose stored `embedding_model`/`embedding_dim` disagree
|
|
892
|
+
* with the active provider (backfill `--re-embed`).
|
|
893
|
+
*/
|
|
894
|
+
async listObservationsNeedingEmbedding(options) {
|
|
895
|
+
const { afterId, limit, activeModel, activeDim, includeMismatched } = options;
|
|
896
|
+
let sql = `
|
|
897
|
+
SELECT id, title, content FROM memory_observations
|
|
898
|
+
WHERE id > ?
|
|
899
|
+
AND (
|
|
900
|
+
embedding IS NULL
|
|
901
|
+
`;
|
|
902
|
+
const params = [afterId];
|
|
903
|
+
if (includeMismatched) {
|
|
904
|
+
sql += `
|
|
905
|
+
OR embedding_model IS NULL OR embedding_model != ?
|
|
906
|
+
OR embedding_dim IS NULL OR embedding_dim != ?
|
|
907
|
+
`;
|
|
908
|
+
params.push(activeModel, activeDim);
|
|
909
|
+
}
|
|
910
|
+
sql += `
|
|
911
|
+
)
|
|
912
|
+
ORDER BY id ASC
|
|
913
|
+
LIMIT ?
|
|
914
|
+
`;
|
|
915
|
+
params.push(limit);
|
|
916
|
+
const stmt = this.db.prepare(sql);
|
|
917
|
+
stmt.bind(params);
|
|
918
|
+
const rows = [];
|
|
919
|
+
while (stmt.step()) {
|
|
920
|
+
const row = stmt.getAsObject();
|
|
921
|
+
rows.push({
|
|
922
|
+
id: row.id,
|
|
923
|
+
text: `${row.title} ${row.content}`,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
stmt.free();
|
|
927
|
+
return rows;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Persist a backfilled embedding for a single observation (design D6).
|
|
931
|
+
* Metadata-only write — content/updated_at/last_accessed_at are untouched.
|
|
932
|
+
*/
|
|
933
|
+
async updateObservationEmbedding(id, embedding, model, dim) {
|
|
934
|
+
this.db.run('UPDATE memory_observations SET embedding = ?, embedding_model = ?, embedding_dim = ? WHERE id = ?', [serializeEmbedding(embedding), model, dim, id]);
|
|
935
|
+
}
|
|
756
936
|
}
|
|
757
937
|
//# sourceMappingURL=sqlite.js.map
|