claude-mem-lite 3.2.0 → 3.3.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/deep-search.mjs +105 -13
- package/haiku-client.mjs +105 -1
- package/mem-cli.mjs +3 -3
- package/package.json +1 -1
- package/server.mjs +3 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.3.0",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/deep-search.mjs
CHANGED
|
@@ -68,15 +68,24 @@ export function hasEscalatableCorpus(db, project, min = AUTO_DEEP_MIN_CORPUS) {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Is a usable LLM available for AUTO escalation? True when a stub/real llm is
|
|
71
|
-
* injected (tests),
|
|
72
|
-
*
|
|
73
|
-
*
|
|
71
|
+
* injected (tests), a FAST provider key is set, OR the claude-CLI fallback is
|
|
72
|
+
* enabled (D#40: default-on for CLI-auth users; kill switch
|
|
73
|
+
* CLAUDE_MEM_AUTO_DEEP_CLI=0). The CLI path is made safe for the long-lived
|
|
74
|
+
* server hot path by the async/fail-fast/throttled auto provider (deepSearch
|
|
75
|
+
* auto), not by being excluded as it was before D#40.
|
|
74
76
|
* @param {object} [env=process.env]
|
|
75
77
|
* @param {Function|undefined} [injectedLlm]
|
|
76
78
|
* @returns {boolean}
|
|
77
79
|
*/
|
|
78
80
|
export function autoDeepLlmReady(env = process.env, injectedLlm) {
|
|
79
|
-
|
|
81
|
+
if (injectedLlm) return true;
|
|
82
|
+
if (env.ANTHROPIC_API_KEY || env.OPENROUTER_API_KEY) return true;
|
|
83
|
+
// No provider key → detectMode() would be 'cli'. CLI-auth users get auto
|
|
84
|
+
// escalation by default; the burst/latency cost is bounded by the auto
|
|
85
|
+
// provider (fail-fast + throttle) and a failed rewrite degrades to baseline.
|
|
86
|
+
// Kill switch honors the common disable spellings, not just the exact '0'.
|
|
87
|
+
const off = String(env.CLAUDE_MEM_AUTO_DEEP_CLI ?? '').trim().toLowerCase();
|
|
88
|
+
return !(off === '0' || off === 'false' || off === 'no' || off === 'off');
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
/**
|
|
@@ -189,12 +198,75 @@ export function assembleVariants(query, parsed, { max = MAX_VARIANTS } = {}) {
|
|
|
189
198
|
return out;
|
|
190
199
|
}
|
|
191
200
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
201
|
+
// ─── Auto-escalation safety machinery (D#40) ─────────────────────────────────
|
|
202
|
+
// The AUTO path can fire on every weak search across the long-lived MCP server,
|
|
203
|
+
// so it must be fail-fast (short timeout, no retry), throttled (bound bursts),
|
|
204
|
+
// and cached (skip repeat rewrites). The EXPLICIT deep=true path stays patient.
|
|
205
|
+
|
|
206
|
+
export const AUTO_DEEP_TIMEOUT_MS = 5000; // fail-fast budget for the auto path; no retry
|
|
207
|
+
export const AUTO_DEEP_THROTTLE_MS = 3000; // min gap between auto LLM rewrites, per process (bounds spawn rate)
|
|
208
|
+
const REWRITE_CACHE_MAX = 256; // LRU cap for the query→variants cache
|
|
209
|
+
|
|
210
|
+
let _lastAutoLlmAt = 0;
|
|
211
|
+
const _rewriteCache = new Map(); // normalized query → variants (string[]); successes only
|
|
212
|
+
|
|
213
|
+
/** Reset auto-path throttle + cache. Test-only; production state is per-process. */
|
|
214
|
+
export function _resetAutoDeepState() { _lastAutoLlmAt = 0; _rewriteCache.clear(); }
|
|
215
|
+
|
|
216
|
+
function cacheGet(key) {
|
|
217
|
+
if (!_rewriteCache.has(key)) return null;
|
|
218
|
+
const v = _rewriteCache.get(key);
|
|
219
|
+
_rewriteCache.delete(key); _rewriteCache.set(key, v); // LRU bump
|
|
220
|
+
return v.slice();
|
|
221
|
+
}
|
|
222
|
+
function cacheSet(key, variants) {
|
|
223
|
+
if (_rewriteCache.has(key)) _rewriteCache.delete(key);
|
|
224
|
+
_rewriteCache.set(key, variants.slice());
|
|
225
|
+
if (_rewriteCache.size > REWRITE_CACHE_MAX) {
|
|
226
|
+
_rewriteCache.delete(_rewriteCache.keys().next().value); // evict oldest
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Wrap an llm so it fires at most once per `intervalMs` per process. A throttled
|
|
232
|
+
* call resolves null → rewriteQuery degrades to baseline (never worse). Exported
|
|
233
|
+
* for tests. Throttle state is module-global (shared across deepSearch calls).
|
|
234
|
+
*
|
|
235
|
+
* The clock advances on every ACTUAL call — success OR failure — deliberately:
|
|
236
|
+
* the throttle bounds the subprocess SPAWN RATE, and a failed spawn still costs a
|
|
237
|
+
* subprocess + its timeout, so a broken provider that always fails must be rate-
|
|
238
|
+
* limited too (gating only on success would let a persistent failure spawn on
|
|
239
|
+
* every weak search). The interval is kept short so one failure suppresses
|
|
240
|
+
* escalation only briefly, not for a long window.
|
|
241
|
+
*/
|
|
242
|
+
export function makeThrottled(llm, { intervalMs = AUTO_DEEP_THROTTLE_MS } = {}) {
|
|
243
|
+
return async (prompt) => {
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
if (now - _lastAutoLlmAt < intervalMs) return null;
|
|
246
|
+
_lastAutoLlmAt = now;
|
|
247
|
+
return llm(prompt);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Run one rewrite LLM call via the fully-async dispatcher (callModelJSONAsync):
|
|
252
|
+
// every CLI invocation — cli-mode primary AND the post-provider-failure fallback
|
|
253
|
+
// — is non-blocking, so an MCP request handler never blocks the event loop even
|
|
254
|
+
// under a keyed-provider outage (D#40). Lazy import so tests with an injected llm
|
|
255
|
+
// never load the LLM client.
|
|
256
|
+
async function callRewriteLLM(prompt, { timeout }) {
|
|
257
|
+
const { callModelJSONAsync } = await import('./haiku-client.mjs');
|
|
258
|
+
return callModelJSONAsync(prompt, 'haiku', { timeout, maxTokens: 400 });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Default (explicit deep=true) provider: patient timeout, no throttle/cache.
|
|
195
262
|
async function defaultLLM(prompt) {
|
|
196
|
-
|
|
197
|
-
|
|
263
|
+
return callRewriteLLM(prompt, { timeout: 12000 });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Auto-path provider: fail-fast timeout + throttle. Built fresh per deepSearch
|
|
267
|
+
// call; the throttle clock it reads is module-global (per-process).
|
|
268
|
+
function makeAutoLlm() {
|
|
269
|
+
return makeThrottled((prompt) => callRewriteLLM(prompt, { timeout: AUTO_DEEP_TIMEOUT_MS }));
|
|
198
270
|
}
|
|
199
271
|
|
|
200
272
|
/**
|
|
@@ -205,11 +277,17 @@ async function defaultLLM(prompt) {
|
|
|
205
277
|
* @param {object} [opts]
|
|
206
278
|
* @param {(prompt: object) => Promise<object|null>} [opts.llm]
|
|
207
279
|
* @param {number} [opts.retries=1]
|
|
280
|
+
* @param {boolean} [opts.cache=false] memoize successful rewrites (auto path)
|
|
208
281
|
* @returns {Promise<string[]>}
|
|
209
282
|
*/
|
|
210
|
-
export async function rewriteQuery(query, { llm = defaultLLM, retries = 1 } = {}) {
|
|
283
|
+
export async function rewriteQuery(query, { llm = defaultLLM, retries = 1, cache = false } = {}) {
|
|
211
284
|
const original = String(query ?? '').trim();
|
|
212
285
|
if (!original) return [];
|
|
286
|
+
const key = original.toLowerCase();
|
|
287
|
+
if (cache) {
|
|
288
|
+
const hit = cacheGet(key);
|
|
289
|
+
if (hit) return hit; // process-lifetime memo of a prior successful rewrite
|
|
290
|
+
}
|
|
213
291
|
const prompt = buildRewritePrompt(original);
|
|
214
292
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
215
293
|
let parsed;
|
|
@@ -219,7 +297,10 @@ export async function rewriteQuery(query, { llm = defaultLLM, retries = 1 } = {}
|
|
|
219
297
|
parsed = null;
|
|
220
298
|
}
|
|
221
299
|
const variants = assembleVariants(original, parsed);
|
|
222
|
-
if (variants.length > 1)
|
|
300
|
+
if (variants.length > 1) { // got at least one real rewrite
|
|
301
|
+
if (cache) cacheSet(key, variants); // cache successes only — failures retry next time
|
|
302
|
+
return variants;
|
|
303
|
+
}
|
|
223
304
|
}
|
|
224
305
|
return [original]; // robust floor — single-query == baseline
|
|
225
306
|
}
|
|
@@ -304,13 +385,24 @@ function defaultSearchFn(db, query, params) {
|
|
|
304
385
|
* @param {(prompt:object)=>Promise<object|null>} [deps.llm]
|
|
305
386
|
* @param {(db:Database, query:string, params:object)=>Array} [deps.searchFn]
|
|
306
387
|
* @param {number} [deps.rrfK=RRF_K]
|
|
388
|
+
* @param {boolean} [deps.auto=false] use the fail-fast/throttled/cached auto provider
|
|
307
389
|
* @returns {Promise<{results: Array, variants: string[]}>}
|
|
308
390
|
*/
|
|
309
|
-
export async function deepSearch(db, params, { llm
|
|
391
|
+
export async function deepSearch(db, params, { llm, searchFn = defaultSearchFn, rrfK = RRF_K, auto = false } = {}) {
|
|
310
392
|
const query = String(params?.query ?? '').trim();
|
|
311
393
|
if (!query) return { results: [], variants: [] };
|
|
312
394
|
|
|
313
|
-
|
|
395
|
+
// No injected llm: EXPLICIT deep=true uses the patient defaultLLM; the AUTO
|
|
396
|
+
// path uses a fail-fast + throttled provider with no retry and a process-
|
|
397
|
+
// lifetime rewrite cache (D#40). An injected llm (tests) is used verbatim.
|
|
398
|
+
let rewriteLlm = llm;
|
|
399
|
+
let retries = 1;
|
|
400
|
+
let cache = false;
|
|
401
|
+
if (!rewriteLlm) {
|
|
402
|
+
if (auto) { rewriteLlm = makeAutoLlm(); retries = 0; cache = true; }
|
|
403
|
+
else rewriteLlm = defaultLLM;
|
|
404
|
+
}
|
|
405
|
+
const variants = await rewriteQuery(query, { llm: rewriteLlm, retries, cache });
|
|
314
406
|
const lists = variants.map((v, i) => {
|
|
315
407
|
// variant[0] is the ORIGINAL query: let an engine error propagate exactly as
|
|
316
408
|
// it does on the single-query baseline path, so "never worse than baseline"
|
package/haiku-client.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Model configurable via CLAUDE_MEM_MODEL (haiku|sonnet); OpenRouter slug
|
|
6
6
|
// overridable via OPENROUTER_MODEL
|
|
7
7
|
|
|
8
|
-
import { execFileSync } from 'child_process';
|
|
8
|
+
import { execFileSync, spawn } from 'child_process';
|
|
9
9
|
import { readFileSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
import { randomUUID } from 'crypto';
|
|
@@ -247,6 +247,44 @@ export async function callModelJSON(prompt, model = 'haiku', opts) {
|
|
|
247
247
|
return parseJsonFromLLM(result.text);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* JSON-returning, FULLY-ASYNC model call for the long-lived server hot path
|
|
252
|
+
* (deep-search auto-escalation). Like callModelJSON, but every CLI invocation —
|
|
253
|
+
* cli-mode primary AND the post-provider-failure fallback — uses the
|
|
254
|
+
* non-blocking callModelCLIAsync, so a keyed-provider outage can never drop onto
|
|
255
|
+
* the blocking execFileSync path and freeze the MCP event loop (D#40). Never
|
|
256
|
+
* throws; returns parsed JSON or null.
|
|
257
|
+
* @param {string|{system?:string,user:string}} prompt
|
|
258
|
+
* @param {'haiku'|'sonnet'} model
|
|
259
|
+
* @param {{timeout?:number,maxTokens?:number,temperature?:number}} [opts]
|
|
260
|
+
* @returns {Promise<object|null>}
|
|
261
|
+
*/
|
|
262
|
+
export async function callModelJSONAsync(prompt, model = 'haiku', { timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE } = {}) {
|
|
263
|
+
if (!prompt) return null;
|
|
264
|
+
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
265
|
+
const mode = detectMode();
|
|
266
|
+
|
|
267
|
+
if (mode === 'cli') {
|
|
268
|
+
const res = await callModelCLIAsync(prompt, resolvedModel, { timeout });
|
|
269
|
+
return res?.text ? parseJsonFromLLM(res.text) : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Keyed provider (api/openrouter): try it, then degrade to the ASYNC CLI on any
|
|
273
|
+
// failure — NOT the blocking execFileSync callModelCLI that callModelJSON uses.
|
|
274
|
+
let primary = null;
|
|
275
|
+
try {
|
|
276
|
+
primary = mode === 'api'
|
|
277
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature })
|
|
278
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature });
|
|
279
|
+
} catch (e) {
|
|
280
|
+
debugCatch(e, `callModelJSONAsync:${mode}:${resolvedModel}`);
|
|
281
|
+
}
|
|
282
|
+
if (primary?.text) return parseJsonFromLLM(primary.text);
|
|
283
|
+
|
|
284
|
+
const res = await callModelCLIAsync(prompt, resolvedModel, { timeout });
|
|
285
|
+
return res?.text ? parseJsonFromLLM(res.text) : null;
|
|
286
|
+
}
|
|
287
|
+
|
|
250
288
|
async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE }) {
|
|
251
289
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
252
290
|
if (!apiKey) return null;
|
|
@@ -319,6 +357,72 @@ function callModelCLI(prompt, model, { timeout }) {
|
|
|
319
357
|
}
|
|
320
358
|
}
|
|
321
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Async, non-blocking sibling of callModelCLI for the long-lived MCP server hot
|
|
362
|
+
* path (deep-search auto-escalation, D#40). execFileSync blocks the event loop for
|
|
363
|
+
* the whole subprocess lifetime — acceptable in short-lived hook processes
|
|
364
|
+
* (callModelCLI), not inside an MCP request handler. Uses spawn + stdin so the
|
|
365
|
+
* untrusted query stays out of argv (ps-visible) and the boundary-marker model is
|
|
366
|
+
* preserved. Never rejects: resolves {text} on non-empty stdout, null on
|
|
367
|
+
* error/empty. On timeout it SIGKILLs the child with NO retry (fail-fast) and
|
|
368
|
+
* salvages a complete JSON payload from partial stdout (mirrors callModelCLI's
|
|
369
|
+
* catch-salvage; tolerant of Haiku's ```json fencing per #8605, which the upstream
|
|
370
|
+
* parseJsonFromLLM strips).
|
|
371
|
+
* @param {string|{system?:string,user:string}} prompt
|
|
372
|
+
* @param {'haiku'|'sonnet'} model
|
|
373
|
+
* @param {{timeout:number}} opts SIGKILL after `timeout` ms; no retry.
|
|
374
|
+
* @returns {Promise<{text:string}|null>}
|
|
375
|
+
*/
|
|
376
|
+
export function callModelCLIAsync(prompt, model, { timeout }) {
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
const modelName = MODEL_MAP[model] ? model : 'haiku';
|
|
379
|
+
let child;
|
|
380
|
+
try {
|
|
381
|
+
child = spawn(getClaudePath(), ['-p', '--model', modelName], {
|
|
382
|
+
env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1' },
|
|
383
|
+
cwd: '/tmp',
|
|
384
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
385
|
+
});
|
|
386
|
+
} catch (e) {
|
|
387
|
+
debugCatch(e, `${model}-cli-async`);
|
|
388
|
+
resolve(null);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
let stdout = '';
|
|
392
|
+
let settled = false;
|
|
393
|
+
const done = (val) => {
|
|
394
|
+
if (settled) return;
|
|
395
|
+
settled = true;
|
|
396
|
+
clearTimeout(timer);
|
|
397
|
+
resolve(val);
|
|
398
|
+
};
|
|
399
|
+
const timer = setTimeout(() => {
|
|
400
|
+
try { child.kill('SIGKILL'); } catch { /* already gone */ }
|
|
401
|
+
const t = stdout.trim();
|
|
402
|
+
if (t.startsWith('{') && t.endsWith('}')) {
|
|
403
|
+
try { JSON.parse(t); done({ text: t }); return; } catch { /* not complete JSON */ }
|
|
404
|
+
}
|
|
405
|
+
done(null);
|
|
406
|
+
}, timeout);
|
|
407
|
+
child.stdout?.setEncoding('utf8'); // decode multi-byte UTF-8 (CJK) across chunk boundaries
|
|
408
|
+
child.stdout?.on('data', (d) => { stdout += d; });
|
|
409
|
+
child.stderr?.on('data', () => {}); // drain stderr so a chatty child can't block on a full pipe
|
|
410
|
+
child.on('error', (e) => { debugCatch(e, `${model}-cli-async`); done(null); });
|
|
411
|
+
child.on('close', () => {
|
|
412
|
+
const t = stdout.trim();
|
|
413
|
+
done(t ? { text: t } : null);
|
|
414
|
+
});
|
|
415
|
+
// EPIPE guard: the child may exit before we finish writing stdin.
|
|
416
|
+
child.stdin?.on('error', () => {});
|
|
417
|
+
try {
|
|
418
|
+
child.stdin?.write(flattenForCLI(prompt));
|
|
419
|
+
child.stdin?.end();
|
|
420
|
+
} catch (e) {
|
|
421
|
+
debugCatch(e, `${model}-cli-async:stdin`);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
322
426
|
// ─── API Mode ────────────────────────────────────────────────────────────────
|
|
323
427
|
|
|
324
428
|
async function callHaikuAPI(prompt, { timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE }) {
|
package/mem-cli.mjs
CHANGED
|
@@ -182,7 +182,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
182
182
|
orFallbackFired: false,
|
|
183
183
|
};
|
|
184
184
|
|
|
185
|
-
const runDeep = async () => {
|
|
185
|
+
const runDeep = async ({ auto = false } = {}) => {
|
|
186
186
|
const ds = await deepSearch(db, {
|
|
187
187
|
query,
|
|
188
188
|
project: project || null,
|
|
@@ -194,7 +194,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
194
194
|
epochTo: dateTo,
|
|
195
195
|
limit: perSourceLimit,
|
|
196
196
|
currentProject: project ? null : inferProject(),
|
|
197
|
-
}, llm ? { llm } :
|
|
197
|
+
}, llm ? { llm } : { auto });
|
|
198
198
|
deepVariants = ds.variants;
|
|
199
199
|
if (deepVariants.length > 1) {
|
|
200
200
|
process.stderr.write(`[mem] Deep search: rewrote into ${deepVariants.length} query variants, RRF-fused\n`);
|
|
@@ -212,7 +212,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
212
212
|
if (obsCtx.orFallbackFired) orFallbackFired = true;
|
|
213
213
|
if (deepMode === 'auto' && autoDeepLlmReady(process.env, llm) && shouldEscalateToDeep(obsResults, obsCtx) && hasEscalatableCorpus(db, project || null)) {
|
|
214
214
|
process.stderr.write(`[mem] auto-escalated to deep search (weak results: ${obsResults.length} hits)\n`);
|
|
215
|
-
obsResults = await runDeep();
|
|
215
|
+
obsResults = await runDeep({ auto: true });
|
|
216
216
|
isDeep = true;
|
|
217
217
|
}
|
|
218
218
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
package/server.mjs
CHANGED
|
@@ -370,7 +370,7 @@ async function runSearchPipeline(db, args, { llm } = {}) {
|
|
|
370
370
|
let escalatedObsCount = 0;
|
|
371
371
|
|
|
372
372
|
// Helper: run deepSearch and load results into the shared `results` array.
|
|
373
|
-
const runDeepInto = async () => {
|
|
373
|
+
const runDeepInto = async ({ auto = false } = {}) => {
|
|
374
374
|
const { results: deepRows, variants } = await deepSearch(db, {
|
|
375
375
|
query: args.query,
|
|
376
376
|
project: args.project || null,
|
|
@@ -381,7 +381,7 @@ async function runSearchPipeline(db, args, { llm } = {}) {
|
|
|
381
381
|
epochFrom, epochTo,
|
|
382
382
|
limit: perSourceLimit,
|
|
383
383
|
currentProject,
|
|
384
|
-
}, llm ? { llm } :
|
|
384
|
+
}, llm ? { llm } : { auto });
|
|
385
385
|
// Safe to reset: sessions/prompts are pushed AFTER the obs block, so nothing is lost here.
|
|
386
386
|
results.length = 0;
|
|
387
387
|
results.push(...deepRows);
|
|
@@ -405,7 +405,7 @@ async function runSearchPipeline(db, args, { llm } = {}) {
|
|
|
405
405
|
// filter makes the invariant explicit and robust to future reordering.
|
|
406
406
|
const obsCountBeforeEscalation = results.length;
|
|
407
407
|
if (deepMode === 'auto' && autoDeepLlmReady(process.env, llm) && shouldEscalateToDeep(results.filter(r => r.source === 'obs'), ctx) && hasEscalatableCorpus(db, args.project || null)) {
|
|
408
|
-
await runDeepInto();
|
|
408
|
+
await runDeepInto({ auto: true });
|
|
409
409
|
isDeep = true;
|
|
410
410
|
escalated = true;
|
|
411
411
|
escalatedObsCount = obsCountBeforeEscalation;
|