scream-code 0.18.7 → 0.18.9
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/README.md +6 -3
- package/dist/{app-IU7G9gs0.mjs → app-D3QT1bx1.mjs} +502 -211
- package/dist/{dispatch-7Mas_Tvb.mjs → dispatch-BcRHd4ya.mjs} +477 -56
- package/dist/{dispatch-BqBnrIMj.mjs → dispatch-JYsyD0aN.mjs} +1 -1
- package/dist/main.mjs +1 -1
- package/dist/{markit-CVQeFGFG.mjs → markit-CV3BXOV_.mjs} +20 -13
- package/dist/{src-ChTWl9Cn.mjs → src-B1S88rK6.mjs} +1 -1
- package/dist/{src-CQwXASmd.mjs → src-DIVvoAYO.mjs} +522 -134
- package/dist/{text-input-dialog-D7dm6kak.mjs → text-input-dialog-CgAOMXv1.mjs} +1 -1
- package/dist/{text-input-dialog-gDr3i7xz.mjs → text-input-dialog-CqagQQtW.mjs} +96 -14
- package/package.json +3 -4
|
@@ -4,14 +4,14 @@ import { dirname as __cjsShimDirname } from 'node:path';
|
|
|
4
4
|
const __filename = __cjsShimFileURLToPath(import.meta.url);
|
|
5
5
|
const __dirname = __cjsShimDirname(__filename);
|
|
6
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
7
|
-
import ro, { mkdir, readFile, readdir, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import ro, { mkdir, open, readFile, readdir, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
8
8
|
import I from "fs";
|
|
9
9
|
import Qr, { EventEmitter } from "events";
|
|
10
10
|
import js, { dirname, parse } from "path";
|
|
11
11
|
import { Buffer as Buffer$1 } from "buffer";
|
|
12
12
|
import Cs from "node:stream";
|
|
13
|
-
import Kt, { createReadStream, existsSync, mkdirSync, rmSync, statSync } from "node:fs";
|
|
14
|
-
import path, { basename, join, posix, win32 } from "node:path";
|
|
13
|
+
import Kt, { createReadStream, existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
14
|
+
import path, { basename, join, posix, resolve, win32 } from "node:path";
|
|
15
15
|
import { DatabaseSync } from "node:sqlite";
|
|
16
16
|
import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
17
17
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -218,7 +218,7 @@ function computeRelevanceScore(memo, query, usageCount = 0, currentProjectDir, p
|
|
|
218
218
|
const ti = intent?.temporalBias ?? .3;
|
|
219
219
|
const fi = intent?.factualBias ?? .6;
|
|
220
220
|
const factors = {
|
|
221
|
-
keywordOverlap:
|
|
221
|
+
keywordOverlap: computeKeywordOverlap(memo, query),
|
|
222
222
|
recency: computeRecency(memo.recordedAt),
|
|
223
223
|
usageBoost: Math.min(.3, usageCount * .1),
|
|
224
224
|
projectBoost: computeProjectBoost(memo.projectDir, currentProjectDir),
|
|
@@ -416,7 +416,21 @@ function extractKeywords(text) {
|
|
|
416
416
|
return [...new Set(tokens)];
|
|
417
417
|
}
|
|
418
418
|
function computeKeywordSimilarity(memo, query) {
|
|
419
|
-
|
|
419
|
+
return jaccardSimilarity(keywordText(memo), query);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Ranking variant: the distilled conclusion and the note are separate
|
|
423
|
+
* retrieval surfaces. Score each field on its own and keep the best match —
|
|
424
|
+
* a short conclusion would otherwise be diluted by the long base text.
|
|
425
|
+
*/
|
|
426
|
+
function computeKeywordOverlap(memo, query) {
|
|
427
|
+
return Math.max(jaccardSimilarity(keywordText(memo), query), jaccardSimilarity(memo.distilled ?? "", query), jaccardSimilarity(memo.note ?? "", query));
|
|
428
|
+
}
|
|
429
|
+
function keywordText(memo) {
|
|
430
|
+
return `${memo.userNeed} ${memo.approach} ${memo.whatFailed} ${memo.whatWorked}`;
|
|
431
|
+
}
|
|
432
|
+
function jaccardSimilarity(memoText, query) {
|
|
433
|
+
const memoWords = extractKeywords(memoText);
|
|
420
434
|
const queryWords = extractKeywords(query);
|
|
421
435
|
if (memoWords.length === 0 || queryWords.length === 0) return 0;
|
|
422
436
|
const intersection = memoWords.filter((w) => queryWords.includes(w)).length;
|
|
@@ -469,6 +483,10 @@ function createMemoryMemo(partial) {
|
|
|
469
483
|
outcome: partial.outcome,
|
|
470
484
|
whatFailed: partial.whatFailed,
|
|
471
485
|
whatWorked: partial.whatWorked,
|
|
486
|
+
distilled: partial.distilled,
|
|
487
|
+
origin: partial.origin ?? "session",
|
|
488
|
+
distilledFrom: partial.distilledFrom,
|
|
489
|
+
distilledAt: partial.distilledAt,
|
|
472
490
|
extractionSource: partial.extractionSource,
|
|
473
491
|
recordedAt: partial.recordedAt ?? Date.now(),
|
|
474
492
|
projectDir: partial.projectDir ?? "",
|
|
@@ -490,6 +508,10 @@ function toSummary(memo) {
|
|
|
490
508
|
outcome: memo.outcome,
|
|
491
509
|
whatFailed: memo.whatFailed,
|
|
492
510
|
whatWorked: memo.whatWorked,
|
|
511
|
+
distilled: memo.distilled,
|
|
512
|
+
origin: memo.origin,
|
|
513
|
+
distilledFrom: memo.distilledFrom,
|
|
514
|
+
distilledAt: memo.distilledAt,
|
|
493
515
|
extractionSource: memo.extractionSource,
|
|
494
516
|
recordedAt: memo.recordedAt,
|
|
495
517
|
projectDir: memo.projectDir,
|
|
@@ -3531,7 +3553,7 @@ function buildEmbeddingText(memo) {
|
|
|
3531
3553
|
*/
|
|
3532
3554
|
const engineCache = /* @__PURE__ */ new Map();
|
|
3533
3555
|
/**
|
|
3534
|
-
* Create an embedding engine backed by
|
|
3556
|
+
* Create an embedding engine backed by the in-process WASM pipeline.
|
|
3535
3557
|
* Lazily loads the model on first use so startup is not blocked.
|
|
3536
3558
|
* Engines are cached by cacheDir so repeated calls with the same cacheDir
|
|
3537
3559
|
* return the same instance, sharing model state and download progress.
|
|
@@ -3539,21 +3561,22 @@ const engineCache = /* @__PURE__ */ new Map();
|
|
|
3539
3561
|
* Defaults to "local_cache" (CWD-relative) if not provided — prefer
|
|
3540
3562
|
* passing an explicit path so the cache doesn't duplicate across CWDs.
|
|
3541
3563
|
*/
|
|
3542
|
-
function
|
|
3543
|
-
const key = cacheDir
|
|
3564
|
+
function createEmbeddingEngine(cacheDir) {
|
|
3565
|
+
const key = cacheDir === void 0 ? "" : resolve(cacheDir);
|
|
3544
3566
|
const cached = engineCache.get(key);
|
|
3545
3567
|
if (cached !== void 0) return cached;
|
|
3546
|
-
const engine =
|
|
3568
|
+
const engine = createEmbeddingEngineImpl(cacheDir);
|
|
3547
3569
|
engineCache.set(key, engine);
|
|
3548
3570
|
return engine;
|
|
3549
3571
|
}
|
|
3550
3572
|
/** Stable identity of the model used by every engine created here. */
|
|
3551
3573
|
const EMBEDDING_MODEL_NAME = "bge-small-zh-v1.5";
|
|
3552
|
-
function
|
|
3574
|
+
function createEmbeddingEngineImpl(cacheDir) {
|
|
3553
3575
|
let embedder = null;
|
|
3554
3576
|
let initPromise = null;
|
|
3555
3577
|
let loadFailed = false;
|
|
3556
3578
|
let lastError;
|
|
3579
|
+
let progressListener;
|
|
3557
3580
|
return {
|
|
3558
3581
|
modelName: EMBEDDING_MODEL_NAME,
|
|
3559
3582
|
get available() {
|
|
@@ -3562,6 +3585,9 @@ function createFastEmbedEngineImpl(cacheDir) {
|
|
|
3562
3585
|
get lastError() {
|
|
3563
3586
|
return lastError;
|
|
3564
3587
|
},
|
|
3588
|
+
setProgressListener(listener) {
|
|
3589
|
+
progressListener = listener;
|
|
3590
|
+
},
|
|
3565
3591
|
async embedBatch(texts) {
|
|
3566
3592
|
if (!this.available) return null;
|
|
3567
3593
|
if (texts.length === 0) return [];
|
|
@@ -3598,12 +3624,12 @@ function createFastEmbedEngineImpl(cacheDir) {
|
|
|
3598
3624
|
if (loadFailed || initPromise === null) {
|
|
3599
3625
|
loadFailed = false;
|
|
3600
3626
|
lastError = void 0;
|
|
3601
|
-
initPromise =
|
|
3627
|
+
initPromise = loadEmbeddingRuntime(cacheDir, (progress) => progressListener?.(progress));
|
|
3602
3628
|
}
|
|
3603
3629
|
embedder = await initPromise;
|
|
3604
3630
|
if (embedder === null) {
|
|
3605
3631
|
initPromise = null;
|
|
3606
|
-
lastError = "
|
|
3632
|
+
lastError = "embedding runtime returned an empty session";
|
|
3607
3633
|
return false;
|
|
3608
3634
|
}
|
|
3609
3635
|
loadFailed = false;
|
|
@@ -3618,42 +3644,157 @@ function createFastEmbedEngineImpl(cacheDir) {
|
|
|
3618
3644
|
}
|
|
3619
3645
|
};
|
|
3620
3646
|
}
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3647
|
+
/** Maximum tokens per text; matches the model's position-embedding limit. */
|
|
3648
|
+
const MAX_SEQ_LEN = 512;
|
|
3649
|
+
/** Texts per inference batch: 16 × 512 × 512 floats ≈ 16MB of activations. */
|
|
3650
|
+
const EMBED_BATCH_SIZE = 16;
|
|
3651
|
+
/** Threads for the WASM backend. One keeps resolution deterministic in packaged
|
|
3652
|
+
* builds; `SCREAM_CODE_EMBEDDING_THREADS=4` is the supported opt-in. */
|
|
3653
|
+
const EMBEDDING_THREADS_ENV = "SCREAM_CODE_EMBEDDING_THREADS";
|
|
3654
|
+
/**
|
|
3655
|
+
* Token-level truncation that keeps the sentence boundary tokens.
|
|
3656
|
+
*
|
|
3657
|
+
* The tokenizer package does not apply the truncation policy itself and the
|
|
3658
|
+
* model package ships no truncation config, so this is the only place it can
|
|
3659
|
+
* happen. Keeping the head (which starts with `[CLS]`) and re-appending the
|
|
3660
|
+
* trailing special token (`[SEP]`) mirrors what a HuggingFace pipeline would
|
|
3661
|
+
* produce; slicing the tail off blindly would feed the encoder a sentence that
|
|
3662
|
+
* never closes and drift from the reference vectors on long inputs.
|
|
3663
|
+
*/
|
|
3664
|
+
function truncateEncoded(ids, types) {
|
|
3665
|
+
if (ids.length <= MAX_SEQ_LEN) return {
|
|
3666
|
+
ids: [...ids],
|
|
3667
|
+
types: [...types]
|
|
3668
|
+
};
|
|
3669
|
+
const headSize = MAX_SEQ_LEN - 1;
|
|
3670
|
+
const head = ids.slice(0, headSize);
|
|
3671
|
+
const headTypes = types.slice(0, headSize);
|
|
3672
|
+
return {
|
|
3673
|
+
ids: [...head, ids.at(-1)],
|
|
3674
|
+
types: [...headTypes, types.at(-1)]
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
/**
|
|
3678
|
+
* Builds the embedding runtime in-process on `onnxruntime-web` (WASM) and the
|
|
3679
|
+
* pure-JS tokenizer. No native binaries, no install scripts: the whole stack is
|
|
3680
|
+
* JavaScript plus one WebAssembly artifact resolved from the package.
|
|
3681
|
+
*
|
|
3682
|
+
* The numeric pipeline is load-bearing and must stay compatible with the
|
|
3683
|
+
* reference embeddings of this model: token ids come from `tokenizer.json`, and
|
|
3684
|
+
* the sentence vector is the **CLS token** hidden state (`last_hidden_state[0]`)
|
|
3685
|
+
* followed by L2 normalization. Mean pooling produces visibly different vectors
|
|
3686
|
+
* (cosine ≈ 0.9 against the reference), so pooling is not a stylistic choice.
|
|
3687
|
+
*/
|
|
3688
|
+
async function createWasmRuntime(modelDir) {
|
|
3689
|
+
const ort = await import("onnxruntime-web");
|
|
3690
|
+
const { Tokenizer } = await import("@huggingface/tokenizers");
|
|
3691
|
+
const requestedThreads = Number(process.env[EMBEDDING_THREADS_ENV]);
|
|
3692
|
+
ort.env.wasm.numThreads = Number.isFinite(requestedThreads) && requestedThreads > 0 ? requestedThreads : 1;
|
|
3693
|
+
ort.env.logLevel = "error";
|
|
3694
|
+
const tokenizer = new Tokenizer(JSON.parse(readFileSync(join(modelDir, "tokenizer.json"), "utf8")), JSON.parse(readFileSync(join(modelDir, "tokenizer_config.json"), "utf8")));
|
|
3695
|
+
const session = await ort.InferenceSession.create(readFileSync(join(modelDir, "model_optimized.onnx")), { executionProviders: ["wasm"] });
|
|
3696
|
+
if (session === null || session === void 0) return null;
|
|
3697
|
+
return { async *embed(textStrings) {
|
|
3698
|
+
if (textStrings.length === 0) return;
|
|
3699
|
+
const encoded = textStrings.map((text, index) => {
|
|
3700
|
+
const encoding = tokenizer.encode(text, { return_token_type_ids: true });
|
|
3701
|
+
const types = encoding.token_type_ids ?? encoding.ids.map(() => 0);
|
|
3702
|
+
const truncated = truncateEncoded(encoding.ids, types);
|
|
3703
|
+
return {
|
|
3704
|
+
index,
|
|
3705
|
+
ids: truncated.ids,
|
|
3706
|
+
types: truncated.types
|
|
3707
|
+
};
|
|
3708
|
+
});
|
|
3709
|
+
const results = Array.from({ length: textStrings.length });
|
|
3710
|
+
const buckets = encoded.toSorted((a, b) => a.ids.length - b.ids.length);
|
|
3711
|
+
for (let start = 0; start < buckets.length; start += EMBED_BATCH_SIZE) {
|
|
3712
|
+
const group = buckets.slice(start, start + EMBED_BATCH_SIZE);
|
|
3713
|
+
const rows = group.length;
|
|
3714
|
+
const width = Math.max(...group.map((entry) => entry.ids.length));
|
|
3715
|
+
const ids = new BigInt64Array(rows * width);
|
|
3716
|
+
const mask = new BigInt64Array(rows * width);
|
|
3717
|
+
const types = new BigInt64Array(rows * width);
|
|
3718
|
+
group.forEach((entry, row) => {
|
|
3719
|
+
for (let column = 0; column < width; column++) {
|
|
3720
|
+
const offset = row * width + column;
|
|
3721
|
+
ids[offset] = BigInt(column < entry.ids.length ? entry.ids[column] : 0);
|
|
3722
|
+
mask[offset] = column < entry.ids.length ? 1n : 0n;
|
|
3723
|
+
types[offset] = BigInt(column < entry.types.length ? entry.types[column] : 0);
|
|
3724
|
+
}
|
|
3725
|
+
});
|
|
3726
|
+
const output = await session.run({
|
|
3727
|
+
input_ids: new ort.Tensor("int64", ids, [rows, width]),
|
|
3728
|
+
attention_mask: new ort.Tensor("int64", mask, [rows, width]),
|
|
3729
|
+
token_type_ids: new ort.Tensor("int64", types, [rows, width])
|
|
3730
|
+
});
|
|
3731
|
+
const hidden = output["last_hidden_state"] ?? output[Object.keys(output)[0]];
|
|
3732
|
+
const hiddenSize = hidden.dims.at(-1);
|
|
3733
|
+
const data = hidden.data;
|
|
3734
|
+
group.forEach((entry, row) => {
|
|
3735
|
+
const rowStart = row * width * hiddenSize;
|
|
3736
|
+
const vector = Array.from(data.subarray(rowStart, rowStart + hiddenSize));
|
|
3737
|
+
let norm = 0;
|
|
3738
|
+
for (const value of vector) norm += value * value;
|
|
3739
|
+
norm = Math.sqrt(norm) || 1;
|
|
3740
|
+
for (let i = 0; i < vector.length; i += 1) vector[i] = vector[i] / norm;
|
|
3741
|
+
results[entry.index] = vector;
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
for (let start = 0; start < results.length; start += EMBED_BATCH_SIZE) yield results.slice(start, start + EMBED_BATCH_SIZE);
|
|
3745
|
+
} };
|
|
3746
|
+
}
|
|
3747
|
+
async function loadEmbeddingRuntime(cacheDir, emit) {
|
|
3748
|
+
if (cacheDir === void 0) {
|
|
3749
|
+
emit?.({ phase: "load" });
|
|
3750
|
+
return createWasmRuntime(join("local_cache", BGESMALLZH_CACHE_NAME));
|
|
3639
3751
|
}
|
|
3752
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
3753
|
+
await ensureEmbeddingModelFiles(cacheDir, {
|
|
3754
|
+
onProgress: (received, total) => {
|
|
3755
|
+
emit?.(total === void 0 ? {
|
|
3756
|
+
phase: "download",
|
|
3757
|
+
received
|
|
3758
|
+
} : {
|
|
3759
|
+
phase: "download",
|
|
3760
|
+
received,
|
|
3761
|
+
total
|
|
3762
|
+
});
|
|
3763
|
+
},
|
|
3764
|
+
onPhase: (phase) => {
|
|
3765
|
+
if (phase === "verify") emit?.({ phase: "verify" });
|
|
3766
|
+
}
|
|
3767
|
+
});
|
|
3768
|
+
emit?.({ phase: "load" });
|
|
3769
|
+
return createWasmRuntime(join(cacheDir, BGESMALLZH_CACHE_NAME));
|
|
3640
3770
|
}
|
|
3641
3771
|
/**
|
|
3642
|
-
* Cache entry
|
|
3643
|
-
*
|
|
3772
|
+
* Cache entry name for the fixed BGESmallZH model: the staged archive extracts
|
|
3773
|
+
* to `<cacheDir>/<name>/` and the runtime loads from that directory.
|
|
3644
3774
|
*/
|
|
3645
3775
|
const BGESMALLZH_CACHE_NAME = "fast-bge-small-zh-v1.5";
|
|
3776
|
+
/** Files the runtime needs before the model can be considered installed. */
|
|
3777
|
+
const EMBEDDING_REQUIRED_FILES = [
|
|
3778
|
+
"model_optimized.onnx",
|
|
3779
|
+
"tokenizer.json",
|
|
3780
|
+
"tokenizer_config.json"
|
|
3781
|
+
];
|
|
3646
3782
|
/** Escape hatch for air-gapped installs and tests: points at an archive file. */
|
|
3647
3783
|
const EMBEDDING_MODEL_URL_ENV = "SCREAM_CODE_EMBEDDING_MODEL_URL";
|
|
3648
|
-
|
|
3649
|
-
|
|
3784
|
+
/** Default stall guards: a trickling connection must not spin a spinner forever. */
|
|
3785
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 6e4;
|
|
3786
|
+
const DEFAULT_TOTAL_TIMEOUT_MS = 15 * 6e4;
|
|
3787
|
+
/** Progress callback throttle: frequent enough to move, rare enough to be free. */
|
|
3788
|
+
const PROGRESS_THROTTLE_MS = 100;
|
|
3789
|
+
function modelDirPath(cacheDir) {
|
|
3790
|
+
return join(cacheDir, BGESMALLZH_CACHE_NAME);
|
|
3650
3791
|
}
|
|
3651
3792
|
function sha256File(filePath) {
|
|
3652
3793
|
return readFile(filePath).then((data) => createHash("sha256").update(data).digest("hex"));
|
|
3653
3794
|
}
|
|
3654
3795
|
/**
|
|
3655
3796
|
* Stage the embedding model files when they are missing: download the pinned
|
|
3656
|
-
* archive (HTTP status verified
|
|
3797
|
+
* archive (HTTP status verified, unlike a naive downloader), extract,
|
|
3657
3798
|
* and verify the weights payload against the pinned sha256.
|
|
3658
3799
|
*
|
|
3659
3800
|
* A partially extracted tree or a hash mismatch is removed so the caller keeps
|
|
@@ -3663,62 +3804,149 @@ function sha256File(filePath) {
|
|
|
3663
3804
|
async function ensureEmbeddingModelFiles(cacheDir, options) {
|
|
3664
3805
|
if (hasEmbeddingModelCache(cacheDir)) return;
|
|
3665
3806
|
mkdirSync(cacheDir, { recursive: true });
|
|
3666
|
-
const modelDir =
|
|
3807
|
+
const modelDir = modelDirPath(cacheDir);
|
|
3667
3808
|
const stagingPath = join(cacheDir, `${BGESMALLZH_CACHE_NAME}.tar.gz.part`);
|
|
3668
3809
|
const url = options?.sourceUrl ?? process.env[EMBEDDING_MODEL_URL_ENV] ?? "https://github.com/LIUTod/scream-code/releases/download/embedding-models-v1/fast-bge-small-zh-v1.5.tar.gz";
|
|
3810
|
+
const idleTimeoutMs = options?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
3811
|
+
const totalTimeoutMs = options?.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS;
|
|
3812
|
+
const controller = new AbortController();
|
|
3813
|
+
const onExternalAbort = () => {
|
|
3814
|
+
controller.abort();
|
|
3815
|
+
};
|
|
3816
|
+
if (options?.signal !== void 0) if (options.signal.aborted) controller.abort();
|
|
3817
|
+
else options.signal.addEventListener("abort", onExternalAbort, { once: true });
|
|
3818
|
+
let idleTimer;
|
|
3819
|
+
const armIdleTimer = () => {
|
|
3820
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
3821
|
+
idleTimer = setTimeout(() => {
|
|
3822
|
+
controller.abort();
|
|
3823
|
+
}, idleTimeoutMs);
|
|
3824
|
+
};
|
|
3825
|
+
const totalTimer = setTimeout(() => {
|
|
3826
|
+
controller.abort();
|
|
3827
|
+
}, totalTimeoutMs);
|
|
3828
|
+
let received = 0;
|
|
3829
|
+
let announcedTotal;
|
|
3830
|
+
let downloadCompleted = false;
|
|
3831
|
+
const announceProgress = () => {
|
|
3832
|
+
options?.onProgress?.(received, announcedTotal);
|
|
3833
|
+
};
|
|
3669
3834
|
try {
|
|
3670
3835
|
let response;
|
|
3671
3836
|
try {
|
|
3672
|
-
|
|
3837
|
+
armIdleTimer();
|
|
3838
|
+
response = await fetch(url, { signal: controller.signal });
|
|
3673
3839
|
} catch (error) {
|
|
3674
3840
|
const message = error instanceof Error ? error.message : String(error);
|
|
3675
3841
|
throw new Error(`fetch failed for ${url}: ${message}`, { cause: error });
|
|
3676
3842
|
}
|
|
3677
3843
|
if (!response.ok) throw new Error(`model source returned HTTP ${response.status} for ${url}`);
|
|
3678
|
-
|
|
3844
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
3845
|
+
announcedTotal = Number.isFinite(contentLength) && contentLength > 0 ? contentLength : void 0;
|
|
3846
|
+
announceProgress();
|
|
3847
|
+
const body = response.body;
|
|
3848
|
+
if (body === null) throw new Error(`model source returned an empty body for ${url}`);
|
|
3849
|
+
const reader = body.getReader();
|
|
3850
|
+
const abortRejection = new Promise((_, reject) => {
|
|
3851
|
+
if (controller.signal.aborted) {
|
|
3852
|
+
reject(/* @__PURE__ */ new Error("aborted"));
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
controller.signal.addEventListener("abort", () => {
|
|
3856
|
+
reject(/* @__PURE__ */ new Error("aborted"));
|
|
3857
|
+
}, { once: true });
|
|
3858
|
+
});
|
|
3859
|
+
const abortAwareRead = () => Promise.race([reader.read(), abortRejection]);
|
|
3860
|
+
let handle;
|
|
3861
|
+
try {
|
|
3862
|
+
handle = await open(stagingPath, "w");
|
|
3863
|
+
} catch (error) {
|
|
3864
|
+
await reader.cancel().catch(() => void 0);
|
|
3865
|
+
throw error;
|
|
3866
|
+
}
|
|
3867
|
+
let lastProgressAt = 0;
|
|
3868
|
+
try {
|
|
3869
|
+
await (async () => {
|
|
3870
|
+
for (;;) {
|
|
3871
|
+
let chunk;
|
|
3872
|
+
try {
|
|
3873
|
+
chunk = await abortAwareRead();
|
|
3874
|
+
} catch (error) {
|
|
3875
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3876
|
+
throw new Error(`${message} (received ${received} of ${announcedTotal ?? "?"} bytes): ${url}`, { cause: error });
|
|
3877
|
+
}
|
|
3878
|
+
if (chunk.done) break;
|
|
3879
|
+
const bytes = chunk.value;
|
|
3880
|
+
armIdleTimer();
|
|
3881
|
+
await handle.write(bytes);
|
|
3882
|
+
received += bytes.byteLength;
|
|
3883
|
+
const now = Date.now();
|
|
3884
|
+
if (now - lastProgressAt >= PROGRESS_THROTTLE_MS) {
|
|
3885
|
+
lastProgressAt = now;
|
|
3886
|
+
announceProgress();
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
announceProgress();
|
|
3890
|
+
downloadCompleted = true;
|
|
3891
|
+
})();
|
|
3892
|
+
} finally {
|
|
3893
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
3894
|
+
await handle.close();
|
|
3895
|
+
await reader.cancel().catch(() => void 0);
|
|
3896
|
+
}
|
|
3897
|
+
options?.onPhase?.("verify");
|
|
3679
3898
|
await So({
|
|
3680
3899
|
file: stagingPath,
|
|
3681
3900
|
cwd: cacheDir
|
|
3682
3901
|
});
|
|
3683
|
-
const
|
|
3684
|
-
|
|
3685
|
-
|
|
3902
|
+
for (const fileName of EMBEDDING_REQUIRED_FILES) {
|
|
3903
|
+
const target = join(modelDir, fileName);
|
|
3904
|
+
if (!existsSync(target) || statSync(target).size === 0) throw new Error(`model package is missing ${fileName} after extraction: ${url}`);
|
|
3905
|
+
}
|
|
3906
|
+
const sha256 = await sha256File(join(modelDir, "model_optimized.onnx"));
|
|
3686
3907
|
const expected = options?.expectedOnnxSha256 ?? "1294ea4b6331115a353d81f96b85e8c8d7fdcc284453d5b2fab5b016230aad38";
|
|
3687
3908
|
if (sha256 !== expected) throw new Error(`model weights sha256 mismatch (${sha256.slice(0, 12)}… != ${expected.slice(0, 12)}…): ${url}`);
|
|
3688
3909
|
} catch (error) {
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3910
|
+
try {
|
|
3911
|
+
rmSync(modelDir, {
|
|
3912
|
+
recursive: true,
|
|
3913
|
+
force: true
|
|
3914
|
+
});
|
|
3915
|
+
} catch {}
|
|
3916
|
+
if (!downloadCompleted && controller.signal.aborted && !(options?.signal?.aborted ?? false)) throw new Error(`model download timed out (received ${received} of ${announcedTotal ?? "?"} bytes): ${url}`, { cause: error });
|
|
3917
|
+
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
3694
3918
|
} finally {
|
|
3919
|
+
clearTimeout(totalTimer);
|
|
3920
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
3921
|
+
if (options?.signal !== void 0) options.signal.removeEventListener("abort", onExternalAbort);
|
|
3695
3922
|
rmSync(stagingPath, { force: true });
|
|
3696
3923
|
}
|
|
3697
3924
|
}
|
|
3698
|
-
const
|
|
3699
|
-
const ARCHIVE_FAILURE = /\bTAR_[A-Z_]+|incorrect header check|unexpected end of file|invalid tar|zlib/iu;
|
|
3700
|
-
const NETWORK_FAILURE = /\b(?:ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|socket hang up|fetch failed)\b|network error|HTTP \d{3}/iu;
|
|
3925
|
+
const INSTALL_FAILURE = /Cannot find (?:package|module) '(?:onnxruntime-web|@huggingface\/tokenizers)'|ERR_MODULE_NOT_FOUND|Failed to resolve module specifier/iu;
|
|
3926
|
+
const ARCHIVE_FAILURE = /\bTAR_[A-Z_]+|incorrect header check|unexpected end of file|invalid tar|zlib|after extraction|sha256 mismatch/iu;
|
|
3927
|
+
const NETWORK_FAILURE = /\b(?:ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|socket hang up|fetch failed)\b|network error|HTTP \d{3}|timed out|empty body/iu;
|
|
3701
3928
|
/**
|
|
3702
|
-
* Classify a load failure so callers can say something true about it:
|
|
3703
|
-
*
|
|
3704
|
-
*
|
|
3929
|
+
* Classify a load failure so callers can say something true about it: an
|
|
3930
|
+
* incomplete install, a corrupt cache, a network problem and anything else need
|
|
3931
|
+
* four different answers, and they used to be reported as one network problem.
|
|
3705
3932
|
*/
|
|
3706
3933
|
function classifyEmbeddingFailure(message) {
|
|
3707
3934
|
if (message === void 0) return "other";
|
|
3708
|
-
if (
|
|
3935
|
+
if (INSTALL_FAILURE.test(message)) return "install";
|
|
3709
3936
|
if (ARCHIVE_FAILURE.test(message)) return "archive";
|
|
3710
3937
|
if (NETWORK_FAILURE.test(message)) return "network";
|
|
3711
3938
|
return "other";
|
|
3712
3939
|
}
|
|
3713
3940
|
/**
|
|
3714
|
-
* Loads
|
|
3715
|
-
*
|
|
3716
|
-
* failed".
|
|
3717
|
-
*
|
|
3941
|
+
* Loads both runtime packages — no model download — so callers can tell "this
|
|
3942
|
+
* installation cannot run the local model at all" apart from "the download
|
|
3943
|
+
* failed". The runtime is JavaScript plus WebAssembly, so a failure here is an
|
|
3944
|
+
* incomplete installation rather than an unsupported platform.
|
|
3718
3945
|
*/
|
|
3719
3946
|
async function probeLocalEmbeddingSupport() {
|
|
3720
3947
|
try {
|
|
3721
|
-
await import("
|
|
3948
|
+
await import("onnxruntime-web");
|
|
3949
|
+
await import("@huggingface/tokenizers");
|
|
3722
3950
|
return { supported: true };
|
|
3723
3951
|
} catch (error) {
|
|
3724
3952
|
return {
|
|
@@ -3729,15 +3957,11 @@ async function probeLocalEmbeddingSupport() {
|
|
|
3729
3957
|
}
|
|
3730
3958
|
/**
|
|
3731
3959
|
* Remove every trace of a previous BGESmallZH download: the extracted model
|
|
3732
|
-
* directory
|
|
3960
|
+
* directory and the legacy staged archive name.
|
|
3733
3961
|
*
|
|
3734
|
-
* The
|
|
3735
|
-
*
|
|
3736
|
-
*
|
|
3737
|
-
* error) reject and leave whatever was written behind. So an archive sitting
|
|
3738
|
-
* next to a failed attempt is usually a partial leftover, and keeping it makes
|
|
3739
|
-
* every retry extract the same junk locally: no network traffic, a few
|
|
3740
|
-
* milliseconds, the same failure forever.
|
|
3962
|
+
* The legacy `<name>.tar.gz` file is still swept because older versions staged
|
|
3963
|
+
* the archive under that name; leaving it behind would waste 55MB per doomed
|
|
3964
|
+
* attempt. Our own downloader stages `.tar.gz.part` and always removes it.
|
|
3741
3965
|
*
|
|
3742
3966
|
* Never throws: a file locked by another process (on Windows a mapped ONNX file
|
|
3743
3967
|
* stays locked) must not replace the download error the caller is reporting.
|
|
@@ -3755,48 +3979,19 @@ function clearEmbeddingModelCache(cacheDir) {
|
|
|
3755
3979
|
}
|
|
3756
3980
|
}
|
|
3757
3981
|
/**
|
|
3758
|
-
* True when the
|
|
3759
|
-
*
|
|
3760
|
-
*
|
|
3761
|
-
*
|
|
3982
|
+
* True when every file the runtime needs is already on disk. Loading then never
|
|
3983
|
+
* downloads, so callers can safely trigger a local load without risking a
|
|
3984
|
+
* surprise mid-query download. All required files are checked (not just the
|
|
3985
|
+
* weights): a partially restored cache must count as missing so the setup flow
|
|
3986
|
+
* repairs it instead of failing at load time.
|
|
3762
3987
|
*/
|
|
3763
3988
|
function hasEmbeddingModelCache(cacheDir) {
|
|
3764
|
-
const modelDir =
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
/**
|
|
3768
|
-
* Small config/tokenizer files that fastembed expects alongside model.onnx.
|
|
3769
|
-
* If these are missing (e.g. GCS download partially failed), fastembed throws.
|
|
3770
|
-
* We download them from HuggingFace so the model can load — this covers the
|
|
3771
|
-
* case where the GCS tarball was incomplete but the HF repo is reachable.
|
|
3772
|
-
*/
|
|
3773
|
-
const FASTEMBED_SIDECARS = [
|
|
3774
|
-
"config.json",
|
|
3775
|
-
"tokenizer.json",
|
|
3776
|
-
"tokenizer_config.json",
|
|
3777
|
-
"special_tokens_map.json"
|
|
3778
|
-
];
|
|
3779
|
-
const FASTEMBED_HF_REPOS = { "fast-bge-small-zh-v1.5": "BAAI/bge-small-zh-v1.5" };
|
|
3780
|
-
async function ensureFastembedModelSidecars(model, cacheDir) {
|
|
3781
|
-
const repo = FASTEMBED_HF_REPOS[model];
|
|
3782
|
-
if (repo === void 0) return;
|
|
3783
|
-
const modelDir = join(cacheDir ?? "local_cache", model);
|
|
3784
|
-
mkdirSync(modelDir, { recursive: true });
|
|
3785
|
-
for (const fileName of FASTEMBED_SIDECARS) {
|
|
3989
|
+
const modelDir = modelDirPath(cacheDir);
|
|
3990
|
+
if (!existsSync(modelDir)) return false;
|
|
3991
|
+
return EMBEDDING_REQUIRED_FILES.every((fileName) => {
|
|
3786
3992
|
const target = join(modelDir, fileName);
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
await access(target);
|
|
3790
|
-
continue;
|
|
3791
|
-
} catch {}
|
|
3792
|
-
const hfUrl = `https://huggingface.co/${repo}/resolve/main/${fileName}`;
|
|
3793
|
-
try {
|
|
3794
|
-
const response = await fetch(hfUrl);
|
|
3795
|
-
if (!response.ok) continue;
|
|
3796
|
-
const { writeFile } = await import("node:fs/promises");
|
|
3797
|
-
await writeFile(target, Buffer.from(await response.arrayBuffer()));
|
|
3798
|
-
} catch {}
|
|
3799
|
-
}
|
|
3993
|
+
return existsSync(target) && statSync(target).size > 0;
|
|
3994
|
+
});
|
|
3800
3995
|
}
|
|
3801
3996
|
//#endregion
|
|
3802
3997
|
//#region ../../packages/memory/src/store.ts
|
|
@@ -3940,7 +4135,19 @@ var MemoryMemoStore = class MemoryMemoStore {
|
|
|
3940
4135
|
JOIN memos_fts f ON m.rowid = f.rowid
|
|
3941
4136
|
WHERE f.memos_fts MATCH ? AND (m.project_dir = ? OR m.project_dir = '')
|
|
3942
4137
|
ORDER BY m.recorded_at DESC LIMIT ?`);
|
|
3943
|
-
|
|
4138
|
+
const rows = projectDir === void 0 ? stmt.all(ftsQuery, limit) : stmt.all(ftsQuery, projectDir, limit);
|
|
4139
|
+
const like = likePattern(query);
|
|
4140
|
+
const remaining = Math.max(0, limit - rows.length);
|
|
4141
|
+
const extraRows = remaining === 0 ? [] : projectDir === void 0 ? this.db.prepare("SELECT * FROM memos WHERE distilled LIKE ? ESCAPE '\\' OR note LIKE ? ESCAPE '\\' ORDER BY recorded_at DESC LIMIT ?").all(like, like, remaining) : this.db.prepare("SELECT * FROM memos WHERE (distilled LIKE ? ESCAPE '\\' OR note LIKE ? ESCAPE '\\') AND (project_dir = ? OR project_dir = '') ORDER BY recorded_at DESC LIMIT ?").all(like, like, projectDir, remaining);
|
|
4142
|
+
const seen = new Set(rows.map((row) => String(row["id"])));
|
|
4143
|
+
const merged = rows.map(rowToMemo);
|
|
4144
|
+
for (const row of extraRows) {
|
|
4145
|
+
const id = String(row["id"]);
|
|
4146
|
+
if (seen.has(id)) continue;
|
|
4147
|
+
seen.add(id);
|
|
4148
|
+
merged.push(rowToMemo(row));
|
|
4149
|
+
}
|
|
4150
|
+
return merged;
|
|
3944
4151
|
}
|
|
3945
4152
|
/** List memos with optional full-text search and pagination. */
|
|
3946
4153
|
async list(options) {
|
|
@@ -4082,7 +4289,12 @@ var MemoryMemoStore = class MemoryMemoStore {
|
|
|
4082
4289
|
extraction_source TEXT NOT NULL CHECK(extraction_source IN ('compaction', 'exit', 'manual')),
|
|
4083
4290
|
recorded_at INTEGER NOT NULL,
|
|
4084
4291
|
project_dir TEXT NOT NULL DEFAULT '',
|
|
4085
|
-
tags TEXT NOT NULL DEFAULT '[]'
|
|
4292
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
4293
|
+
note TEXT,
|
|
4294
|
+
distilled TEXT NOT NULL DEFAULT '',
|
|
4295
|
+
origin TEXT NOT NULL DEFAULT 'session',
|
|
4296
|
+
distilled_from TEXT,
|
|
4297
|
+
distilled_at INTEGER
|
|
4086
4298
|
);
|
|
4087
4299
|
|
|
4088
4300
|
CREATE INDEX IF NOT EXISTS idx_memos_project_dir ON memos(project_dir);
|
|
@@ -4112,6 +4324,14 @@ var MemoryMemoStore = class MemoryMemoStore {
|
|
|
4112
4324
|
const info = this.db.prepare("PRAGMA table_info(memos)").all();
|
|
4113
4325
|
if (!info.some((col) => col.name === "project_dir")) this.db.exec("ALTER TABLE memos ADD COLUMN project_dir TEXT NOT NULL DEFAULT ''");
|
|
4114
4326
|
if (!info.some((col) => col.name === "tags")) this.db.exec("ALTER TABLE memos ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'");
|
|
4327
|
+
if (!info.some((col) => col.name === "note")) this.db.exec("ALTER TABLE memos ADD COLUMN note TEXT");
|
|
4328
|
+
if (!info.some((col) => col.name === "distilled")) this.db.exec("ALTER TABLE memos ADD COLUMN distilled TEXT NOT NULL DEFAULT ''");
|
|
4329
|
+
if (!info.some((col) => col.name === "origin")) {
|
|
4330
|
+
this.db.exec("ALTER TABLE memos ADD COLUMN origin TEXT NOT NULL DEFAULT 'session'");
|
|
4331
|
+
this.db.exec("UPDATE memos SET origin = 'dream' WHERE distilled <> ''");
|
|
4332
|
+
}
|
|
4333
|
+
if (!info.some((col) => col.name === "distilled_from")) this.db.exec("ALTER TABLE memos ADD COLUMN distilled_from TEXT");
|
|
4334
|
+
if (!info.some((col) => col.name === "distilled_at")) this.db.exec("ALTER TABLE memos ADD COLUMN distilled_at INTEGER");
|
|
4115
4335
|
this.db.exec(`
|
|
4116
4336
|
CREATE INDEX IF NOT EXISTS idx_memos_project_dir ON memos(project_dir);
|
|
4117
4337
|
CREATE INDEX IF NOT EXISTS idx_memory_embeddings_created_at ON memory_embeddings(created_at DESC);
|
|
@@ -4155,15 +4375,16 @@ var MemoryMemoStore = class MemoryMemoStore {
|
|
|
4155
4375
|
if (this.db === void 0 || memos.length === 0) return;
|
|
4156
4376
|
const insert = this.db.prepare(`INSERT OR IGNORE INTO memos (
|
|
4157
4377
|
id, source_session_id, source_session_title, user_need, approach,
|
|
4158
|
-
outcome, what_failed, what_worked, extraction_source, recorded_at, project_dir, tags
|
|
4159
|
-
|
|
4378
|
+
outcome, what_failed, what_worked, extraction_source, recorded_at, project_dir, tags,
|
|
4379
|
+
note, distilled, origin, distilled_from, distilled_at
|
|
4380
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4160
4381
|
RETURNING rowid`);
|
|
4161
4382
|
const insertFts = this.db.prepare(`INSERT INTO memos_fts(rowid, user_need, approach, what_failed, what_worked, source_session_title)
|
|
4162
4383
|
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
4163
4384
|
this.db.exec("BEGIN TRANSACTION");
|
|
4164
4385
|
try {
|
|
4165
4386
|
for (const memo of memos) {
|
|
4166
|
-
const row = insert.get(memo.id, memo.sourceSessionId, memo.sourceSessionTitle ?? null, memo.userNeed, memo.approach, memo.outcome, memo.whatFailed, memo.whatWorked, memo.extractionSource, memo.recordedAt, memo.projectDir ?? "", JSON.stringify(memo.tags ?? []));
|
|
4387
|
+
const row = insert.get(memo.id, memo.sourceSessionId, memo.sourceSessionTitle ?? null, memo.userNeed, memo.approach, memo.outcome, memo.whatFailed, memo.whatWorked, memo.extractionSource, memo.recordedAt, memo.projectDir ?? "", JSON.stringify(memo.tags ?? []), memo.note ?? null, memo.distilled ?? "", memo.origin ?? "session", stringifyDistilledFrom(memo.distilledFrom), memo.distilledAt ?? null);
|
|
4167
4388
|
if (row === void 0) continue;
|
|
4168
4389
|
insertFts.run(row.rowid, toFtsText$1(memo.userNeed), toFtsText$1(memo.approach), toFtsText$1(memo.whatFailed), toFtsText$1(memo.whatWorked), toFtsText$1(memo.sourceSessionTitle ?? ""));
|
|
4169
4390
|
}
|
|
@@ -4178,14 +4399,15 @@ var MemoryMemoStore = class MemoryMemoStore {
|
|
|
4178
4399
|
if (this.db === void 0) return;
|
|
4179
4400
|
const insert = this.db.prepare(`INSERT INTO memos (
|
|
4180
4401
|
id, source_session_id, source_session_title, user_need, approach,
|
|
4181
|
-
outcome, what_failed, what_worked, extraction_source, recorded_at, project_dir, tags
|
|
4182
|
-
|
|
4402
|
+
outcome, what_failed, what_worked, extraction_source, recorded_at, project_dir, tags,
|
|
4403
|
+
note, distilled, origin, distilled_from, distilled_at
|
|
4404
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4183
4405
|
RETURNING rowid`);
|
|
4184
4406
|
const insertFts = this.db.prepare(`INSERT INTO memos_fts(rowid, user_need, approach, what_failed, what_worked, source_session_title)
|
|
4185
4407
|
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
4186
4408
|
this.db.exec("BEGIN TRANSACTION");
|
|
4187
4409
|
try {
|
|
4188
|
-
const row = insert.get(entry.id, entry.sourceSessionId, entry.sourceSessionTitle ?? null, entry.userNeed, entry.approach, entry.outcome, entry.whatFailed, entry.whatWorked, entry.extractionSource, entry.recordedAt, entry.projectDir ?? "", JSON.stringify(entry.tags ?? []));
|
|
4410
|
+
const row = insert.get(entry.id, entry.sourceSessionId, entry.sourceSessionTitle ?? null, entry.userNeed, entry.approach, entry.outcome, entry.whatFailed, entry.whatWorked, entry.extractionSource, entry.recordedAt, entry.projectDir ?? "", JSON.stringify(entry.tags ?? []), entry.note ?? null, entry.distilled ?? "", entry.origin ?? "session", stringifyDistilledFrom(entry.distilledFrom), entry.distilledAt ?? null);
|
|
4189
4411
|
insertFts.run(row.rowid, toFtsText$1(entry.userNeed), toFtsText$1(entry.approach), toFtsText$1(entry.whatFailed), toFtsText$1(entry.whatWorked), toFtsText$1(entry.sourceSessionTitle ?? ""));
|
|
4190
4412
|
this.db.exec("COMMIT");
|
|
4191
4413
|
this.scheduleEmbedding(entry);
|
|
@@ -4221,7 +4443,12 @@ var MemoryMemoStore = class MemoryMemoStore {
|
|
|
4221
4443
|
extraction_source = ?,
|
|
4222
4444
|
recorded_at = ?,
|
|
4223
4445
|
project_dir = ?,
|
|
4224
|
-
tags =
|
|
4446
|
+
tags = ?,
|
|
4447
|
+
note = ?,
|
|
4448
|
+
distilled = ?,
|
|
4449
|
+
origin = ?,
|
|
4450
|
+
distilled_from = ?,
|
|
4451
|
+
distilled_at = ?
|
|
4225
4452
|
WHERE id = ?
|
|
4226
4453
|
RETURNING rowid`);
|
|
4227
4454
|
const updateFts = this.db.prepare(`INSERT INTO memos_fts(rowid, user_need, approach, what_failed, what_worked, source_session_title)
|
|
@@ -4235,7 +4462,7 @@ var MemoryMemoStore = class MemoryMemoStore {
|
|
|
4235
4462
|
this.db.exec("ROLLBACK");
|
|
4236
4463
|
return false;
|
|
4237
4464
|
}
|
|
4238
|
-
const row = update.get(updated.sourceSessionId, updated.sourceSessionTitle ?? null, updated.userNeed, updated.approach, updated.outcome, updated.whatFailed, updated.whatWorked, updated.extractionSource, updated.recordedAt, updated.projectDir ?? "", JSON.stringify(updated.tags ?? []), id);
|
|
4465
|
+
const row = update.get(updated.sourceSessionId, updated.sourceSessionTitle ?? null, updated.userNeed, updated.approach, updated.outcome, updated.whatFailed, updated.whatWorked, updated.extractionSource, updated.recordedAt, updated.projectDir ?? "", JSON.stringify(updated.tags ?? []), updated.note ?? null, updated.distilled ?? "", updated.origin ?? "session", stringifyDistilledFrom(updated.distilledFrom), updated.distilledAt ?? null, id);
|
|
4239
4466
|
if (row === void 0) {
|
|
4240
4467
|
this.db.exec("ROLLBACK");
|
|
4241
4468
|
return false;
|
|
@@ -4483,6 +4710,9 @@ var MemoryMemoStore = class MemoryMemoStore {
|
|
|
4483
4710
|
function rowToMemo(row) {
|
|
4484
4711
|
const sourceSessionTitle = row["source_session_title"];
|
|
4485
4712
|
const projectDir = row["project_dir"];
|
|
4713
|
+
const distilled = row["distilled"];
|
|
4714
|
+
const note = row["note"];
|
|
4715
|
+
const distilledAt = row["distilled_at"];
|
|
4486
4716
|
return {
|
|
4487
4717
|
id: String(row["id"]),
|
|
4488
4718
|
sourceSessionId: String(row["source_session_id"]),
|
|
@@ -4492,12 +4722,36 @@ function rowToMemo(row) {
|
|
|
4492
4722
|
outcome: String(row["outcome"]),
|
|
4493
4723
|
whatFailed: String(row["what_failed"]),
|
|
4494
4724
|
whatWorked: String(row["what_worked"]),
|
|
4725
|
+
distilled: typeof distilled === "string" && distilled.trim().length > 0 ? distilled : void 0,
|
|
4726
|
+
origin: parseOrigin(row["origin"]),
|
|
4727
|
+
distilledFrom: parseDistilledFrom(row["distilled_from"]),
|
|
4728
|
+
distilledAt: typeof distilledAt === "number" && Number.isFinite(distilledAt) ? distilledAt : void 0,
|
|
4495
4729
|
extractionSource: row["extraction_source"],
|
|
4496
4730
|
recordedAt: Number(row["recorded_at"]),
|
|
4497
4731
|
projectDir: typeof projectDir === "string" ? projectDir : "",
|
|
4498
|
-
tags: parseTags(row["tags"])
|
|
4732
|
+
tags: parseTags(row["tags"]),
|
|
4733
|
+
note: typeof note === "string" && note.length > 0 ? note : void 0
|
|
4499
4734
|
};
|
|
4500
4735
|
}
|
|
4736
|
+
function parseOrigin(value) {
|
|
4737
|
+
return value === "dream" || value === "user" || value === "session" ? value : "session";
|
|
4738
|
+
}
|
|
4739
|
+
/** Column value for `distilled_from`: undefined when there is nothing to store. */
|
|
4740
|
+
function stringifyDistilledFrom(sources) {
|
|
4741
|
+
return sources === void 0 || sources.length === 0 ? null : JSON.stringify(sources);
|
|
4742
|
+
}
|
|
4743
|
+
/** Parse the `distilled_from` JSON column, dropping entries that fail validation. */
|
|
4744
|
+
function parseDistilledFrom(value) {
|
|
4745
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
4746
|
+
try {
|
|
4747
|
+
const parsed = JSON.parse(value);
|
|
4748
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
4749
|
+
const sources = parsed.filter((item) => typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.title === "string" && typeof item.at === "number" && Number.isFinite(item.at));
|
|
4750
|
+
return sources.length > 0 ? sources.slice(0, 100) : void 0;
|
|
4751
|
+
} catch {
|
|
4752
|
+
return;
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4501
4755
|
function parseTags(value) {
|
|
4502
4756
|
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
4503
4757
|
try {
|
|
@@ -4509,12 +4763,17 @@ function parseTags(value) {
|
|
|
4509
4763
|
return;
|
|
4510
4764
|
}
|
|
4511
4765
|
}
|
|
4766
|
+
function likePattern(value) {
|
|
4767
|
+
return `%${value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
|
|
4768
|
+
}
|
|
4512
4769
|
function memoMatchesSearch(memo, search) {
|
|
4513
4770
|
return [
|
|
4514
4771
|
memo.userNeed,
|
|
4515
4772
|
memo.approach,
|
|
4516
4773
|
memo.whatFailed,
|
|
4517
4774
|
memo.whatWorked,
|
|
4775
|
+
memo.distilled ?? "",
|
|
4776
|
+
memo.note ?? "",
|
|
4518
4777
|
memo.sourceSessionTitle ?? "",
|
|
4519
4778
|
...memo.tags ?? []
|
|
4520
4779
|
].join(" ").toLowerCase().replaceAll(/\s+/g, "").includes(search.replaceAll(/\s+/g, ""));
|
|
@@ -4688,6 +4947,7 @@ async function buildConsolidationPlan(store, options) {
|
|
|
4688
4947
|
async function applyConsolidation(store, plan) {
|
|
4689
4948
|
let deleted = 0;
|
|
4690
4949
|
let created = 0;
|
|
4950
|
+
let skipped = 0;
|
|
4691
4951
|
if (plan.resolved.length > 0) {
|
|
4692
4952
|
const worked = plan.resolved.filter((m) => m.whatWorked).map((m) => m.whatWorked).join("; ");
|
|
4693
4953
|
const failed = plan.resolved.filter((m) => m.whatFailed).map((m) => m.whatFailed).join("; ");
|
|
@@ -4726,9 +4986,22 @@ async function applyConsolidation(store, plan) {
|
|
|
4726
4986
|
created++;
|
|
4727
4987
|
}
|
|
4728
4988
|
}
|
|
4989
|
+
const mergeableGroups = [];
|
|
4729
4990
|
for (const group of plan.duplicateGroups) {
|
|
4991
|
+
const distilledText = (group.merged.distilled ?? "").trim();
|
|
4992
|
+
if (distilledText.length === 0 && group.memos.some(isDistilledRule)) {
|
|
4993
|
+
skipped++;
|
|
4994
|
+
continue;
|
|
4995
|
+
}
|
|
4996
|
+
mergeableGroups.push({
|
|
4997
|
+
group,
|
|
4998
|
+
distilledText
|
|
4999
|
+
});
|
|
5000
|
+
}
|
|
5001
|
+
for (const { group, distilledText } of mergeableGroups) {
|
|
4730
5002
|
const newest = group.memos.reduce((a, b) => a.recordedAt > b.recordedAt ? a : b);
|
|
4731
5003
|
const mergedTags = normalizeTags(group.memos.flatMap((m) => m.tags ?? []));
|
|
5004
|
+
const mergedNotes = [...new Set(group.memos.map((memo) => memo.note?.trim() ?? "").filter((note) => note.length > 0))];
|
|
4732
5005
|
const merged = createMemoryMemo({
|
|
4733
5006
|
sourceSessionId: newest.sourceSessionId,
|
|
4734
5007
|
sourceSessionTitle: newest.sourceSessionTitle,
|
|
@@ -4737,14 +5010,19 @@ async function applyConsolidation(store, plan) {
|
|
|
4737
5010
|
outcome: group.merged.outcome,
|
|
4738
5011
|
whatFailed: group.merged.whatFailed,
|
|
4739
5012
|
whatWorked: group.merged.whatWorked,
|
|
5013
|
+
note: mergedNotes.length > 0 ? mergedNotes.join("; ") : void 0,
|
|
4740
5014
|
tags: group.merged.tags ?? mergedTags,
|
|
5015
|
+
distilled: distilledText.length > 0 ? distilledText : void 0,
|
|
4741
5016
|
projectDir: newest.projectDir,
|
|
4742
|
-
extractionSource: "compaction"
|
|
5017
|
+
extractionSource: "compaction",
|
|
5018
|
+
origin: "dream",
|
|
5019
|
+
distilledAt: distilledText.length > 0 ? Date.now() : void 0,
|
|
5020
|
+
distilledFrom: collectLineage(group.memos)
|
|
4743
5021
|
});
|
|
4744
5022
|
await store.append(merged);
|
|
4745
5023
|
created++;
|
|
4746
5024
|
}
|
|
4747
|
-
for (const group of
|
|
5025
|
+
for (const { group } of mergeableGroups) for (const memo of group.memos) {
|
|
4748
5026
|
await store.delete(memo.id);
|
|
4749
5027
|
deleted++;
|
|
4750
5028
|
}
|
|
@@ -4758,9 +5036,31 @@ async function applyConsolidation(store, plan) {
|
|
|
4758
5036
|
}
|
|
4759
5037
|
return {
|
|
4760
5038
|
deleted,
|
|
4761
|
-
created
|
|
5039
|
+
created,
|
|
5040
|
+
skipped
|
|
4762
5041
|
};
|
|
4763
5042
|
}
|
|
5043
|
+
/**
|
|
5044
|
+
* Build the lineage snapshot for a merged memo: every source memo in the group
|
|
5045
|
+
* plus each source's own lineage, deduped by id so a rule distilled from other
|
|
5046
|
+
* rules keeps its whole ancestry. Sorted oldest-first and capped at 100.
|
|
5047
|
+
*/
|
|
5048
|
+
function collectLineage(memos) {
|
|
5049
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5050
|
+
for (const memo of memos) {
|
|
5051
|
+
if (!seen.has(memo.id)) seen.set(memo.id, {
|
|
5052
|
+
id: memo.id,
|
|
5053
|
+
title: clip(memo.userNeed, 80),
|
|
5054
|
+
at: memo.recordedAt
|
|
5055
|
+
});
|
|
5056
|
+
for (const source of memo.distilledFrom ?? []) if (!seen.has(source.id)) seen.set(source.id, source);
|
|
5057
|
+
}
|
|
5058
|
+
return [...seen.values()].toSorted((a, b) => a.at - b.at).slice(-100);
|
|
5059
|
+
}
|
|
5060
|
+
function clip(text, maxLength) {
|
|
5061
|
+
const chars = Array.from(text);
|
|
5062
|
+
return chars.length <= maxLength ? text : chars.slice(0, maxLength).join("");
|
|
5063
|
+
}
|
|
4764
5064
|
function findDuplicateGroups(memos) {
|
|
4765
5065
|
const groups = [];
|
|
4766
5066
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -4839,7 +5139,8 @@ function buildDuplicateGroup(cluster) {
|
|
|
4839
5139
|
approach: `合并 ${cluster.length} 条相关记录。最新方案: ${newest.approach}`,
|
|
4840
5140
|
outcome: bestOutcome,
|
|
4841
5141
|
whatFailed: failures.size > 0 ? [...failures].join("; ") : "none",
|
|
4842
|
-
whatWorked: successes.size > 0 ? [...successes].join("; ") : "none"
|
|
5142
|
+
whatWorked: successes.size > 0 ? [...successes].join("; ") : "none",
|
|
5143
|
+
distilled: ""
|
|
4843
5144
|
},
|
|
4844
5145
|
reason: `发现 ${cluster.length} 条相似记录(关键词重叠 > ${Math.round(SIMILARITY_THRESHOLD * 100)}%)`
|
|
4845
5146
|
};
|
|
@@ -4902,12 +5203,19 @@ function isOutcomeCompleted(outcome) {
|
|
|
4902
5203
|
const lower = outcome.toLowerCase();
|
|
4903
5204
|
return lower.includes("完成") || lower.includes("done") || lower.includes("completed") || lower.includes("成功") || lower.includes("success");
|
|
4904
5205
|
}
|
|
5206
|
+
/**
|
|
5207
|
+
* A distilled rule is not a stale experience: pruning it by age would delete
|
|
5208
|
+
* the conclusion /dream exists to accumulate.
|
|
5209
|
+
*/
|
|
5210
|
+
function isDistilledRule(memo) {
|
|
5211
|
+
return memo.origin === "dream" || memo.origin === "user" || (memo.distilled ?? "").trim().length > 0;
|
|
5212
|
+
}
|
|
4905
5213
|
function findResolved(memos) {
|
|
4906
|
-
return memos.filter((m) => isOutcomeCompleted(m.outcome) && Date.now() - m.recordedAt > 10080 * 60 * 1e3);
|
|
5214
|
+
return memos.filter((m) => !isDistilledRule(m) && isOutcomeCompleted(m.outcome) && Date.now() - m.recordedAt > 10080 * 60 * 1e3);
|
|
4907
5215
|
}
|
|
4908
5216
|
function findStale(memos, staleDays) {
|
|
4909
5217
|
const threshold = Date.now() - staleDays * 24 * 60 * 60 * 1e3;
|
|
4910
|
-
return memos.filter((m) => m.recordedAt < threshold && !isOutcomeCompleted(m.outcome) && !m.outcome.includes("blocked"));
|
|
5218
|
+
return memos.filter((m) => !isDistilledRule(m) && m.recordedAt < threshold && !isOutcomeCompleted(m.outcome) && !m.outcome.includes("blocked"));
|
|
4911
5219
|
}
|
|
4912
5220
|
//#endregion
|
|
4913
5221
|
//#region ../../packages/memory/src/dream.ts
|
|
@@ -5977,7 +6285,7 @@ function entryFor(homeDir) {
|
|
|
5977
6285
|
await store.init();
|
|
5978
6286
|
const cacheDir = join$1(homeDir, "cache", "fastembed");
|
|
5979
6287
|
mkdirSync(cacheDir, { recursive: true });
|
|
5980
|
-
store.setEmbeddingEngine(
|
|
6288
|
+
store.setEmbeddingEngine(createEmbeddingEngine(cacheDir));
|
|
5981
6289
|
} catch (error) {
|
|
5982
6290
|
entries.delete(homeDir);
|
|
5983
6291
|
throw error;
|
|
@@ -5994,7 +6302,7 @@ function entryFor(homeDir) {
|
|
|
5994
6302
|
*
|
|
5995
6303
|
* The map key is the raw homeDir string so the engine cache key
|
|
5996
6304
|
* join(homeDir, 'cache', 'fastembed') stays byte-identical with every other
|
|
5997
|
-
*
|
|
6305
|
+
* createEmbeddingEngine call site: same string → same engine instance.
|
|
5998
6306
|
*/
|
|
5999
6307
|
function sharedKnowledgeStore(homeDir) {
|
|
6000
6308
|
return entryFor(homeDir).store;
|
|
@@ -6008,7 +6316,7 @@ function sharedKnowledgeStoreReady(homeDir) {
|
|
|
6008
6316
|
const HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
6009
6317
|
const CODE_FENCE_RE = /^(\s*)(`{3,}|~{3,})/;
|
|
6010
6318
|
/**
|
|
6011
|
-
* Soft cap on chunk token count. BGE-small-zh-v1.5 (the
|
|
6319
|
+
* Soft cap on chunk token count. BGE-small-zh-v1.5 (the embedding model used
|
|
6012
6320
|
* here) has a 512-token sequence limit; chunks longer than that get truncated
|
|
6013
6321
|
* at the embedding layer, hurting retrieval quality. We split at ~480 to leave
|
|
6014
6322
|
* headroom for the heading line that gets prepended at embed time.
|
|
@@ -6544,14 +6852,78 @@ async function embedOrNulls(engine, texts, skipEmbedding) {
|
|
|
6544
6852
|
}
|
|
6545
6853
|
/** LLM concurrency for extraction — callers and comments below refer to this constant. */
|
|
6546
6854
|
const LLM_CONCURRENCY = 20;
|
|
6547
|
-
|
|
6855
|
+
/** Formats read directly as UTF-8 text. */
|
|
6856
|
+
const TEXT_EXTENSIONS = new Set([
|
|
6548
6857
|
".md",
|
|
6549
6858
|
".markdown",
|
|
6550
6859
|
".txt"
|
|
6551
6860
|
]);
|
|
6552
|
-
/**
|
|
6553
|
-
|
|
6554
|
-
|
|
6861
|
+
/**
|
|
6862
|
+
* Binary documents run through the markit converter (they become Markdown,
|
|
6863
|
+
* then take the exact same chunking/embedding path as a `.md` file).
|
|
6864
|
+
*/
|
|
6865
|
+
const DOCUMENT_EXTENSIONS = new Set([
|
|
6866
|
+
".pdf",
|
|
6867
|
+
".docx",
|
|
6868
|
+
".xlsx"
|
|
6869
|
+
]);
|
|
6870
|
+
const SUPPORTED_EXTENSIONS = new Set([...TEXT_EXTENSIONS, ...DOCUMENT_EXTENSIONS]);
|
|
6871
|
+
/**
|
|
6872
|
+
* Hard ceiling for a converted document. Every chunk costs one LLM extraction
|
|
6873
|
+
* call, so an unbounded PDF would silently turn into hundreds of requests.
|
|
6874
|
+
*/
|
|
6875
|
+
const MAX_DOCUMENT_BYTES = 50 * 1024 * 1024;
|
|
6876
|
+
/** Lazily-loaded converter instance: only binary ingests pay the import cost. */
|
|
6877
|
+
let markitPromise;
|
|
6878
|
+
function getMarkit() {
|
|
6879
|
+
markitPromise ??= import("./markit-CV3BXOV_.mjs").then(({ Markit }) => new Markit()).catch((error) => {
|
|
6880
|
+
markitPromise = void 0;
|
|
6881
|
+
throw error;
|
|
6882
|
+
});
|
|
6883
|
+
return markitPromise;
|
|
6884
|
+
}
|
|
6885
|
+
/**
|
|
6886
|
+
* The converters record images as `<!-- image: id (page N, WxHpt) -->` when
|
|
6887
|
+
* they are not asked to write them out. Those comments carry no searchable
|
|
6888
|
+
* meaning and would otherwise burn tokens in every chunk they land in.
|
|
6889
|
+
*/
|
|
6890
|
+
function stripImagePlaceholders(markdown) {
|
|
6891
|
+
return markdown.replaceAll(/<!--\s*image:[^>]*-->/g, "").replaceAll(/\n{3,}/g, "\n\n").trim();
|
|
6892
|
+
}
|
|
6893
|
+
/**
|
|
6894
|
+
* Read a source file and fingerprint it.
|
|
6895
|
+
*
|
|
6896
|
+
* Text files hash the decoded string — exactly what the previous
|
|
6897
|
+
* `sha256(readFile(path, 'utf-8'))` produced, so existing `contentHash` values
|
|
6898
|
+
* keep matching even for non-UTF-8 text. Binary files hash the raw bytes, which
|
|
6899
|
+
* stays stable across converter upgrades.
|
|
6900
|
+
*/
|
|
6901
|
+
async function readSource(filePath) {
|
|
6902
|
+
const ext = getFileExtension(filePath);
|
|
6903
|
+
if (!TEXT_EXTENSIONS.has(ext)) {
|
|
6904
|
+
const { size } = await stat(filePath);
|
|
6905
|
+
if (size > MAX_DOCUMENT_BYTES) throw new Error(`文档超过 ${MAX_DOCUMENT_BYTES / 1024 / 1024}MB 上限,已跳过: ${basename$1(filePath)}`);
|
|
6906
|
+
}
|
|
6907
|
+
const bytes = await readFile(filePath);
|
|
6908
|
+
return {
|
|
6909
|
+
bytes,
|
|
6910
|
+
ext,
|
|
6911
|
+
contentHash: TEXT_EXTENSIONS.has(ext) ? createHash("sha256").update(bytes.toString("utf8")).digest("hex") : createHash("sha256").update(bytes).digest("hex")
|
|
6912
|
+
};
|
|
6913
|
+
}
|
|
6914
|
+
/**
|
|
6915
|
+
* Convert a binary document to Markdown. Only called once the bytes are known
|
|
6916
|
+
* to be new or changed — an unchanged PDF is never re-parsed.
|
|
6917
|
+
*/
|
|
6918
|
+
async function convertSource(filePath, bytes, ext, onConverting) {
|
|
6919
|
+
onConverting?.(basename$1(filePath));
|
|
6920
|
+
const { markdown } = await (await getMarkit()).convert(bytes, {
|
|
6921
|
+
extension: ext,
|
|
6922
|
+
filename: basename$1(filePath)
|
|
6923
|
+
});
|
|
6924
|
+
const content = stripImagePlaceholders(markdown ?? "");
|
|
6925
|
+
if (content.length === 0) throw new Error(ext === ".pdf" ? "文档未提取到文字(可能是扫描版 PDF),本功能不含 OCR" : "文档未提取到可索引的文字,文件可能已损坏或为空");
|
|
6926
|
+
return content;
|
|
6555
6927
|
}
|
|
6556
6928
|
function getFileExtension(filePath) {
|
|
6557
6929
|
const lastDot = filePath.lastIndexOf(".");
|
|
@@ -6619,6 +6991,10 @@ async function ensureEmbeddingModelMatches(store, engine) {
|
|
|
6619
6991
|
* Reports progress via callback.
|
|
6620
6992
|
*/
|
|
6621
6993
|
async function ingestFile(store, llm, filePath, onProgress) {
|
|
6994
|
+
if (!isSupportedFile(filePath)) {
|
|
6995
|
+
const ext = getFileExtension(filePath);
|
|
6996
|
+
throw new Error(`不支持的文件类型: ${ext === "" ? "(无扩展名)" : ext}`);
|
|
6997
|
+
}
|
|
6622
6998
|
const engine = store.getEmbeddingEngine();
|
|
6623
6999
|
const skipEmbed = engine === void 0 || !engine.available;
|
|
6624
7000
|
if (skipEmbed) onProgress?.({
|
|
@@ -6630,8 +7006,7 @@ async function ingestFile(store, llm, filePath, onProgress) {
|
|
|
6630
7006
|
stage: "chunking",
|
|
6631
7007
|
message: `读取并切分文件: ${basename$1(filePath)}`
|
|
6632
7008
|
});
|
|
6633
|
-
const
|
|
6634
|
-
const contentHash = createHash("sha256").update(content).digest("hex");
|
|
7009
|
+
const { bytes, ext, contentHash } = await readSource(filePath);
|
|
6635
7010
|
const existing = await store.findSourceByFilePath(filePath);
|
|
6636
7011
|
let replacedOldVersion = false;
|
|
6637
7012
|
if (existing !== void 0) {
|
|
@@ -6653,10 +7028,18 @@ async function ingestFile(store, llm, filePath, onProgress) {
|
|
|
6653
7028
|
stage: "chunking",
|
|
6654
7029
|
message: `检测到内容变更,替换旧版本: ${existing.name}`
|
|
6655
7030
|
});
|
|
7031
|
+
}
|
|
7032
|
+
const content = TEXT_EXTENSIONS.has(ext) ? bytes.toString("utf8") : await convertSource(filePath, bytes, ext, (file) => {
|
|
7033
|
+
onProgress?.({
|
|
7034
|
+
stage: "converting",
|
|
7035
|
+
message: file
|
|
7036
|
+
});
|
|
7037
|
+
});
|
|
7038
|
+
if (existing !== void 0) {
|
|
6656
7039
|
await store.deleteSource(existing.id);
|
|
6657
7040
|
replacedOldVersion = true;
|
|
6658
7041
|
}
|
|
6659
|
-
const sections = chunkContent(content,
|
|
7042
|
+
const sections = chunkContent(content, ext);
|
|
6660
7043
|
store.beginTransaction();
|
|
6661
7044
|
try {
|
|
6662
7045
|
if (sections.length === 0) {
|
|
@@ -6860,6 +7243,7 @@ async function collectSupportedFiles(dirPath) {
|
|
|
6860
7243
|
const files = [];
|
|
6861
7244
|
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
6862
7245
|
for (const entry of entries) {
|
|
7246
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("~$")) continue;
|
|
6863
7247
|
const fullPath = join$1(dirPath, entry.name);
|
|
6864
7248
|
if (entry.isDirectory()) files.push(...await collectSupportedFiles(fullPath));
|
|
6865
7249
|
else if (entry.isFile() && isSupportedFile(fullPath)) files.push(fullPath);
|
|
@@ -6868,7 +7252,7 @@ async function collectSupportedFiles(dirPath) {
|
|
|
6868
7252
|
}
|
|
6869
7253
|
async function ingestDirectory(store, llm, dirPath, onProgress) {
|
|
6870
7254
|
const files = await collectSupportedFiles(dirPath);
|
|
6871
|
-
if (files.length === 0) throw new Error(`目录下没有支持的文件 (.
|
|
7255
|
+
if (files.length === 0) throw new Error(`目录下没有支持的文件 (${[...SUPPORTED_EXTENSIONS].join(", ")}): ${dirPath}`);
|
|
6872
7256
|
const errors = [];
|
|
6873
7257
|
let succeeded = 0;
|
|
6874
7258
|
let created = 0;
|
|
@@ -6880,6 +7264,10 @@ async function ingestDirectory(store, llm, dirPath, onProgress) {
|
|
|
6880
7264
|
for (let i = 0; i < files.length; i++) {
|
|
6881
7265
|
const filePath = files[i];
|
|
6882
7266
|
const fileProgress = (progress) => {
|
|
7267
|
+
if (progress.stage === "converting") {
|
|
7268
|
+
onProgress?.(progress);
|
|
7269
|
+
return;
|
|
7270
|
+
}
|
|
6883
7271
|
const prefix = `批量摄入 ${i + 1}/${files.length}: ${basename$1(filePath)}`;
|
|
6884
7272
|
onProgress?.({
|
|
6885
7273
|
...progress,
|
|
@@ -7254,4 +7642,4 @@ async function chunkVectorFallback(store, queryVec, topK) {
|
|
|
7254
7642
|
return results;
|
|
7255
7643
|
}
|
|
7256
7644
|
//#endregion
|
|
7257
|
-
export { sharedMemoStore as A, buildProjectTagCloud as B, applyConsolidation as C, parseMemoryMemos as D, buildExitExtractionPrompt as E, probeLocalEmbeddingSupport as F, join$1 as G, basename$1 as H, createMemoryMemo as I, relative$1 as J, normalize as K, toSummary as L, clearEmbeddingModelCache as M,
|
|
7645
|
+
export { sharedMemoStore as A, buildProjectTagCloud as B, applyConsolidation as C, parseMemoryMemos as D, buildExitExtractionPrompt as E, probeLocalEmbeddingSupport as F, join$1 as G, basename$1 as H, createMemoryMemo as I, relative$1 as J, normalize as K, toSummary as L, clearEmbeddingModelCache as M, createEmbeddingEngine as N, stripMemoryTags as O, hasEmbeddingModelCache as P, generateTags as R, DreamTracker as S, EXIT_EXTRACTION_SYSTEM_PROMPT as T, dirname$2 as U, rankMemos as V, isAbsolute$1 as W, resolve$1 as Y, splitLargeSection as _, isSupportedFile as a, sharedKnowledgeStoreReady as b, buildExtractionUserPrompt as c, extractQueryEntities as d, parseExtractionResponse as f, estimateTokens as g, chunkText as h, ingestFile as i, classifyEmbeddingFailure as j, MemoryMemoStore as k, extractEventFromChunk as l, chunkMarkdown as m, multiSearchWithTrace as n, ENTITY_TYPES as o, rerankEventsWithLlm as p, parse$1 as q, ingestDirectory as r, EXTRACTION_SYSTEM_PROMPT as s, multiSearch as t, extractJsonFromText as u, stripMarkdown as v, buildConsolidationPlan as w, KnowledgeStore as x, sharedKnowledgeStore as y, normalizeTags as z };
|