claude-mem-lite 3.96.0 → 3.97.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/adopt-cli.mjs +125 -51
- package/bash-utils.mjs +105 -29
- package/claudemd.mjs +63 -19
- package/cli/activity.mjs +36 -18
- package/cli/common.mjs +101 -18
- package/cli/doctor.mjs +29 -9
- package/cli.mjs +69 -10
- package/deep-search.mjs +63 -20
- package/format-utils.mjs +15 -7
- package/haiku-client.mjs +189 -111
- package/hash-utils.mjs +15 -6
- package/hook-context.mjs +135 -46
- package/hook-episode.mjs +76 -23
- package/hook-handoff.mjs +216 -84
- package/hook-llm.mjs +476 -216
- package/hook-memory.mjs +106 -43
- package/hook-optimize.mjs +272 -109
- package/hook-precompact.mjs +1 -1
- package/hook-semaphore.mjs +46 -12
- package/hook-shared.mjs +101 -131
- package/hook-update.mjs +286 -88
- package/hook.mjs +1207 -617
- package/install-metadata.mjs +246 -125
- package/install.mjs +1327 -917
- package/lib/activity.mjs +52 -29
- package/lib/atomic-write.mjs +23 -4
- package/lib/binding-probe.mjs +40 -38
- package/lib/browse-core.mjs +22 -8
- package/lib/citation-tracker.mjs +189 -79
- package/lib/cite-back-hint.mjs +37 -19
- package/lib/cite-recall-path.mjs +3 -1
- package/lib/cli-flags.mjs +4 -2
- package/lib/cli-project.mjs +8 -5
- package/lib/compress-core.mjs +34 -9
- package/lib/cooldown-path.mjs +3 -1
- package/lib/db-backup.mjs +33 -11
- package/lib/deferred-work.mjs +73 -32
- package/lib/delete-core.mjs +25 -9
- package/lib/doctor-benchmark.mjs +5 -1
- package/lib/doctor-drift.mjs +25 -8
- package/lib/edge-attribution.mjs +23 -9
- package/lib/env-number.mjs +7 -8
- package/lib/err-sampler.mjs +22 -11
- package/lib/error-recall-core.mjs +15 -17
- package/lib/events-injection.mjs +25 -8
- package/lib/export-columns.mjs +49 -10
- package/lib/fast-summary.mjs +22 -9
- package/lib/file-edge-match.mjs +4 -2
- package/lib/file-intel.mjs +38 -13
- package/lib/frontmatter.mjs +22 -5
- package/lib/get-core.mjs +78 -11
- package/lib/handoff-constants.mjs +18 -0
- package/lib/hook-stdin.mjs +10 -3
- package/lib/hook-stdout.mjs +21 -7
- package/lib/hook-telemetry.mjs +44 -17
- package/lib/id-routing.mjs +11 -6
- package/lib/import-jsonl.mjs +92 -41
- package/lib/inject-search-core.mjs +5 -2
- package/lib/injected-ids.mjs +19 -12
- package/lib/install-shape.mjs +47 -23
- package/lib/keyctx-marker.mjs +3 -1
- package/lib/lesson-bridge.mjs +8 -5
- package/lib/llm-call.mjs +76 -0
- package/lib/llm-provider-probe.mjs +24 -12
- package/lib/low-signal-patterns.mjs +25 -22
- package/lib/maintain-core.mjs +221 -92
- package/lib/mem-override.mjs +4 -2
- package/lib/metrics.mjs +30 -25
- package/lib/native-binding-hint.mjs +29 -8
- package/lib/observation-write.mjs +74 -19
- package/lib/patha-exclude-meter.mjs +10 -6
- package/lib/persist-reminder.mjs +22 -10
- package/lib/plan-reader.mjs +2 -2
- package/lib/private-strip.mjs +3 -3
- package/lib/proc-lock.mjs +15 -3
- package/lib/proxy-fetch.mjs +49 -13
- package/lib/quiet-scope.mjs +42 -0
- package/lib/recall-core.mjs +19 -7
- package/lib/recent-core.mjs +18 -5
- package/lib/registry-core.mjs +12 -6
- package/lib/release-digest.mjs +4 -1
- package/lib/relevance-floor.mjs +5 -2
- package/lib/reread-guard.mjs +16 -8
- package/lib/resolve-data-dir.mjs +1 -1
- package/lib/rrf.mjs +4 -1
- package/lib/save-enrich.mjs +36 -18
- package/lib/save-observation.mjs +78 -35
- package/lib/scrub-record.mjs +20 -6
- package/lib/search-core.mjs +456 -123
- package/lib/shard-gc.mjs +50 -0
- package/lib/startup-dashboard.mjs +20 -9
- package/lib/stats-core.mjs +101 -35
- package/lib/stats-quality.mjs +56 -20
- package/lib/summary-extractor.mjs +2 -2
- package/lib/task-imperative.mjs +12 -4
- package/lib/timeline-core.mjs +88 -28
- package/lib/tmp-fixture-sweep.mjs +32 -9
- package/lib/transcript-scan.mjs +20 -4
- package/lib/upgrade-banner.mjs +11 -7
- package/mem-cli.mjs +1314 -506
- package/memdir.mjs +53 -21
- package/nlp.mjs +68 -38
- package/npm-shrinkwrap.json +2 -2
- package/package.json +9 -2
- package/plugin-cache-guard.mjs +31 -15
- package/project-utils.mjs +44 -20
- package/registry-enricher.mjs +26 -6
- package/registry-github.mjs +4 -2
- package/registry-importer.mjs +151 -40
- package/registry-recommend.mjs +189 -96
- package/registry-retriever.mjs +229 -70
- package/registry-scanner.mjs +11 -6
- package/registry.mjs +163 -66
- package/rerank.mjs +18 -4
- package/resource-discovery.mjs +14 -4
- package/schema.mjs +258 -99
- package/scoring-sql.mjs +21 -10
- package/scripts/binding-probe-cli.mjs +17 -12
- package/scripts/hook-launcher.mjs +148 -31
- package/scripts/launch-preflight.mjs +5 -7
- package/scripts/launch.mjs +21 -9
- package/scripts/post-tool-recall.js +21 -5
- package/scripts/pre-agent-inject.js +51 -13
- package/scripts/pre-skill-bridge.js +22 -7
- package/scripts/pre-tool-recall.js +157 -77
- package/scripts/prompt-search-utils.mjs +70 -25
- package/scripts/setup.sh +16 -1
- package/scripts/user-prompt-search.js +238 -97
- package/search-engine.mjs +329 -96
- package/search-scoring.mjs +75 -33
- package/secret-scrub.mjs +48 -12
- package/server/fts-check.mjs +12 -9
- package/server.mjs +674 -243
- package/skip-tools.mjs +19 -8
- package/source-files.mjs +54 -21
- package/stop-words.mjs +152 -19
- package/synonyms.mjs +579 -164
- package/tfidf.mjs +130 -40
- package/tier.mjs +1 -1
- package/tool-schemas.mjs +381 -131
- package/utils.mjs +184 -36
package/haiku-client.mjs
CHANGED
|
@@ -110,7 +110,9 @@ export function detectMode() {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/** Reset cached mode (for testing). */
|
|
113
|
-
export function _resetMode() {
|
|
113
|
+
export function _resetMode() {
|
|
114
|
+
_mode = null;
|
|
115
|
+
}
|
|
114
116
|
|
|
115
117
|
// ─── CLI Path ────────────────────────────────────────────────────────────────
|
|
116
118
|
|
|
@@ -174,15 +176,22 @@ export function flattenForCLI(input) {
|
|
|
174
176
|
* @param {number} [opts.maxTokens=500] Max tokens in response
|
|
175
177
|
* @returns {Promise<{text: string}|null>} Response or null on failure
|
|
176
178
|
*/
|
|
177
|
-
export async function callHaiku(
|
|
179
|
+
export async function callHaiku(
|
|
180
|
+
prompt,
|
|
181
|
+
{ timeout = 10000, maxTokens = 500, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
182
|
+
) {
|
|
178
183
|
if (!prompt) return null;
|
|
179
184
|
|
|
180
185
|
const mode = detectMode();
|
|
181
186
|
|
|
182
187
|
// CLI is terminal — no provider to fall back to.
|
|
183
188
|
if (mode === 'cli') {
|
|
184
|
-
try {
|
|
185
|
-
|
|
189
|
+
try {
|
|
190
|
+
return callHaikuCLI(prompt, { timeout });
|
|
191
|
+
} catch (e) {
|
|
192
|
+
debugCatch(e, 'callHaiku');
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
186
195
|
}
|
|
187
196
|
|
|
188
197
|
// Keyed provider (api/openrouter): attempt it, then degrade to the CLI on any
|
|
@@ -196,17 +205,22 @@ export async function callHaiku(prompt, { timeout = 10000, maxTokens = 500, temp
|
|
|
196
205
|
// log label that lied under CLAUDE_MEM_MODEL=sonnet. Two copies of an HTTP client
|
|
197
206
|
// means every proxy fix has to land twice, on the path where missing the proxy is
|
|
198
207
|
// the difference between 1.4s and 13.5s.
|
|
199
|
-
primary =
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
primary =
|
|
209
|
+
mode === 'api'
|
|
210
|
+
? await callModelAPI(prompt, resolveModel().cli, { timeout, maxTokens, temperature })
|
|
211
|
+
: await callOpenRouterAPI(prompt, resolveModel().cli, { timeout, maxTokens, temperature });
|
|
202
212
|
} catch (e) {
|
|
203
213
|
debugCatch(e, `callHaiku:${mode}`);
|
|
204
214
|
}
|
|
205
215
|
if (primary) return primary;
|
|
206
216
|
|
|
207
217
|
debugLog('WARN', 'haiku-client', `${mode} call failed, falling back to claude CLI`);
|
|
208
|
-
try {
|
|
209
|
-
|
|
218
|
+
try {
|
|
219
|
+
return callHaikuCLI(prompt, { timeout });
|
|
220
|
+
} catch (e) {
|
|
221
|
+
debugCatch(e, 'callHaiku:cli-fallback');
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
210
224
|
}
|
|
211
225
|
|
|
212
226
|
/**
|
|
@@ -240,7 +254,10 @@ export async function callHaikuJSON(prompt, opts) {
|
|
|
240
254
|
* @param {{timeout?:number,maxTokens?:number,temperature?:number}} [opts]
|
|
241
255
|
* @returns {Promise<object|null>} Parsed JSON or null
|
|
242
256
|
*/
|
|
243
|
-
export async function callHaikuJSONAsync(
|
|
257
|
+
export async function callHaikuJSONAsync(
|
|
258
|
+
prompt,
|
|
259
|
+
{ timeout = 10000, maxTokens = 500, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
260
|
+
) {
|
|
244
261
|
return callModelJSONAsync(prompt, resolveModel().cli, { timeout, maxTokens, temperature });
|
|
245
262
|
}
|
|
246
263
|
|
|
@@ -258,32 +275,45 @@ export async function callHaikuJSONAsync(prompt, { timeout = 10000, maxTokens =
|
|
|
258
275
|
* @param {number} [opts.maxTokens=1000] Max tokens in response
|
|
259
276
|
* @returns {Promise<{text: string}|null>} Response or null on failure
|
|
260
277
|
*/
|
|
261
|
-
export async function callLLMWithModel(
|
|
278
|
+
export async function callLLMWithModel(
|
|
279
|
+
prompt,
|
|
280
|
+
model = 'haiku',
|
|
281
|
+
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
282
|
+
) {
|
|
262
283
|
if (!prompt) return null;
|
|
263
284
|
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
264
285
|
const mode = detectMode();
|
|
265
286
|
|
|
266
287
|
// CLI is terminal — no provider to fall back to.
|
|
267
288
|
if (mode === 'cli') {
|
|
268
|
-
try {
|
|
269
|
-
|
|
289
|
+
try {
|
|
290
|
+
return callModelCLI(prompt, resolvedModel, { timeout });
|
|
291
|
+
} catch (e) {
|
|
292
|
+
debugCatch(e, `callLLMWithModel:${resolvedModel}`);
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
270
295
|
}
|
|
271
296
|
|
|
272
297
|
// Keyed provider (api/openrouter): attempt it, then degrade to the CLI on any
|
|
273
298
|
// failure so a region-blocked / out-of-credit key still produces output.
|
|
274
299
|
let primary = null;
|
|
275
300
|
try {
|
|
276
|
-
primary =
|
|
277
|
-
|
|
278
|
-
|
|
301
|
+
primary =
|
|
302
|
+
mode === 'api'
|
|
303
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature })
|
|
304
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature });
|
|
279
305
|
} catch (e) {
|
|
280
306
|
debugCatch(e, `callLLMWithModel:${mode}:${resolvedModel}`);
|
|
281
307
|
}
|
|
282
308
|
if (primary) return primary;
|
|
283
309
|
|
|
284
310
|
debugLog('WARN', 'haiku-client', `${mode} call failed, falling back to claude CLI (${resolvedModel})`);
|
|
285
|
-
try {
|
|
286
|
-
|
|
311
|
+
try {
|
|
312
|
+
return callModelCLI(prompt, resolvedModel, { timeout });
|
|
313
|
+
} catch (e) {
|
|
314
|
+
debugCatch(e, `callLLMWithModel:cli-fallback:${resolvedModel}`);
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
287
317
|
}
|
|
288
318
|
|
|
289
319
|
/**
|
|
@@ -302,7 +332,11 @@ export async function callLLMWithModel(prompt, model = 'haiku', { timeout = 1500
|
|
|
302
332
|
* @param {{timeout?:number,maxTokens?:number,temperature?:number}} [opts]
|
|
303
333
|
* @returns {Promise<{text: string}|null>} Response or null on failure
|
|
304
334
|
*/
|
|
305
|
-
export async function callLLMWithModelAsync(
|
|
335
|
+
export async function callLLMWithModelAsync(
|
|
336
|
+
prompt,
|
|
337
|
+
model = 'haiku',
|
|
338
|
+
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
339
|
+
) {
|
|
306
340
|
if (!prompt) return null;
|
|
307
341
|
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
308
342
|
const mode = detectMode();
|
|
@@ -312,15 +346,20 @@ export async function callLLMWithModelAsync(prompt, model = 'haiku', { timeout =
|
|
|
312
346
|
|
|
313
347
|
let primary = null;
|
|
314
348
|
try {
|
|
315
|
-
primary =
|
|
316
|
-
|
|
317
|
-
|
|
349
|
+
primary =
|
|
350
|
+
mode === 'api'
|
|
351
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature })
|
|
352
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature });
|
|
318
353
|
} catch (e) {
|
|
319
354
|
debugCatch(e, `callLLMWithModelAsync:${mode}:${resolvedModel}`);
|
|
320
355
|
}
|
|
321
356
|
if (primary) return primary;
|
|
322
357
|
|
|
323
|
-
debugLog(
|
|
358
|
+
debugLog(
|
|
359
|
+
'WARN',
|
|
360
|
+
'haiku-client',
|
|
361
|
+
`${mode} call failed, falling back to async claude CLI (${resolvedModel})`,
|
|
362
|
+
);
|
|
324
363
|
return callModelCLIAsync(prompt, resolvedModel, { timeout });
|
|
325
364
|
}
|
|
326
365
|
|
|
@@ -349,7 +388,11 @@ export async function callModelJSON(prompt, model = 'haiku', opts) {
|
|
|
349
388
|
* @param {{timeout?:number,maxTokens?:number,temperature?:number}} [opts]
|
|
350
389
|
* @returns {Promise<object|null>}
|
|
351
390
|
*/
|
|
352
|
-
export async function callModelJSONAsync(
|
|
391
|
+
export async function callModelJSONAsync(
|
|
392
|
+
prompt,
|
|
393
|
+
model = 'haiku',
|
|
394
|
+
{ timeout = 15000, maxTokens = 1000, temperature = DEFAULT_LLM_TEMPERATURE } = {},
|
|
395
|
+
) {
|
|
353
396
|
if (!prompt) return null;
|
|
354
397
|
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
355
398
|
const mode = detectMode();
|
|
@@ -363,9 +406,10 @@ export async function callModelJSONAsync(prompt, model = 'haiku', { timeout = 15
|
|
|
363
406
|
// failure — NOT the blocking execFileSync callModelCLI that callModelJSON uses.
|
|
364
407
|
let primary = null;
|
|
365
408
|
try {
|
|
366
|
-
primary =
|
|
367
|
-
|
|
368
|
-
|
|
409
|
+
primary =
|
|
410
|
+
mode === 'api'
|
|
411
|
+
? await callModelAPI(prompt, resolvedModel, { timeout, maxTokens, temperature })
|
|
412
|
+
: await callOpenRouterAPI(prompt, resolvedModel, { timeout, maxTokens, temperature });
|
|
369
413
|
} catch (e) {
|
|
370
414
|
debugCatch(e, `callModelJSONAsync:${mode}:${resolvedModel}`);
|
|
371
415
|
}
|
|
@@ -413,13 +457,17 @@ async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = D
|
|
|
413
457
|
};
|
|
414
458
|
const apiProxy = httpConnectProxyFor(apiUrl);
|
|
415
459
|
const res = apiProxy
|
|
416
|
-
? await postViaConnectProxy(apiProxy, apiUrl, {
|
|
460
|
+
? await postViaConnectProxy(apiProxy, apiUrl, {
|
|
461
|
+
headers: apiHeaders,
|
|
462
|
+
body: JSON.stringify(body),
|
|
463
|
+
timeout,
|
|
464
|
+
})
|
|
417
465
|
: await fetch(apiUrl, {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
466
|
+
method: 'POST',
|
|
467
|
+
headers: apiHeaders,
|
|
468
|
+
body: JSON.stringify(body),
|
|
469
|
+
signal: controller.signal,
|
|
470
|
+
});
|
|
423
471
|
|
|
424
472
|
if (!res.ok) {
|
|
425
473
|
debugLog('WARN', `${model}-api`, `HTTP ${res.status}`);
|
|
@@ -460,12 +508,12 @@ const HEADLESS_FLAG = '--no-session-persistence';
|
|
|
460
508
|
let _headlessFlagOk = true;
|
|
461
509
|
|
|
462
510
|
/** @internal test hook — module-level compat state must not leak across cases. */
|
|
463
|
-
export function _resetHeadlessFlag() {
|
|
511
|
+
export function _resetHeadlessFlag() {
|
|
512
|
+
_headlessFlagOk = true;
|
|
513
|
+
}
|
|
464
514
|
|
|
465
515
|
function claudeArgs(modelName) {
|
|
466
|
-
return _headlessFlagOk
|
|
467
|
-
? ['-p', '--model', modelName, HEADLESS_FLAG]
|
|
468
|
-
: ['-p', '--model', modelName];
|
|
516
|
+
return _headlessFlagOk ? ['-p', '--model', modelName, HEADLESS_FLAG] : ['-p', '--model', modelName];
|
|
469
517
|
}
|
|
470
518
|
|
|
471
519
|
// A retry is only ever worth it when the diagnostic NAMES the token it rejected —
|
|
@@ -480,7 +528,8 @@ function claudeArgs(modelName) {
|
|
|
480
528
|
// Deliberately NOT keyed on exit code alone either: a non-zero exit is also the
|
|
481
529
|
// normal shape of an overload/auth failure.
|
|
482
530
|
const FLAG_TOKEN = /no-session-persistence/;
|
|
483
|
-
const PARSE_REJECTION =
|
|
531
|
+
const PARSE_REJECTION =
|
|
532
|
+
/(unknown|unrecognized|unsupported|invalid|unexpected)[^\n]{0,40}(option|argument|flag|switch)/i;
|
|
484
533
|
|
|
485
534
|
// Below this many ms left, a retry can only spawn a process and immediately kill
|
|
486
535
|
// it — worse than returning the original failure.
|
|
@@ -541,7 +590,11 @@ export function execClaudeCliSync(modelName, { input, timeout }) {
|
|
|
541
590
|
if (remaining < RETRY_MIN_BUDGET_MS) throw e;
|
|
542
591
|
const out = execFileSync(getClaudePath(), ['-p', '--model', modelName], { ...opts, timeout: remaining });
|
|
543
592
|
_headlessFlagOk = false;
|
|
544
|
-
debugLog(
|
|
593
|
+
debugLog(
|
|
594
|
+
'WARN',
|
|
595
|
+
'cli-compat',
|
|
596
|
+
`claude CLI rejected ${HEADLESS_FLAG}; dropped for this process (the headless session tax returns — upgrade Claude Code to avoid it)`,
|
|
597
|
+
);
|
|
545
598
|
return out;
|
|
546
599
|
}
|
|
547
600
|
}
|
|
@@ -590,75 +643,90 @@ export async function callModelCLIAsync(prompt, model, { timeout }) {
|
|
|
590
643
|
// a number ONLY when the child exited on its own; a timeout/SIGKILL or a spawn
|
|
591
644
|
// error reports null, which is what keeps either from being mistaken for an
|
|
592
645
|
// argument-parse rejection and costing a second full-budget spawn.
|
|
593
|
-
const attempt = (args, budget) =>
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
607
|
-
let stdout = '';
|
|
608
|
-
let stderr = '';
|
|
609
|
-
let settled = false;
|
|
610
|
-
const done = (val) => {
|
|
611
|
-
if (settled) return;
|
|
612
|
-
settled = true;
|
|
613
|
-
clearTimeout(timer);
|
|
614
|
-
resolve(val);
|
|
615
|
-
};
|
|
616
|
-
const timer = setTimeout(() => {
|
|
617
|
-
try { child.kill('SIGKILL'); } catch { /* already gone */ }
|
|
618
|
-
const t = stdout.trim();
|
|
619
|
-
// Salvage fenced-or-bare JSON from partial stdout (mirrors callModelCLI). A raw
|
|
620
|
-
// brace check would discard a complete-but-```json-fenced payload (#8605);
|
|
621
|
-
// parseJsonFromLLM strips fences before validating, and the caller re-parses
|
|
622
|
-
// the returned text the same way.
|
|
623
|
-
if (t && parseJsonFromLLM(t) !== null) { done({ result: { text: t }, stderr, stdout, code: null }); return; }
|
|
624
|
-
done({ result: null, stderr, stdout, code: null });
|
|
625
|
-
}, budget);
|
|
626
|
-
child.stdout?.setEncoding('utf8'); // decode multi-byte UTF-8 (CJK) across chunk boundaries
|
|
627
|
-
child.stdout?.on('data', (d) => { stdout += d; });
|
|
628
|
-
// Keep draining stderr so a chatty child can't block on a full pipe, but keep
|
|
629
|
-
// a bounded head of it — the flag-compat probe needs the parser's complaint.
|
|
630
|
-
// Slice AFTER appending: checking the length first lets one arbitrarily large
|
|
631
|
-
// chunk through whole, which is the shape a single big stderr write takes.
|
|
632
|
-
child.stderr?.setEncoding?.('utf8');
|
|
633
|
-
child.stderr?.on('data', (d) => { stderr = (stderr + d).slice(0, 4096); });
|
|
634
|
-
child.on('error', (e) => { debugCatch(e, `${model}-cli-async`); done({ result: null, stderr: '', stdout: '', code: null }); });
|
|
635
|
-
child.on('close', (code) => {
|
|
636
|
-
const t = stdout.trim();
|
|
637
|
-
// Parity with callModelCLI: execFileSync THROWS on a non-zero exit, so the
|
|
638
|
-
// sync leg only ever returns such output when parseJsonFromLLM accepts it
|
|
639
|
-
// (its catch-salvage). Without the same gate, a CLI that prints a
|
|
640
|
-
// diagnostic to stdout and dies — auth failure, overload banner, wrapper
|
|
641
|
-
// error — has that diagnostic returned as the model's ANSWER. rerank is the
|
|
642
|
-
// first caller to consume the raw {text}: extractRanked's last resort
|
|
643
|
-
// matches any bracketed number list in prose, so a `[1]` inside a stack
|
|
644
|
-
// frame becomes a ranking and silently reorders search results. The
|
|
645
|
-
// flag-compat probe below reads stderr/stdout/code directly, not `result`,
|
|
646
|
-
// so nulling here does not cost it its retry.
|
|
647
|
-
if (t && typeof code === 'number' && code !== 0 && parseJsonFromLLM(t) === null) {
|
|
648
|
-
done({ result: null, stderr, stdout, code });
|
|
646
|
+
const attempt = (args, budget) =>
|
|
647
|
+
new Promise((resolve) => {
|
|
648
|
+
let child;
|
|
649
|
+
try {
|
|
650
|
+
// Same headless-tax flags + flag-compat retry as callModelCLI (rationale there).
|
|
651
|
+
child = spawn(getClaudePath(), args, {
|
|
652
|
+
env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1', DISABLE_CLAUDEMD_HOOKS: '1' },
|
|
653
|
+
cwd: '/tmp',
|
|
654
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
655
|
+
});
|
|
656
|
+
} catch (e) {
|
|
657
|
+
debugCatch(e, `${model}-cli-async`);
|
|
658
|
+
resolve({ result: null, stderr: '', stdout: '', code: null });
|
|
649
659
|
return;
|
|
650
660
|
}
|
|
651
|
-
|
|
661
|
+
let stdout = '';
|
|
662
|
+
let stderr = '';
|
|
663
|
+
let settled = false;
|
|
664
|
+
const done = (val) => {
|
|
665
|
+
if (settled) return;
|
|
666
|
+
settled = true;
|
|
667
|
+
clearTimeout(timer);
|
|
668
|
+
resolve(val);
|
|
669
|
+
};
|
|
670
|
+
const timer = setTimeout(() => {
|
|
671
|
+
try {
|
|
672
|
+
child.kill('SIGKILL');
|
|
673
|
+
} catch {
|
|
674
|
+
/* already gone */
|
|
675
|
+
}
|
|
676
|
+
const t = stdout.trim();
|
|
677
|
+
// Salvage fenced-or-bare JSON from partial stdout (mirrors callModelCLI). A raw
|
|
678
|
+
// brace check would discard a complete-but-```json-fenced payload (#8605);
|
|
679
|
+
// parseJsonFromLLM strips fences before validating, and the caller re-parses
|
|
680
|
+
// the returned text the same way.
|
|
681
|
+
if (t && parseJsonFromLLM(t) !== null) {
|
|
682
|
+
done({ result: { text: t }, stderr, stdout, code: null });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
done({ result: null, stderr, stdout, code: null });
|
|
686
|
+
}, budget);
|
|
687
|
+
child.stdout?.setEncoding('utf8'); // decode multi-byte UTF-8 (CJK) across chunk boundaries
|
|
688
|
+
child.stdout?.on('data', (d) => {
|
|
689
|
+
stdout += d;
|
|
690
|
+
});
|
|
691
|
+
// Keep draining stderr so a chatty child can't block on a full pipe, but keep
|
|
692
|
+
// a bounded head of it — the flag-compat probe needs the parser's complaint.
|
|
693
|
+
// Slice AFTER appending: checking the length first lets one arbitrarily large
|
|
694
|
+
// chunk through whole, which is the shape a single big stderr write takes.
|
|
695
|
+
child.stderr?.setEncoding?.('utf8');
|
|
696
|
+
child.stderr?.on('data', (d) => {
|
|
697
|
+
stderr = (stderr + d).slice(0, 4096);
|
|
698
|
+
});
|
|
699
|
+
child.on('error', (e) => {
|
|
700
|
+
debugCatch(e, `${model}-cli-async`);
|
|
701
|
+
done({ result: null, stderr: '', stdout: '', code: null });
|
|
702
|
+
});
|
|
703
|
+
child.on('close', (code) => {
|
|
704
|
+
const t = stdout.trim();
|
|
705
|
+
// Parity with callModelCLI: execFileSync THROWS on a non-zero exit, so the
|
|
706
|
+
// sync leg only ever returns such output when parseJsonFromLLM accepts it
|
|
707
|
+
// (its catch-salvage). Without the same gate, a CLI that prints a
|
|
708
|
+
// diagnostic to stdout and dies — auth failure, overload banner, wrapper
|
|
709
|
+
// error — has that diagnostic returned as the model's ANSWER. rerank is the
|
|
710
|
+
// first caller to consume the raw {text}: extractRanked's last resort
|
|
711
|
+
// matches any bracketed number list in prose, so a `[1]` inside a stack
|
|
712
|
+
// frame becomes a ranking and silently reorders search results. The
|
|
713
|
+
// flag-compat probe below reads stderr/stdout/code directly, not `result`,
|
|
714
|
+
// so nulling here does not cost it its retry.
|
|
715
|
+
if (t && typeof code === 'number' && code !== 0 && parseJsonFromLLM(t) === null) {
|
|
716
|
+
done({ result: null, stderr, stdout, code });
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
done({ result: t ? { text: t } : null, stderr, stdout, code });
|
|
720
|
+
});
|
|
721
|
+
// EPIPE guard: the child may exit before we finish writing stdin.
|
|
722
|
+
child.stdin?.on('error', () => {});
|
|
723
|
+
try {
|
|
724
|
+
child.stdin?.write(payload);
|
|
725
|
+
child.stdin?.end();
|
|
726
|
+
} catch (e) {
|
|
727
|
+
debugCatch(e, `${model}-cli-async:stdin`);
|
|
728
|
+
}
|
|
652
729
|
});
|
|
653
|
-
// EPIPE guard: the child may exit before we finish writing stdin.
|
|
654
|
-
child.stdin?.on('error', () => {});
|
|
655
|
-
try {
|
|
656
|
-
child.stdin?.write(payload);
|
|
657
|
-
child.stdin?.end();
|
|
658
|
-
} catch (e) {
|
|
659
|
-
debugCatch(e, `${model}-cli-async:stdin`);
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
730
|
|
|
663
731
|
const firstArgs = claudeArgs(modelName);
|
|
664
732
|
const first = await attempt(firstArgs, timeout);
|
|
@@ -668,9 +736,11 @@ export async function callModelCLIAsync(prompt, model, { timeout }) {
|
|
|
668
736
|
// deep-search escalations). Gating on the exit code before `first.result` also
|
|
669
737
|
// covers a CLI that prints its usage banner to stdout and exits non-zero —
|
|
670
738
|
// otherwise that banner is returned as the model's answer and nothing retries.
|
|
671
|
-
const rejected =
|
|
672
|
-
|
|
673
|
-
|
|
739
|
+
const rejected =
|
|
740
|
+
firstArgs.includes(HEADLESS_FLAG) &&
|
|
741
|
+
typeof first.code === 'number' &&
|
|
742
|
+
first.code !== 0 &&
|
|
743
|
+
_isUnknownFlagError(`${first.stderr}\n${first.stdout.slice(0, 4096)}`);
|
|
674
744
|
if (!rejected) return first.result;
|
|
675
745
|
// The rejection is instantaneous (the child dies in argv parsing), so the retry
|
|
676
746
|
// normally gets nearly the whole budget; spend only what is left of it.
|
|
@@ -684,7 +754,11 @@ export async function callModelCLIAsync(prompt, model, { timeout }) {
|
|
|
684
754
|
// twin caches on any non-throwing run; this now means the same thing.
|
|
685
755
|
if (second.code === 0) {
|
|
686
756
|
_headlessFlagOk = false;
|
|
687
|
-
debugLog(
|
|
757
|
+
debugLog(
|
|
758
|
+
'WARN',
|
|
759
|
+
`${model}-cli-async`,
|
|
760
|
+
`claude CLI rejected ${HEADLESS_FLAG}; dropped for this process (the headless session tax returns — upgrade Claude Code to avoid it)`,
|
|
761
|
+
);
|
|
688
762
|
}
|
|
689
763
|
return second.result;
|
|
690
764
|
}
|
|
@@ -698,7 +772,11 @@ export async function callModelCLIAsync(prompt, model, { timeout }) {
|
|
|
698
772
|
// `cache_control` field has no OpenAI-format equivalent and is omitted.
|
|
699
773
|
// `tier` is the resolved model tier ('haiku'|'sonnet'); OPENROUTER_MODEL can
|
|
700
774
|
// override the resulting slug entirely (see resolveOpenRouterModel).
|
|
701
|
-
async function callOpenRouterAPI(
|
|
775
|
+
async function callOpenRouterAPI(
|
|
776
|
+
prompt,
|
|
777
|
+
tier,
|
|
778
|
+
{ timeout, maxTokens, temperature = DEFAULT_LLM_TEMPERATURE },
|
|
779
|
+
) {
|
|
702
780
|
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
703
781
|
if (!apiKey) return null;
|
|
704
782
|
|
|
@@ -715,7 +793,7 @@ async function callOpenRouterAPI(prompt, tier, { timeout, maxTokens, temperature
|
|
|
715
793
|
const url = 'https://openrouter.ai/api/v1/chat/completions';
|
|
716
794
|
const reqHeaders = {
|
|
717
795
|
'Content-Type': 'application/json',
|
|
718
|
-
|
|
796
|
+
Authorization: `Bearer ${apiKey}`,
|
|
719
797
|
// Optional OpenRouter attribution headers (ignored by the API if absent).
|
|
720
798
|
'X-Title': 'claude-mem-lite',
|
|
721
799
|
};
|
package/hash-utils.mjs
CHANGED
|
@@ -11,11 +11,17 @@ export function jaccardSimilarity(a, b) {
|
|
|
11
11
|
if (!a || !b) return 0;
|
|
12
12
|
// Strip trailing punctuation from tokens to match MinHash normalization
|
|
13
13
|
// (prevents "server.rs," ≠ "server.rs" dedup failures)
|
|
14
|
-
const norm =
|
|
14
|
+
const norm = (s) =>
|
|
15
|
+
s
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.split(/\s+/)
|
|
18
|
+
.map((t) => t.replace(/[,;:!?]+$/, ''));
|
|
15
19
|
const setA = new Set(norm(a));
|
|
16
20
|
const setB = new Set(norm(b));
|
|
17
21
|
let intersection = 0;
|
|
18
|
-
for (const w of setA) {
|
|
22
|
+
for (const w of setA) {
|
|
23
|
+
if (setB.has(w)) intersection++;
|
|
24
|
+
}
|
|
19
25
|
const union = setA.size + setB.size - intersection;
|
|
20
26
|
return union === 0 ? 0 : intersection / union;
|
|
21
27
|
}
|
|
@@ -42,19 +48,22 @@ function fnv1a(str) {
|
|
|
42
48
|
*/
|
|
43
49
|
export function computeMinHash(text, numHashes = 64) {
|
|
44
50
|
if (!text || typeof text !== 'string') return null;
|
|
45
|
-
const tokens = text
|
|
46
|
-
.
|
|
51
|
+
const tokens = text
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
54
|
+
.split(/\s+/)
|
|
55
|
+
.filter((t) => t.length > 2);
|
|
47
56
|
// Require at least 3 tokens for meaningful signature (avoids high collision on short texts)
|
|
48
57
|
if (tokens.length < 3) return null;
|
|
49
58
|
|
|
50
|
-
const mins = new Array(numHashes).fill(
|
|
59
|
+
const mins = new Array(numHashes).fill(0xffffffff);
|
|
51
60
|
for (const token of tokens) {
|
|
52
61
|
for (let i = 0; i < numHashes; i++) {
|
|
53
62
|
const val = fnv1a(`${i}-${token}`);
|
|
54
63
|
if (val < mins[i]) mins[i] = val;
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
|
-
return mins.map(v => v.toString(16).padStart(8, '0')).join('');
|
|
66
|
+
return mins.map((v) => v.toString(16).padStart(8, '0')).join('');
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
/**
|