@cerefox/memory 1.17.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cerefox.js
CHANGED
|
@@ -5318,7 +5318,7 @@ var init_cli_core = __esm(() => {
|
|
|
5318
5318
|
});
|
|
5319
5319
|
|
|
5320
5320
|
// src/meta.ts
|
|
5321
|
-
var PKG_VERSION = "1.17.
|
|
5321
|
+
var PKG_VERSION = "1.17.1";
|
|
5322
5322
|
var init_meta = () => {};
|
|
5323
5323
|
|
|
5324
5324
|
// ../../_shared/config/paths.ts
|
|
@@ -26230,7 +26230,7 @@ var init_bundled_docs = __esm(() => {
|
|
|
26230
26230
|
});
|
|
26231
26231
|
|
|
26232
26232
|
// ../../_shared/ef-meta/index.ts
|
|
26233
|
-
var EF_VERSION = "1.17.
|
|
26233
|
+
var EF_VERSION = "1.17.1", CEREFOX_VERSION = "1.17.1", EF_LAST_CHANGED = "1.17.1";
|
|
26234
26234
|
var init_ef_meta = () => {};
|
|
26235
26235
|
|
|
26236
26236
|
// ../../_shared/compatibility/index.ts
|
|
@@ -26369,20 +26369,22 @@ var init_compatibility = __esm(() => {
|
|
|
26369
26369
|
// ../../_shared/embeddings/onnx-embedder.ts
|
|
26370
26370
|
var exports_onnx_embedder = {};
|
|
26371
26371
|
__export(exports_onnx_embedder, {
|
|
26372
|
+
EmbedderDownloadError: () => EmbedderDownloadError,
|
|
26372
26373
|
ONNX_MODEL_APPROX_MB: () => ONNX_MODEL_APPROX_MB,
|
|
26373
26374
|
ONNX_MODEL_DIM: () => ONNX_MODEL_DIM,
|
|
26374
26375
|
ONNX_MODEL_DTYPE: () => ONNX_MODEL_DTYPE,
|
|
26375
26376
|
ONNX_MODEL_ID: () => ONNX_MODEL_ID,
|
|
26376
26377
|
ONNX_MODEL_NAME: () => ONNX_MODEL_NAME,
|
|
26377
26378
|
buildPrefixedInputs: () => buildPrefixedInputs,
|
|
26379
|
+
downloadModelFiles: () => downloadModelFiles,
|
|
26378
26380
|
isModelCached: () => isModelCached,
|
|
26379
26381
|
nomicPrefix: () => nomicPrefix,
|
|
26380
26382
|
onnxEmbed: () => onnxEmbed,
|
|
26381
26383
|
warmup: () => warmup
|
|
26382
26384
|
});
|
|
26383
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
|
|
26385
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, renameSync, statSync as statSync2, writeFileSync as writeFileSync5, appendFileSync } from "node:fs";
|
|
26384
26386
|
import { homedir as homedir6 } from "node:os";
|
|
26385
|
-
import { join as join9 } from "node:path";
|
|
26387
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
26386
26388
|
function nomicPrefix(role) {
|
|
26387
26389
|
return role === "query" ? "search_query: " : "search_document: ";
|
|
26388
26390
|
}
|
|
@@ -26390,6 +26392,86 @@ function buildPrefixedInputs(texts, role) {
|
|
|
26390
26392
|
const p = nomicPrefix(role);
|
|
26391
26393
|
return texts.map((t) => p + t);
|
|
26392
26394
|
}
|
|
26395
|
+
async function fetchRange(url, start, end, timeoutMs) {
|
|
26396
|
+
const res = await fetch(url, { headers: { Range: `bytes=${start}-${end}` }, signal: AbortSignal.timeout(timeoutMs) });
|
|
26397
|
+
if (res.status === 200) {
|
|
26398
|
+
await res.body?.cancel();
|
|
26399
|
+
return "no-range";
|
|
26400
|
+
}
|
|
26401
|
+
const total = Number(/\/(\d+)\s*$/.exec(res.headers.get("content-range") ?? "")?.[1] ?? 0);
|
|
26402
|
+
if (res.status === 416 && total > 0) {
|
|
26403
|
+
await res.body?.cancel();
|
|
26404
|
+
return { bytes: new Uint8Array(0), total };
|
|
26405
|
+
}
|
|
26406
|
+
if (res.status !== 206)
|
|
26407
|
+
throw new Error(`HTTP ${res.status}`);
|
|
26408
|
+
if (total === 0)
|
|
26409
|
+
throw new Error("partial response without a total size (Content-Range)");
|
|
26410
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
26411
|
+
const want = Math.min(end, total > 0 ? total - 1 : end) - start + 1;
|
|
26412
|
+
if (bytes.byteLength !== want)
|
|
26413
|
+
throw new Error(`segment ${start}-${end}: got ${bytes.byteLength} bytes, expected ${want}`);
|
|
26414
|
+
return { bytes, total };
|
|
26415
|
+
}
|
|
26416
|
+
async function downloadModelFiles(remoteHost, onProgress, onDone, opts = {}) {
|
|
26417
|
+
const attempts = opts.attempts ?? DOWNLOAD_ATTEMPTS;
|
|
26418
|
+
const backoffMs = opts.backoffMs ?? ((attempt) => Math.min(30, 2 ** attempt) * 1000);
|
|
26419
|
+
const segment = opts.segmentBytes ?? SEGMENT_BYTES;
|
|
26420
|
+
const timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
26421
|
+
const dir = getCacheDir();
|
|
26422
|
+
const base = remoteHost.endsWith("/") ? remoteHost : `${remoteHost}/`;
|
|
26423
|
+
for (const rel of MODEL_REPO_FILES) {
|
|
26424
|
+
const dest = join9(dir, ONNX_MODEL_ID, rel);
|
|
26425
|
+
if (existsSync10(dest))
|
|
26426
|
+
continue;
|
|
26427
|
+
mkdirSync4(dirname4(dest), { recursive: true });
|
|
26428
|
+
const part = `${dest}.part`;
|
|
26429
|
+
const url = `${base}${ONNX_MODEL_ID}/resolve/main/${rel}`;
|
|
26430
|
+
let failures = 0;
|
|
26431
|
+
let lastError = "";
|
|
26432
|
+
let total = 0;
|
|
26433
|
+
for (;; ) {
|
|
26434
|
+
const have = existsSync10(part) ? statSync2(part).size : 0;
|
|
26435
|
+
if (total > 0 && have >= total)
|
|
26436
|
+
break;
|
|
26437
|
+
try {
|
|
26438
|
+
const got = await fetchRange(url, have, have + segment - 1, timeoutMs);
|
|
26439
|
+
if (got === "no-range") {
|
|
26440
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(Math.max(timeoutMs, ONNX_MODEL_APPROX_MB * 1e5)) });
|
|
26441
|
+
if (!res.ok)
|
|
26442
|
+
throw new Error(`HTTP ${res.status}`);
|
|
26443
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
26444
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
26445
|
+
if (declared > 0 && bytes.byteLength !== declared) {
|
|
26446
|
+
throw new Error(`got ${bytes.byteLength} bytes, expected ${declared}`);
|
|
26447
|
+
}
|
|
26448
|
+
writeFileSync5(part, bytes);
|
|
26449
|
+
total = bytes.byteLength;
|
|
26450
|
+
} else {
|
|
26451
|
+
total = got.total;
|
|
26452
|
+
if (have > total) {
|
|
26453
|
+
writeFileSync5(part, new Uint8Array(0));
|
|
26454
|
+
continue;
|
|
26455
|
+
}
|
|
26456
|
+
appendFileSync(part, got.bytes);
|
|
26457
|
+
}
|
|
26458
|
+
failures = 0;
|
|
26459
|
+
onProgress(rel, statSync2(part).size, total);
|
|
26460
|
+
} catch (err) {
|
|
26461
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
26462
|
+
failures++;
|
|
26463
|
+
if (failures >= attempts)
|
|
26464
|
+
throw new EmbedderDownloadError(`${rel}: ${lastError}`);
|
|
26465
|
+
const wait = backoffMs(failures);
|
|
26466
|
+
process.stderr.write(`[cerefox-embed] ${rel}: ${lastError}; retrying in ${Math.round(wait / 1000)}s (${failures}/${attempts - 1})
|
|
26467
|
+
`);
|
|
26468
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
26469
|
+
}
|
|
26470
|
+
}
|
|
26471
|
+
renameSync(part, dest);
|
|
26472
|
+
onDone(rel, statSync2(dest).size);
|
|
26473
|
+
}
|
|
26474
|
+
}
|
|
26393
26475
|
function isModelCached(dir = getCacheDir()) {
|
|
26394
26476
|
return MODEL_FILES.every((f) => existsSync10(join9(dir, f)));
|
|
26395
26477
|
}
|
|
@@ -26518,9 +26600,9 @@ async function ensurePipeline() {
|
|
|
26518
26600
|
progressState.set(file, { ...prior, done: true });
|
|
26519
26601
|
}
|
|
26520
26602
|
};
|
|
26603
|
+
await downloadModelFiles(String(transformers.env.remoteHost ?? "https://huggingface.co/"), (file, loaded, total) => progressCallback({ status: "progress", file, loaded, total }), (file, size) => progressCallback({ status: "done", file, total: size }));
|
|
26521
26604
|
const pipe = await transformers.pipeline("feature-extraction", ONNX_MODEL_ID, {
|
|
26522
|
-
dtype: ONNX_MODEL_DTYPE
|
|
26523
|
-
progress_callback: progressCallback
|
|
26605
|
+
dtype: ONNX_MODEL_DTYPE
|
|
26524
26606
|
});
|
|
26525
26607
|
process.stderr.write(`[cerefox-embed] embedder ready.
|
|
26526
26608
|
`);
|
|
@@ -26564,14 +26646,23 @@ async function onnxEmbed(texts, role) {
|
|
|
26564
26646
|
}
|
|
26565
26647
|
return vectors;
|
|
26566
26648
|
}
|
|
26567
|
-
var ONNX_MODEL_ID = "nomic-ai/nomic-embed-text-v1.5", ONNX_MODEL_NAME = "nomic-embed-text-v1.5", ONNX_MODEL_DTYPE = "q8", ONNX_MODEL_DIM = 768, ONNX_MODEL_APPROX_MB = 130, MODEL_FILES, transformersModule = null, pipelinePromise = null, DEFAULT_ONNX_BATCH = 4;
|
|
26649
|
+
var ONNX_MODEL_ID = "nomic-ai/nomic-embed-text-v1.5", ONNX_MODEL_NAME = "nomic-embed-text-v1.5", ONNX_MODEL_DTYPE = "q8", ONNX_MODEL_DIM = 768, ONNX_MODEL_APPROX_MB = 130, MODEL_REPO_FILES, MODEL_FILES, EmbedderDownloadError, DOWNLOAD_ATTEMPTS = 6, SEGMENT_BYTES, REQUEST_TIMEOUT_MS = 120000, transformersModule = null, pipelinePromise = null, DEFAULT_ONNX_BATCH = 4;
|
|
26568
26650
|
var init_onnx_embedder = __esm(() => {
|
|
26569
|
-
|
|
26651
|
+
MODEL_REPO_FILES = [
|
|
26570
26652
|
"config.json",
|
|
26571
26653
|
"tokenizer.json",
|
|
26572
26654
|
"tokenizer_config.json",
|
|
26573
26655
|
`onnx/model_quantized.onnx`
|
|
26574
|
-
]
|
|
26656
|
+
];
|
|
26657
|
+
MODEL_FILES = MODEL_REPO_FILES.map((f) => join9(ONNX_MODEL_ID, f));
|
|
26658
|
+
EmbedderDownloadError = class EmbedderDownloadError extends Error {
|
|
26659
|
+
status = 503;
|
|
26660
|
+
constructor(detail) {
|
|
26661
|
+
super(`The local embedding model (${ONNX_MODEL_NAME}, ~${ONNX_MODEL_APPROX_MB} MB) could not be downloaded: ${detail}. ` + "What was downloaded is kept, and the next request resumes it. Check the network and try again.");
|
|
26662
|
+
this.name = "EmbedderDownloadError";
|
|
26663
|
+
}
|
|
26664
|
+
};
|
|
26665
|
+
SEGMENT_BYTES = 4 * 1024 * 1024;
|
|
26575
26666
|
});
|
|
26576
26667
|
|
|
26577
26668
|
// ../../_shared/embeddings/index.ts
|
|
@@ -71773,7 +71864,7 @@ init_config();
|
|
|
71773
71864
|
init_config();
|
|
71774
71865
|
init_compatibility();
|
|
71775
71866
|
init_server_assets();
|
|
71776
|
-
import { existsSync as existsSync11, readFileSync as readFileSync9, realpathSync, statSync as
|
|
71867
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9, realpathSync, statSync as statSync3 } from "node:fs";
|
|
71777
71868
|
import { homedir as homedir7 } from "node:os";
|
|
71778
71869
|
import { join as join10 } from "node:path";
|
|
71779
71870
|
|
|
@@ -72014,7 +72105,7 @@ function checkConfig() {
|
|
|
72014
72105
|
let modeDetail = "";
|
|
72015
72106
|
if (process.platform !== "win32") {
|
|
72016
72107
|
try {
|
|
72017
|
-
const mode =
|
|
72108
|
+
const mode = statSync3(envPath).mode & 511;
|
|
72018
72109
|
if (mode & 63) {
|
|
72019
72110
|
return {
|
|
72020
72111
|
name: "config",
|
|
@@ -72371,6 +72462,17 @@ async function checkEmbedderMismatch() {
|
|
|
72371
72462
|
const recorded = [...new Set(rows.map((r) => r.embedder_primary))];
|
|
72372
72463
|
const stale = recorded.filter((r) => r !== active);
|
|
72373
72464
|
if (stale.length === 0) {
|
|
72465
|
+
if (resolveEmbedderKind() === "local") {
|
|
72466
|
+
await Promise.resolve().then(() => init_onnx_embedder());
|
|
72467
|
+
if (!isModelCached()) {
|
|
72468
|
+
return {
|
|
72469
|
+
name: EMBEDDER_CHECK_NAME,
|
|
72470
|
+
status: "warn",
|
|
72471
|
+
detail: `configured "${active}", but the model is not downloaded yet; the first search or write will fetch it (~130 MB).`,
|
|
72472
|
+
hint: "Fetch it now with `cerefox embedder-warmup` (Cerefox Local: `cerefox-local embedder-warmup`); it resumes if the connection drops."
|
|
72473
|
+
};
|
|
72474
|
+
}
|
|
72475
|
+
}
|
|
72374
72476
|
return {
|
|
72375
72477
|
name: EMBEDDER_CHECK_NAME,
|
|
72376
72478
|
status: "ok",
|
|
@@ -74030,7 +74132,7 @@ init_dist4();
|
|
|
74030
74132
|
init_cli_core();
|
|
74031
74133
|
init_config();
|
|
74032
74134
|
var import_cli_progress = __toESM(require_cli_progress(), 1);
|
|
74033
|
-
import { readdirSync as readdirSync4, statSync as
|
|
74135
|
+
import { readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
|
|
74034
74136
|
import { basename as basename3, extname as extname4, join as join11 } from "node:path";
|
|
74035
74137
|
function walk(dir, extensions) {
|
|
74036
74138
|
let entries;
|
|
@@ -74044,7 +74146,7 @@ function walk(dir, extensions) {
|
|
|
74044
74146
|
const full = join11(dir, name);
|
|
74045
74147
|
let stat;
|
|
74046
74148
|
try {
|
|
74047
|
-
stat =
|
|
74149
|
+
stat = statSync4(full);
|
|
74048
74150
|
} catch {
|
|
74049
74151
|
continue;
|
|
74050
74152
|
}
|
|
@@ -74147,10 +74249,10 @@ import {
|
|
|
74147
74249
|
existsSync as existsSync12,
|
|
74148
74250
|
mkdirSync as mkdirSync5,
|
|
74149
74251
|
readFileSync as readFileSync14,
|
|
74150
|
-
writeFileSync as
|
|
74252
|
+
writeFileSync as writeFileSync6
|
|
74151
74253
|
} from "node:fs";
|
|
74152
74254
|
import { homedir as homedir8 } from "node:os";
|
|
74153
|
-
import { dirname as
|
|
74255
|
+
import { dirname as dirname5, join as join12 } from "node:path";
|
|
74154
74256
|
async function readConfigFile(path) {
|
|
74155
74257
|
if (!existsSync12(path)) {
|
|
74156
74258
|
throw userError(`--config file not found: ${path}`);
|
|
@@ -74454,8 +74556,8 @@ async function postWriteLifecycle(envPath, options) {
|
|
|
74454
74556
|
println(c.dim(` Config in effect: ${envPath}`));
|
|
74455
74557
|
}
|
|
74456
74558
|
function writeAnswersTo(target, answers) {
|
|
74457
|
-
mkdirSync5(
|
|
74458
|
-
|
|
74559
|
+
mkdirSync5(dirname5(target), { recursive: true });
|
|
74560
|
+
writeFileSync6(target, buildEnvFile(answers), "utf8");
|
|
74459
74561
|
if (process.platform !== "win32") {
|
|
74460
74562
|
try {
|
|
74461
74563
|
chmodSync(target, 384);
|
|
@@ -74518,7 +74620,7 @@ async function action23(options) {
|
|
|
74518
74620
|
const ch = await promptMigrationChoice();
|
|
74519
74621
|
println("");
|
|
74520
74622
|
if (ch === "c") {
|
|
74521
|
-
mkdirSync5(
|
|
74623
|
+
mkdirSync5(dirname5(homeEnv), { recursive: true });
|
|
74522
74624
|
copyFileSync2(cwdEnv, homeEnv);
|
|
74523
74625
|
if (process.platform !== "win32") {
|
|
74524
74626
|
try {
|
|
@@ -75135,7 +75237,7 @@ function registerMigrateFormat(program) {
|
|
|
75135
75237
|
// src/cli/commands/restore.ts
|
|
75136
75238
|
init_cli_core();
|
|
75137
75239
|
init_client();
|
|
75138
|
-
import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync5, statSync as
|
|
75240
|
+
import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
|
|
75139
75241
|
import { homedir as homedir9 } from "node:os";
|
|
75140
75242
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
75141
75243
|
function expandHome2(path) {
|
|
@@ -75150,10 +75252,10 @@ function resolveBackupFile(target) {
|
|
|
75150
75252
|
if (!existsSync13(path)) {
|
|
75151
75253
|
throw userError(`Backup path not found: ${target}`);
|
|
75152
75254
|
}
|
|
75153
|
-
const stat =
|
|
75255
|
+
const stat = statSync5(path);
|
|
75154
75256
|
if (stat.isFile())
|
|
75155
75257
|
return path;
|
|
75156
|
-
const candidates = readdirSync5(path).filter((n) => n.endsWith(".json") && n.startsWith("cerefox-")).map((n) => ({ name: n, mtime:
|
|
75258
|
+
const candidates = readdirSync5(path).filter((n) => n.endsWith(".json") && n.startsWith("cerefox-")).map((n) => ({ name: n, mtime: statSync5(join13(path, n)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
75157
75259
|
if (candidates.length === 0) {
|
|
75158
75260
|
throw userError(`No cerefox-*.json files in ${path}`);
|
|
75159
75261
|
}
|
|
@@ -75638,8 +75740,8 @@ init_config();
|
|
|
75638
75740
|
import { randomBytes } from "node:crypto";
|
|
75639
75741
|
|
|
75640
75742
|
// src/cli/util/env-file.ts
|
|
75641
|
-
import { copyFileSync as copyFileSync3, existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as
|
|
75642
|
-
import { dirname as
|
|
75743
|
+
import { copyFileSync as copyFileSync3, existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "node:fs";
|
|
75744
|
+
import { dirname as dirname6 } from "node:path";
|
|
75643
75745
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
75644
75746
|
function escapeRegExp(s) {
|
|
75645
75747
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -75649,7 +75751,7 @@ function upsertEnvVar(path, key, value, opts = {}) {
|
|
|
75649
75751
|
const header = opts.comment ? `# ${opts.comment}
|
|
75650
75752
|
` : "";
|
|
75651
75753
|
if (!existsSync14(path)) {
|
|
75652
|
-
|
|
75754
|
+
writeFileSync7(path, `${header}${line}
|
|
75653
75755
|
`, { mode: 384 });
|
|
75654
75756
|
return { path, action: "created" };
|
|
75655
75757
|
}
|
|
@@ -75674,7 +75776,7 @@ ${header}${line}
|
|
|
75674
75776
|
`;
|
|
75675
75777
|
action = "added";
|
|
75676
75778
|
}
|
|
75677
|
-
|
|
75779
|
+
writeFileSync7(path, next);
|
|
75678
75780
|
return { path, action, backupPath };
|
|
75679
75781
|
}
|
|
75680
75782
|
function readEnvVar(path, key) {
|
|
@@ -75685,7 +75787,7 @@ function readEnvVar(path, key) {
|
|
|
75685
75787
|
}
|
|
75686
75788
|
function envGitignoreWarning(path) {
|
|
75687
75789
|
try {
|
|
75688
|
-
const dir =
|
|
75790
|
+
const dir = dirname6(path);
|
|
75689
75791
|
const inTree = spawnSync6("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], {
|
|
75690
75792
|
encoding: "utf8"
|
|
75691
75793
|
});
|
|
@@ -77645,7 +77747,7 @@ var _baseMimes = {
|
|
|
77645
77747
|
var baseMimes = _baseMimes;
|
|
77646
77748
|
|
|
77647
77749
|
// ../../node_modules/.bun/@hono+node-server@2.1.1+2ac783cc5e75a70c/node_modules/@hono/node-server/dist/serve-static.mjs
|
|
77648
|
-
import { createReadStream, existsSync as existsSync16, statSync as
|
|
77750
|
+
import { createReadStream, existsSync as existsSync16, statSync as statSync6 } from "node:fs";
|
|
77649
77751
|
import { join as join14 } from "node:path";
|
|
77650
77752
|
var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
|
|
77651
77753
|
var ENCODINGS = {
|
|
@@ -77657,7 +77759,7 @@ var ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS);
|
|
|
77657
77759
|
var getStats = (path) => {
|
|
77658
77760
|
let stats;
|
|
77659
77761
|
try {
|
|
77660
|
-
stats =
|
|
77762
|
+
stats = statSync6(path);
|
|
77661
77763
|
} catch {}
|
|
77662
77764
|
return stats;
|
|
77663
77765
|
};
|
|
@@ -77803,7 +77905,7 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
77803
77905
|
|
|
77804
77906
|
// src/web/server.ts
|
|
77805
77907
|
import { existsSync as existsSync20 } from "node:fs";
|
|
77806
|
-
import { readFileSync as readFileSync19, statSync as
|
|
77908
|
+
import { readFileSync as readFileSync19, statSync as statSync9 } from "node:fs";
|
|
77807
77909
|
import { join as join18 } from "node:path";
|
|
77808
77910
|
|
|
77809
77911
|
// ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/compose.js
|
|
@@ -79993,6 +80095,9 @@ function registerDiscoveryRoutes(app, ctx) {
|
|
|
79993
80095
|
if (err instanceof HttpError) {
|
|
79994
80096
|
return c.json({ detail: err.message }, err.status);
|
|
79995
80097
|
}
|
|
80098
|
+
if (err instanceof Error && err.name === "EmbedderDownloadError") {
|
|
80099
|
+
return c.json({ detail: err.message }, 503);
|
|
80100
|
+
}
|
|
79996
80101
|
throw err;
|
|
79997
80102
|
}
|
|
79998
80103
|
const who = resolveCallerIdentity(c);
|
|
@@ -80792,6 +80897,8 @@ function notReady(error) {
|
|
|
80792
80897
|
return { success: false, error, detail: error };
|
|
80793
80898
|
}
|
|
80794
80899
|
function ingestFailure(err) {
|
|
80900
|
+
if (err instanceof Error && err.name === "EmbedderDownloadError")
|
|
80901
|
+
return [notReady(err.message), 503];
|
|
80795
80902
|
if (err instanceof ConcurrencyConflictError) {
|
|
80796
80903
|
return [
|
|
80797
80904
|
{
|
|
@@ -81058,7 +81165,7 @@ function registerIngestRoutes(app, ctx) {
|
|
|
81058
81165
|
init_server_assets();
|
|
81059
81166
|
init_meta();
|
|
81060
81167
|
import { execFileSync } from "node:child_process";
|
|
81061
|
-
import { dirname as
|
|
81168
|
+
import { dirname as dirname8 } from "node:path";
|
|
81062
81169
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
81063
81170
|
|
|
81064
81171
|
// src/web/docs.ts
|
|
@@ -81066,9 +81173,9 @@ import {
|
|
|
81066
81173
|
existsSync as existsSync17,
|
|
81067
81174
|
readFileSync as readFileSync17,
|
|
81068
81175
|
readdirSync as readdirSync6,
|
|
81069
|
-
statSync as
|
|
81176
|
+
statSync as statSync7
|
|
81070
81177
|
} from "node:fs";
|
|
81071
|
-
import { basename as basename5, dirname as
|
|
81178
|
+
import { basename as basename5, dirname as dirname7, join as join15, resolve as resolve5 } from "node:path";
|
|
81072
81179
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
81073
81180
|
var TOP_LEVEL_DOCS = [
|
|
81074
81181
|
{ filename: "README.md", path: "README.md", category: "readme" },
|
|
@@ -81084,7 +81191,7 @@ var TOP_LEVEL_DOCS = [
|
|
|
81084
81191
|
}
|
|
81085
81192
|
];
|
|
81086
81193
|
function moduleDir2() {
|
|
81087
|
-
return
|
|
81194
|
+
return dirname7(fileURLToPath3(import.meta.url));
|
|
81088
81195
|
}
|
|
81089
81196
|
function resolveDocsRoots() {
|
|
81090
81197
|
const here = moduleDir2();
|
|
@@ -81096,7 +81203,7 @@ function resolveDocsRoots() {
|
|
|
81096
81203
|
let pkgTopLevel = null;
|
|
81097
81204
|
for (const pkg of pkgRootCandidates) {
|
|
81098
81205
|
const guides = join15(pkg, "docs", "guides");
|
|
81099
|
-
if (existsSync17(guides) &&
|
|
81206
|
+
if (existsSync17(guides) && statSync7(guides).isDirectory()) {
|
|
81100
81207
|
pkgGuides = guides;
|
|
81101
81208
|
pkgTopLevel = pkg;
|
|
81102
81209
|
break;
|
|
@@ -81162,7 +81269,7 @@ function readDoc(docPath) {
|
|
|
81162
81269
|
if (!candidate.startsWith(rootResolved + "/") && candidate !== rootResolved) {
|
|
81163
81270
|
return null;
|
|
81164
81271
|
}
|
|
81165
|
-
if (existsSync17(candidate) &&
|
|
81272
|
+
if (existsSync17(candidate) && statSync7(candidate).isFile()) {
|
|
81166
81273
|
try {
|
|
81167
81274
|
return readFileSync17(candidate, "utf8");
|
|
81168
81275
|
} catch {
|
|
@@ -81188,7 +81295,7 @@ function readOpenApiDocument() {
|
|
|
81188
81295
|
|
|
81189
81296
|
// src/web/routes/meta.ts
|
|
81190
81297
|
init_compatibility();
|
|
81191
|
-
function resolveGitCommitShort(dir =
|
|
81298
|
+
function resolveGitCommitShort(dir = dirname8(fileURLToPath4(import.meta.url))) {
|
|
81192
81299
|
const env = process.env.CEREFOX_GIT_COMMIT;
|
|
81193
81300
|
if (env)
|
|
81194
81301
|
return env.slice(0, 7);
|
|
@@ -81320,7 +81427,7 @@ function registerPostgrestProxy(app) {
|
|
|
81320
81427
|
|
|
81321
81428
|
// src/web/routes/preferences.ts
|
|
81322
81429
|
init_config();
|
|
81323
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as
|
|
81430
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "node:fs";
|
|
81324
81431
|
import { join as join16 } from "node:path";
|
|
81325
81432
|
function isTheme(v) {
|
|
81326
81433
|
return v === "auto" || v === "light" || v === "dark";
|
|
@@ -81348,7 +81455,7 @@ function registerPreferencesRoutes(app) {
|
|
|
81348
81455
|
const dir = userStateDir();
|
|
81349
81456
|
if (!existsSync18(dir))
|
|
81350
81457
|
mkdirSync6(dir, { recursive: true });
|
|
81351
|
-
|
|
81458
|
+
writeFileSync8(prefsFile(), `${JSON.stringify(next, null, 2)}
|
|
81352
81459
|
`);
|
|
81353
81460
|
} catch (err) {
|
|
81354
81461
|
return c.json({ detail: err instanceof Error ? err.message : String(err) }, 500);
|
|
@@ -81483,14 +81590,14 @@ function registerProjectsRoutes(app, ctx) {
|
|
|
81483
81590
|
}
|
|
81484
81591
|
|
|
81485
81592
|
// src/web/static.ts
|
|
81486
|
-
import { existsSync as existsSync19, statSync as
|
|
81487
|
-
import { dirname as
|
|
81593
|
+
import { existsSync as existsSync19, statSync as statSync8 } from "node:fs";
|
|
81594
|
+
import { dirname as dirname9, join as join17 } from "node:path";
|
|
81488
81595
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
81489
81596
|
function moduleDir3() {
|
|
81490
|
-
return
|
|
81597
|
+
return dirname9(fileURLToPath5(import.meta.url));
|
|
81491
81598
|
}
|
|
81492
81599
|
function isUsableSpaDir(dir) {
|
|
81493
|
-
return existsSync19(dir) &&
|
|
81600
|
+
return existsSync19(dir) && statSync8(dir).isDirectory() && existsSync19(join17(dir, "index.html"));
|
|
81494
81601
|
}
|
|
81495
81602
|
function resolveSpaDist() {
|
|
81496
81603
|
const here = moduleDir3();
|
|
@@ -81512,7 +81619,7 @@ function resolveStaticDir() {
|
|
|
81512
81619
|
join17(here, "..", "..", "..", "..", "web", "static")
|
|
81513
81620
|
];
|
|
81514
81621
|
for (const c of candidates) {
|
|
81515
|
-
if (existsSync19(c) &&
|
|
81622
|
+
if (existsSync19(c) && statSync8(c).isDirectory())
|
|
81516
81623
|
return c;
|
|
81517
81624
|
}
|
|
81518
81625
|
return null;
|
|
@@ -81613,7 +81720,7 @@ function buildApp(ctx = buildWebContext()) {
|
|
|
81613
81720
|
let cached = null;
|
|
81614
81721
|
const readIndex = () => {
|
|
81615
81722
|
try {
|
|
81616
|
-
const { mtimeMs, size } =
|
|
81723
|
+
const { mtimeMs, size } = statSync9(indexPath);
|
|
81617
81724
|
if (!cached || cached.mtimeMs !== mtimeMs || cached.size !== size) {
|
|
81618
81725
|
cached = { mtimeMs, size, html: readFileSync19(indexPath, "utf8") };
|
|
81619
81726
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* doesn't touch `supabase/functions/` leaves it alone).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
export const EF_VERSION = "1.17.
|
|
21
|
+
export const EF_VERSION = "1.17.1";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* The Cerefox RELEASE version — what `cerefox --version` reports and what npm
|
|
@@ -36,7 +36,7 @@ export const EF_VERSION = "1.17.0";
|
|
|
36
36
|
* is imported by the Deno Edge Functions, which cannot reach into the npm
|
|
37
37
|
* package.
|
|
38
38
|
*/
|
|
39
|
-
export const CEREFOX_VERSION = "1.17.
|
|
39
|
+
export const CEREFOX_VERSION = "1.17.1";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* The most recent version whose EF-side SOURCE actually changed (#127).
|
|
@@ -46,7 +46,7 @@ export const CEREFOX_VERSION = "1.17.0";
|
|
|
46
46
|
* `cut_release.ts` ONLY when EF source changed since the last tag; doctor
|
|
47
47
|
* uses it to stay silent on label-only drift.
|
|
48
48
|
*/
|
|
49
|
-
export const EF_LAST_CHANGED = "1.17.
|
|
49
|
+
export const EF_LAST_CHANGED = "1.17.1";
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* The 8 peer EFs the cerefox-mcp aggregator probes (excludes cerefox-mcp
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
* volume so models survive container recreate/upgrade); default `~/.cerefox/models`.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
24
|
+
import { existsSync, mkdirSync, renameSync, statSync, writeFileSync, appendFileSync } from "node:fs";
|
|
25
25
|
import { homedir } from "node:os";
|
|
26
|
-
import { join } from "node:path";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
27
|
|
|
28
28
|
export const ONNX_MODEL_ID = "nomic-ai/nomic-embed-text-v1.5";
|
|
29
29
|
export const ONNX_MODEL_NAME = "nomic-embed-text-v1.5";
|
|
@@ -46,14 +46,149 @@ export function buildPrefixedInputs(texts: string[], role: EmbedRole): string[]
|
|
|
46
46
|
return texts.map((t) => p + t);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/** What the q8 pipeline reads, relative to the
|
|
49
|
+
/** What the q8 pipeline reads, relative to the model dir. Same layout under
|
|
50
50
|
* transformers.js 3.x (the Local image) and 4.x (npm installs). */
|
|
51
|
-
const
|
|
51
|
+
const MODEL_REPO_FILES = [
|
|
52
52
|
"config.json",
|
|
53
53
|
"tokenizer.json",
|
|
54
54
|
"tokenizer_config.json",
|
|
55
55
|
`onnx/model_quantized.onnx`,
|
|
56
|
-
]
|
|
56
|
+
];
|
|
57
|
+
const MODEL_FILES = MODEL_REPO_FILES.map((f) => join(ONNX_MODEL_ID, f));
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The model could not be fetched (#314). Carries a status so HTTP surfaces
|
|
61
|
+
* answer 503 (try again) rather than 500, and a message that says what was
|
|
62
|
+
* happening instead of the transport error underneath.
|
|
63
|
+
*/
|
|
64
|
+
export class EmbedderDownloadError extends Error {
|
|
65
|
+
readonly status = 503;
|
|
66
|
+
constructor(detail: string) {
|
|
67
|
+
super(
|
|
68
|
+
`The local embedding model (${ONNX_MODEL_NAME}, ~${ONNX_MODEL_APPROX_MB} MB) could not be downloaded: ${detail}. ` +
|
|
69
|
+
"What was downloaded is kept, and the next request resumes it. Check the network and try again.",
|
|
70
|
+
);
|
|
71
|
+
this.name = "EmbedderDownloadError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Consecutive failed requests per file before giving up. */
|
|
76
|
+
const DOWNLOAD_ATTEMPTS = 6;
|
|
77
|
+
/** Bytes per Range request. Small enough that a failure loses little. */
|
|
78
|
+
const SEGMENT_BYTES = 4 * 1024 * 1024;
|
|
79
|
+
/**
|
|
80
|
+
* Per-request limit. A connection that dies without a reset (a network that
|
|
81
|
+
* disappears rather than refuses) otherwise leaves fetch waiting forever. 4 MB
|
|
82
|
+
* in 2 minutes is ~35 KB/s, far below any link that could finish the model.
|
|
83
|
+
*/
|
|
84
|
+
const REQUEST_TIMEOUT_MS = 120_000;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* One exact byte range, or an error. The length check is the point: when a
|
|
88
|
+
* connection drops, Bun's fetch can silently re-issue the request and splice
|
|
89
|
+
* the new response onto the old stream (observed: one fetch, two server
|
|
90
|
+
* requests, the bytes of both). Trusting the stream would write a corrupt model;
|
|
91
|
+
* a segment of the wrong length is retried instead.
|
|
92
|
+
*/
|
|
93
|
+
async function fetchRange(
|
|
94
|
+
url: string,
|
|
95
|
+
start: number,
|
|
96
|
+
end: number,
|
|
97
|
+
timeoutMs: number,
|
|
98
|
+
): Promise<{ bytes: Uint8Array; total: number } | "no-range"> {
|
|
99
|
+
const res = await fetch(url, { headers: { Range: `bytes=${start}-${end}` }, signal: AbortSignal.timeout(timeoutMs) });
|
|
100
|
+
if (res.status === 200) {
|
|
101
|
+
await res.body?.cancel();
|
|
102
|
+
return "no-range";
|
|
103
|
+
}
|
|
104
|
+
const total = Number(/\/(\d+)\s*$/.exec(res.headers.get("content-range") ?? "")?.[1] ?? 0);
|
|
105
|
+
if (res.status === 416 && total > 0) {
|
|
106
|
+
// Nothing at or past `start`: the part file already holds the whole file.
|
|
107
|
+
await res.body?.cancel();
|
|
108
|
+
return { bytes: new Uint8Array(0), total };
|
|
109
|
+
}
|
|
110
|
+
if (res.status !== 206) throw new Error(`HTTP ${res.status}`);
|
|
111
|
+
if (total === 0) throw new Error("partial response without a total size (Content-Range)");
|
|
112
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
113
|
+
const want = Math.min(end, total > 0 ? total - 1 : end) - start + 1;
|
|
114
|
+
if (bytes.byteLength !== want) throw new Error(`segment ${start}-${end}: got ${bytes.byteLength} bytes, expected ${want}`);
|
|
115
|
+
return { bytes, total };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Fetch any missing model file into the cache path transformers.js reads,
|
|
120
|
+
* resumably and with retries (#314). Before this the download was one stream
|
|
121
|
+
* inside transformers.js: on a slow link the connection dropped minutes in, the
|
|
122
|
+
* request that triggered it failed with a raw socket error, and the next attempt
|
|
123
|
+
* refetched the 131 MB from zero.
|
|
124
|
+
*
|
|
125
|
+
* Works in verified Range segments appended to `<file>.part`, renamed into
|
|
126
|
+
* place only when the declared size is reached, so neither a partial nor a
|
|
127
|
+
* spliced file is ever mistaken for the model. A host that ignores Range gets
|
|
128
|
+
* one whole-file request, checked against Content-Length.
|
|
129
|
+
*/
|
|
130
|
+
export async function downloadModelFiles(
|
|
131
|
+
remoteHost: string,
|
|
132
|
+
onProgress: (file: string, loaded: number, total: number) => void,
|
|
133
|
+
onDone: (file: string, size: number) => void,
|
|
134
|
+
opts: { attempts?: number; backoffMs?: (attempt: number) => number; segmentBytes?: number; timeoutMs?: number } = {},
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const attempts = opts.attempts ?? DOWNLOAD_ATTEMPTS;
|
|
137
|
+
const backoffMs = opts.backoffMs ?? ((attempt: number) => Math.min(30, 2 ** attempt) * 1000);
|
|
138
|
+
const segment = opts.segmentBytes ?? SEGMENT_BYTES;
|
|
139
|
+
const timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
140
|
+
const dir = getCacheDir();
|
|
141
|
+
const base = remoteHost.endsWith("/") ? remoteHost : `${remoteHost}/`;
|
|
142
|
+
for (const rel of MODEL_REPO_FILES) {
|
|
143
|
+
const dest = join(dir, ONNX_MODEL_ID, rel);
|
|
144
|
+
if (existsSync(dest)) continue;
|
|
145
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
146
|
+
const part = `${dest}.part`;
|
|
147
|
+
const url = `${base}${ONNX_MODEL_ID}/resolve/main/${rel}`;
|
|
148
|
+
let failures = 0;
|
|
149
|
+
let lastError = "";
|
|
150
|
+
let total = 0;
|
|
151
|
+
for (;;) {
|
|
152
|
+
const have = existsSync(part) ? statSync(part).size : 0;
|
|
153
|
+
if (total > 0 && have >= total) break;
|
|
154
|
+
try {
|
|
155
|
+
const got = await fetchRange(url, have, have + segment - 1, timeoutMs);
|
|
156
|
+
if (got === "no-range") {
|
|
157
|
+
// Whole file in one request; only a length-checked body is kept.
|
|
158
|
+
// No segment to bound it, so a generous whole-file limit (10 KB/s).
|
|
159
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(Math.max(timeoutMs, ONNX_MODEL_APPROX_MB * 100_000)) });
|
|
160
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
161
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
162
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
163
|
+
if (declared > 0 && bytes.byteLength !== declared) {
|
|
164
|
+
throw new Error(`got ${bytes.byteLength} bytes, expected ${declared}`);
|
|
165
|
+
}
|
|
166
|
+
writeFileSync(part, bytes);
|
|
167
|
+
total = bytes.byteLength;
|
|
168
|
+
} else {
|
|
169
|
+
total = got.total;
|
|
170
|
+
if (have > total) {
|
|
171
|
+
// A part file longer than the file itself is not ours to trust.
|
|
172
|
+
writeFileSync(part, new Uint8Array(0));
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
appendFileSync(part, got.bytes);
|
|
176
|
+
}
|
|
177
|
+
failures = 0;
|
|
178
|
+
onProgress(rel, statSync(part).size, total);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
181
|
+
failures++;
|
|
182
|
+
if (failures >= attempts) throw new EmbedderDownloadError(`${rel}: ${lastError}`);
|
|
183
|
+
const wait = backoffMs(failures);
|
|
184
|
+
process.stderr.write(`[cerefox-embed] ${rel}: ${lastError}; retrying in ${Math.round(wait / 1000)}s (${failures}/${attempts - 1})\n`);
|
|
185
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
renameSync(part, dest);
|
|
189
|
+
onDone(rel, statSync(dest).size);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
57
192
|
|
|
58
193
|
/**
|
|
59
194
|
* Is every file the pipeline needs already on disk? Decides whether loading is
|
|
@@ -223,9 +358,17 @@ async function ensurePipeline(): Promise<FeaturePipeline> {
|
|
|
223
358
|
}
|
|
224
359
|
};
|
|
225
360
|
|
|
361
|
+
// Our own resumable, retried fetch first (#314); transformers.js then
|
|
362
|
+
// finds every file in its cache and loads locally.
|
|
363
|
+
await downloadModelFiles(
|
|
364
|
+
String(transformers.env.remoteHost ?? "https://huggingface.co/"),
|
|
365
|
+
(file, loaded, total) => progressCallback({ status: "progress", file, loaded, total }),
|
|
366
|
+
(file, size) => progressCallback({ status: "done", file, total: size }),
|
|
367
|
+
);
|
|
368
|
+
// Every file is on disk now, so this is a local load: no progress callback,
|
|
369
|
+
// which would only report each file "done" a second time.
|
|
226
370
|
const pipe = await transformers.pipeline("feature-extraction", ONNX_MODEL_ID, {
|
|
227
371
|
dtype: ONNX_MODEL_DTYPE,
|
|
228
|
-
progress_callback: progressCallback,
|
|
229
372
|
});
|
|
230
373
|
process.stderr.write(`[cerefox-embed] embedder ready.\n`);
|
|
231
374
|
return pipe as unknown as FeaturePipeline;
|
|
@@ -237,6 +237,15 @@ BEGIN
|
|
|
237
237
|
-- scored 1.2 or 0.3 depending on which phrasing found it
|
|
238
238
|
-- (docs/specs/search-calibration.md).
|
|
239
239
|
ts_rank_cd(c.fts, query_fts, 32)::FLOAT AS fts_score,
|
|
240
|
+
-- 0.18.1: the exact cosine of every keyword match, so a chunk
|
|
241
|
+
-- that matched by keyword is never fused as if its meaning
|
|
242
|
+
-- scored 0 merely because it fell outside vec_results.
|
|
243
|
+
CASE
|
|
244
|
+
WHEN p_use_upgrade AND c.embedding_upgrade IS NOT NULL
|
|
245
|
+
THEN (1.0 - (c.embedding_upgrade <=> p_query_embedding))::FLOAT
|
|
246
|
+
ELSE
|
|
247
|
+
(1.0 - (c.embedding_primary <=> p_query_embedding))::FLOAT
|
|
248
|
+
END AS fts_vec_score,
|
|
240
249
|
-- v1.0.4 coverage gate: in AND mode a match means 100% of the
|
|
241
250
|
-- query's terms are present, so the unconditional pass is
|
|
242
251
|
-- earned by construction. In OR-fallback mode, earn it only
|
|
@@ -283,21 +292,24 @@ BEGIN
|
|
|
283
292
|
))
|
|
284
293
|
AND (p_metadata_filter IS NULL OR d.metadata @> p_metadata_filter)
|
|
285
294
|
AND (p_review_status IS NULL OR d.review_status = p_review_status)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
295
|
+
-- 0.18.1: ordered by the computed similarity, NOT by the bare
|
|
296
|
+
-- `embedding <=> query` distance. The bare form (which the CASE
|
|
297
|
+
-- folds to once p_use_upgrade is known) lets the planner serve this
|
|
298
|
+
-- from the HNSW index, which is approximate and returns at most
|
|
299
|
+
-- hnsw.ef_search (40) rows: on a store with heavy version churn it
|
|
300
|
+
-- returned ~38 of the 250 candidates asked for, mostly not the
|
|
301
|
+
-- nearest ones. An exact scan is also faster at knowledge-base
|
|
302
|
+
-- scale (thousands of chunks).
|
|
303
|
+
ORDER BY vec_score DESC
|
|
292
304
|
LIMIT candidate_count
|
|
293
305
|
),
|
|
294
306
|
combined AS (
|
|
295
307
|
SELECT
|
|
296
308
|
COALESCE(f.id, v.id) AS id,
|
|
297
|
-
( v_alpha * COALESCE(v.vec_score, 0.0) +
|
|
309
|
+
( v_alpha * COALESCE(v.vec_score, f.fts_vec_score, 0.0) +
|
|
298
310
|
(1.0 - v_alpha) * COALESCE(f.fts_score, 0.0)
|
|
299
311
|
) AS score,
|
|
300
|
-
COALESCE(v.vec_score, 0.0) AS vec_score,
|
|
312
|
+
COALESCE(v.vec_score, f.fts_vec_score, 0.0) AS vec_score,
|
|
301
313
|
-- TRUE when the chunk matched the @@ FTS operator WITH enough
|
|
302
314
|
-- term coverage to earn the unconditional pass (v1.0.4; always
|
|
303
315
|
-- true for AND-mode matches). We use this flag rather than
|
|
@@ -565,12 +577,10 @@ BEGIN
|
|
|
565
577
|
THEN (1.0 - (c.embedding_upgrade <=> p_query_embedding))::FLOAT
|
|
566
578
|
ELSE (1.0 - (c.embedding_primary <=> p_query_embedding))::FLOAT
|
|
567
579
|
END >= p_min_score
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
ELSE c.embedding_primary <=> p_query_embedding
|
|
573
|
-
END
|
|
580
|
+
-- 0.18.1: by the computed similarity, so the approximate HNSW index cannot
|
|
581
|
+
-- serve (and truncate) it; see vec_results in cerefox_hybrid_search.
|
|
582
|
+
-- By position: `score` would be ambiguous with the OUT column here.
|
|
583
|
+
ORDER BY 8 DESC
|
|
574
584
|
LIMIT p_match_count;
|
|
575
585
|
END;
|
|
576
586
|
$$;
|
|
@@ -3219,6 +3229,11 @@ SET search_path = public, pg_catalog
|
|
|
3219
3229
|
AS $$
|
|
3220
3230
|
-- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
|
|
3221
3231
|
-- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
|
|
3232
|
+
-- 0.18.1: exact vector candidates. Hybrid and semantic search ordered by
|
|
3233
|
+
-- the bare distance, which let the planner use the approximate HNSW index
|
|
3234
|
+
-- (at most ef_search = 40 rows, poor recall after heavy version churn);
|
|
3235
|
+
-- they now order by the computed similarity (exact scan). Keyword matches
|
|
3236
|
+
-- carry their exact cosine into fusion. RPC-only.
|
|
3222
3237
|
-- 0.18.0 (iteration 48): search calibration, RPC-only. Bounded keyword
|
|
3223
3238
|
-- score in hybrid fusion (ts_rank_cd normalisation 32); min_term_coverage
|
|
3224
3239
|
-- default 0.67; built-in min_search_score follows the store's embedder
|
|
@@ -3264,7 +3279,7 @@ AS $$
|
|
|
3264
3279
|
-- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
|
|
3265
3280
|
-- the partial-edit surface, and both migrations (0019, 0020) are in the
|
|
3266
3281
|
-- sequence, so a store deploying this gets everything from both lines.
|
|
3267
|
-
SELECT '0.18.
|
|
3282
|
+
SELECT '0.18.1'::TEXT;
|
|
3268
3283
|
$$;
|
|
3269
3284
|
|
|
3270
3285
|
-- ── cerefox_find_dead_links ──────────────────────────────────────────────────
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
-- Requires extensions: vector (pgvector), uuid-ossp
|
|
6
6
|
-- These are enabled at the top of db_deploy.py before this file is applied.
|
|
7
7
|
--
|
|
8
|
-
-- @version: 0.18.
|
|
8
|
+
-- @version: 0.18.1
|
|
9
9
|
-- The `@version` marker above is read by the schema-version-mismatch banner
|
|
10
10
|
-- (see /api/v1/schema-version). Bump it whenever schema.sql OR rpcs.sql
|
|
11
11
|
-- changes in a way that requires `cerefox server deploy` to be re-run —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerefox/memory",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.1",
|
|
4
4
|
"description": "Cerefox \u2014 user-owned shared memory for AI agents. CLI + stdio MCP server + web UI + ingestion for a knowledge base on your own Supabase project (or fully self-hosted with Cerefox Local).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/fstamatelopoulos/cerefox",
|