@avantf/dsh-mem 0.4.1 → 0.4.2

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/lib/client.js CHANGED
@@ -6457,10 +6457,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6457
6457
  function HealthBlock(props) {
6458
6458
  const r = props.stats.retrieval;
6459
6459
  if (r === void 0) return h("div", { className: pages_module_css_default.status }, "检索健康度不可用。");
6460
+ const vectors = props.stats.vectors;
6461
+ const pendingVectors = vectors === void 0 ? 0 : vectors.stale + vectors.space_stale;
6460
6462
  const pct = (v) => `${String(Math.round(v * 100))}%`;
6461
6463
  const kinds = Object.entries(r.by_kind).map(([kind, v]) => `${kind} ${String(v.queries)} 次(空 ${String(v.zero_results)})`).join(" · ");
6462
6464
  const truncated = r.embedding_truncated + r.rerank_truncated + r.output_truncated;
6463
- return h("div", { className: pages_module_css_default.detail }, h("div", { className: pages_module_css_default.status }, `检索 ${String(r.queries)} 次 · 空结果 ${pct(r.zero_result_rate)} · 平均 ${r.avg_latency_ms.toFixed(1)}ms · 峰值 ${r.max_latency_ms.toFixed(1)}ms`), h("div", { className: pages_module_css_default.meta }, `语义腿在线 ${pct(r.semantic_live_rate)} · 平均命中 ${String(r.avg_results_per_query)} 条${r.rerank_used > 0 ? ` · 重排 ${String(r.rerank_used)} 次` : ""}`), kinds === "" ? null : h("div", { className: pages_module_css_default.meta }, kinds), h("div", { className: pages_module_css_default.meta }, truncated === 0 ? "未被任何预算截断。" : `截断:嵌入 ${String(r.embedding_truncated)} · 重排 ${String(r.rerank_truncated)} · 输出 ${String(r.output_truncated)}${r.output_truncated > 0 ? "(调大 retrieval.max_output_tokens 或用 max_tokens 参数)" : ""}`), r.updated_at === null ? null : h("div", { className: pages_module_css_default.meta }, `更新于 ${r.updated_at}`));
6465
+ return h("div", { className: pages_module_css_default.detail }, pendingVectors === 0 ? null : h("div", { className: pages_module_css_default.status }, `⚠ 向量空间待迁移:${String(pendingVectors)} 条 ACTIVE 向量来自旧嵌入空间(宽度不符 ${String(vectors?.stale ?? 0)} · 其他模型 ${String(vectors?.space_stale ?? 0)})——语义腿对它们失效,正在后台分批重算;手动入口 \`avantf-mem vectors --fix\``), h("div", { className: pages_module_css_default.status }, `检索 ${String(r.queries)} 次 · 空结果 ${pct(r.zero_result_rate)} · 平均 ${r.avg_latency_ms.toFixed(1)}ms · 峰值 ${r.max_latency_ms.toFixed(1)}ms`), h("div", { className: pages_module_css_default.meta }, `语义腿在线 ${pct(r.semantic_live_rate)} · 平均命中 ${String(r.avg_results_per_query)} 条${r.rerank_used > 0 ? ` · 重排 ${String(r.rerank_used)} 次` : ""}`), kinds === "" ? null : h("div", { className: pages_module_css_default.meta }, kinds), h("div", { className: pages_module_css_default.meta }, truncated === 0 ? "未被任何预算截断。" : `截断:嵌入 ${String(r.embedding_truncated)} · 重排 ${String(r.rerank_truncated)} · 输出 ${String(r.output_truncated)}${r.output_truncated > 0 ? "(调大 retrieval.max_output_tokens 或用 max_tokens 参数)" : ""}`), r.updated_at === null ? null : h("div", { className: pages_module_css_default.meta }, `更新于 ${r.updated_at}`));
6464
6466
  }
6465
6467
  /**
6466
6468
  * Group cross-store hits for display: memory facts vs `domain → source`.
@@ -15,7 +15,7 @@ import type { AvantfLogger } from '../../mem-contract/index.js';
15
15
  * UNCOMMENTED line is warned about by the loader, so a wrong name here would be a trap rather than a
16
16
  * hint. `config_files.spec.ts` pins the ones this template names.
17
17
  */
18
- export declare const DEFAULT_COMMON_CONFIG = "# avantf-mem \u516C\u5171\u914D\u7F6E\uFF08\u5206\u5C42\u7B2C\u2461\u5C42\uFF09\u3002\u8FD9\u91CC\u5168\u662F\u6CE8\u91CA\uFF1A\u5220\u6389/\u7559\u7A7A = \u7528\u5185\u5EFA\u9ED8\u8BA4\uFF0C\u53D6\u6D88\u6CE8\u91CA\u5373\u751F\u6548\u3002\n# \u5206\u5C42\uFF08\u4F4E\u2192\u9AD8\uFF09\uFF1A\u2460 \u5185\u5EFA\u9ED8\u8BA4 \u2192 \u2461 \u672C\u6587\u4EF6 \u2192 \u2462 \u540C\u76EE\u5F55\u7684 memory.yaml / knowledge.yaml\uFF08\u7F3A\u5931=\u4E0D\u8986\u76D6\uFF09\n# \u2192 \u2463 \u73AF\u5883\u53D8\u91CF \u2192 \u2464 \u63D2\u4EF6/CLI \u663E\u5F0F\u4F20\u53C2\n# \u6240\u6709\u53EF\u7F16\u8F91\u6587\u672C\u90FD\u5728 ~/.avantf \u4E0B\uFF1Aconfigs/*.yaml\uFF08\u672C\u76EE\u5F55\uFF09\u4E0E prompts/*.md\uFF08\u6A21\u578B\u63D0\u793A\u8BCD\uFF09\u3002\n\n# \u6570\u636E\u6839\u4E0D\u5728\u8FD9\u91CC\u914D\uFF1A\u5B83\u7531 \u2463 AVANTF_HOME\uFF08\u6216 CLI --data-home\u3001\u63D2\u4EF6 profile \u7684 dataHome\uFF09\u51B3\u5B9A \u2014\u2014\n# \u672C\u6587\u4EF6\u5C31\u5728\u6570\u636E\u6839\u91CC\u9762\uFF0C\u89E3\u6790\u6839\u7684\u65F6\u5019\u8FD8\u6CA1\u8BFB\u5230\u5B83\uFF0C\u6240\u4EE5\u5199\u5728\u8FD9\u91CC\u7684 dataHome \u5BF9\u6570\u636E\u6839\u65E0\u6548\uFF08DESIGN \u00A73\uFF09\u3002\n\n# semantic: # \u5D4C\u5165\u540E\u7AEF\uFF08\u8BB0\u5FC6\u4E0E\u77E5\u8BC6\u5171\u7528\uFF0C\u8DE8\u5E93\u5206\u6570\u624D\u53EF\u6BD4\uFF09\n# backend: local_bge\n# local_model: Xenova/bge-base-zh-v1.5 # \u5FC5\u987B\u662F ONNX \u4ED3\u5E93\n# dim: 768\n# auto_download: true\n# \u26A0\uFE0F cache_dir / mirror \u7531\u5BB6\u65CF\u5E95\u5EA7 @avantf/dsh-plugin-base \u7BA1\u7406\uFF1A\u5199\u5728\u8FD9\u91CC\u4F1A\u88AB\u5FFD\u7565\u5E76\u544A\u8B66\u3002\n# \u7F13\u5B58\u76EE\u5F55\u7528\u73AF\u5883\u53D8\u91CF AVANTF_MEM_MODEL_CACHE\uFF0C\u955C\u50CF\u7528 AVANTF_MEM_MODEL_MIRROR\uFF08\u6216 HF_ENDPOINT\uFF09\u3002\n\n# rerank: # \u91CD\u6392\u7684\u552F\u4E00\u5F00\u5173\uFF1Bnone = \u4E0D\u52A0\u8F7D\u4EFB\u4F55\u91CD\u6392\u6A21\u578B\n# backend: none\n# local_model: Xenova/bge-reranker-base\n\n# vectorStore:\n# backend: auto # auto = \u8D8A\u8FC7\u9608\u503C\u81EA\u52A8\u4ECE local_numpy \u8FC1\u5230 hnswlib\n# auto_thresholds:\n# hnswlib: 2000\n# hnswlib_ef_search: 256 # ANN \u7684\u53EC\u56DE/\u901F\u5EA6\u65CB\u94AE\uFF0C\u8BED\u6599\u53D8\u5927\u5148\u8C03\u5B83\n\n# retriever: # \u4E09\u6761\u68C0\u7D22\u817F\u5728\u878D\u5408\u91CC\u7684\u6743\u91CD\u4E0E\"\u76F8\u5173\u6027\u95E8\u69DB\"\n# weight_semantic: 0.55\n# weight_fts: 0.30\n# weight_jaccard: 0.15\n# # \u7EDD\u5BF9\u95E8\u69DB\u6253\u5728\u6BCF\u6761\u817F\u7684\u539F\u59CB\u5206\u4E0A\u3001fuse() \u4E4B\u524D\uFF08\u878D\u5408\u5206\u53EA\u5728\u4E00\u6B21\u67E5\u8BE2\u5185\u53EF\u6BD4\uFF0C\u4E0D\u80FD\u5F53\u95E8\u69DB\uFF09\u3002\n# # 0 = \u5173\u95ED\u8BE5\u95E8\u69DB\uFF1B\u5206\u6570\u7B49\u4E8E\u95E8\u69DB\u4FDD\u7559\u3002\u8BED\u4E49\u540E\u7AEF\u4E0D\u53EF\u7528\u65F6 min_fts_terms \u7684\u751F\u6548\u503C\u653E\u5BBD\u5230 1\u3002\n# min_semantic_similarity: 0.5 # \u8BED\u4E49\u817F\u4F59\u5F26\uFF1B0.5 \u53EA\u5BF9 bge-base-zh-v1.5/768\uFF08mean pooling\uFF09\u6807\u5B9A\u8FC7\uFF0C\u6362\u6A21\u578B\u8981\u91CD\u6807\n# min_fts_terms: 2 # FTS \u817F\uFF1A\u8FD9\u4E00\u884C\u547D\u4E2D\u51E0\u4E2A\u4E0D\u540C\u7684\u67E5\u8BE2\u8BCD\u5143\uFF08\u62C9\u4E01\u8BCD\u22655\u5B57\u7B26 + CJK 3-gram\uFF09\n# min_jaccard: 0.2 # \u5B9E\u4F53\u817F Jaccard \u6BD4\u503C\n\n# lifecycle:\n# purge_after_archived_days: 365 # \u5F52\u6863\u4E8B\u5B9E\u7684\u7269\u7406\u6E05\u7406\u7A97\u53E3\uFF08\u6D3B\u8DC3\u65E5\uFF09\n# contradiction_threshold: 0.6\n\n# trust:\n# decay_per_day: 0.0055\u2026\n# presence:\n# heartbeat_minutes: 60 # \u5E38\u9A7B\u8FDB\u7A0B\u7684\u5FC3\u8DF3\uFF08\u6D3B\u8DC3\u65E5\u65F6\u949F\u4E0E\u751F\u547D\u5468\u671F\u626B\u63CF\uFF09\n\n# tools: # \u5916\u90E8\u4E8C\u8FDB\u5236\uFF08pandoc\uFF09\u7684\u53D7\u7BA1\u76EE\u5F55\n# dir: '' # \u7559\u7A7A = \u5BB6\u65CF\u6839 tools\uFF08~/.avantf/env/tools\uFF09\uFF1BAVANTF_TOOLS_DIR \u53EF\u8986\u76D6\n# auto_install: true\n";
18
+ export declare const DEFAULT_COMMON_CONFIG = "# avantf-mem \u516C\u5171\u914D\u7F6E\uFF08\u5206\u5C42\u7B2C\u2461\u5C42\uFF09\u3002\u8FD9\u91CC\u5168\u662F\u6CE8\u91CA\uFF1A\u5220\u6389/\u7559\u7A7A = \u7528\u5185\u5EFA\u9ED8\u8BA4\uFF0C\u53D6\u6D88\u6CE8\u91CA\u5373\u751F\u6548\u3002\n# \u5206\u5C42\uFF08\u4F4E\u2192\u9AD8\uFF09\uFF1A\u2460 \u5185\u5EFA\u9ED8\u8BA4 \u2192 \u2461 \u672C\u6587\u4EF6 \u2192 \u2462 \u540C\u76EE\u5F55\u7684 memory.yaml / knowledge.yaml\uFF08\u7F3A\u5931=\u4E0D\u8986\u76D6\uFF09\n# \u2192 \u2463 \u73AF\u5883\u53D8\u91CF \u2192 \u2464 \u63D2\u4EF6/CLI \u663E\u5F0F\u4F20\u53C2\n# \u6240\u6709\u53EF\u7F16\u8F91\u6587\u672C\u90FD\u5728 ~/.avantf \u4E0B\uFF1Aconfigs/*.yaml\uFF08\u672C\u76EE\u5F55\uFF09\u4E0E prompts/*.md\uFF08\u6A21\u578B\u63D0\u793A\u8BCD\uFF09\u3002\n\n# \u6570\u636E\u6839\u4E0D\u5728\u8FD9\u91CC\u914D\uFF1A\u5B83\u7531 \u2463 AVANTF_HOME\uFF08\u6216 CLI --data-home\u3001\u63D2\u4EF6 profile \u7684 dataHome\uFF09\u51B3\u5B9A \u2014\u2014\n# \u672C\u6587\u4EF6\u5C31\u5728\u6570\u636E\u6839\u91CC\u9762\uFF0C\u89E3\u6790\u6839\u7684\u65F6\u5019\u8FD8\u6CA1\u8BFB\u5230\u5B83\uFF0C\u6240\u4EE5\u5199\u5728\u8FD9\u91CC\u7684 dataHome \u5BF9\u6570\u636E\u6839\u65E0\u6548\uFF08DESIGN \u00A73\uFF09\u3002\n\n# semantic: # \u5D4C\u5165\u540E\u7AEF\uFF08\u8BB0\u5FC6\u4E0E\u77E5\u8BC6\u5171\u7528\uFF0C\u8DE8\u5E93\u5206\u6570\u624D\u53EF\u6BD4\uFF09\n# backend: local_bge\n# local_model: Xenova/bge-base-zh-v1.5 # \u5FC5\u987B\u662F ONNX \u4ED3\u5E93\n# dim: 768\n# auto_download: true\n# auto_migrate: true # \u6362\u6A21\u578B/\u5BBD\u5EA6\u540E\u81EA\u52A8\u5206\u6279\u91CD\u7B97\u65E7\u7A7A\u95F4\u7684\u5411\u91CF\uFF08\u9ED8\u8BA4\u5F00\uFF09\uFF1B\u5173\u6389\u53EA\u5269\u542F\u52A8\u544A\u8B66 + vectors --fix\n# \u26A0\uFE0F cache_dir / mirror \u7531\u5BB6\u65CF\u5E95\u5EA7 @avantf/dsh-plugin-base \u7BA1\u7406\uFF1A\u5199\u5728\u8FD9\u91CC\u4F1A\u88AB\u5FFD\u7565\u5E76\u544A\u8B66\u3002\n# \u7F13\u5B58\u76EE\u5F55\u7528\u73AF\u5883\u53D8\u91CF AVANTF_MEM_MODEL_CACHE\uFF0C\u955C\u50CF\u7528 AVANTF_MEM_MODEL_MIRROR\uFF08\u6216 HF_ENDPOINT\uFF09\u3002\n\n# rerank: # \u91CD\u6392\u7684\u552F\u4E00\u5F00\u5173\uFF1Bnone = \u4E0D\u52A0\u8F7D\u4EFB\u4F55\u91CD\u6392\u6A21\u578B\n# backend: none\n# local_model: Xenova/bge-reranker-base\n\n# vectorStore:\n# backend: auto # auto = \u8D8A\u8FC7\u9608\u503C\u81EA\u52A8\u4ECE local_numpy \u8FC1\u5230 hnswlib\n# auto_thresholds:\n# hnswlib: 2000\n# hnswlib_ef_search: 256 # ANN \u7684\u53EC\u56DE/\u901F\u5EA6\u65CB\u94AE\uFF0C\u8BED\u6599\u53D8\u5927\u5148\u8C03\u5B83\n\n# retriever: # \u4E09\u6761\u68C0\u7D22\u817F\u5728\u878D\u5408\u91CC\u7684\u6743\u91CD\u4E0E\"\u76F8\u5173\u6027\u95E8\u69DB\"\n# weight_semantic: 0.55\n# weight_fts: 0.30\n# weight_jaccard: 0.15\n# # \u7EDD\u5BF9\u95E8\u69DB\u6253\u5728\u6BCF\u6761\u817F\u7684\u539F\u59CB\u5206\u4E0A\u3001fuse() \u4E4B\u524D\uFF08\u878D\u5408\u5206\u53EA\u5728\u4E00\u6B21\u67E5\u8BE2\u5185\u53EF\u6BD4\uFF0C\u4E0D\u80FD\u5F53\u95E8\u69DB\uFF09\u3002\n# # 0 = \u5173\u95ED\u8BE5\u95E8\u69DB\uFF1B\u5206\u6570\u7B49\u4E8E\u95E8\u69DB\u4FDD\u7559\u3002\u8BED\u4E49\u540E\u7AEF\u4E0D\u53EF\u7528\u65F6 min_fts_terms \u7684\u751F\u6548\u503C\u653E\u5BBD\u5230 1\u3002\n# min_semantic_similarity: 0.5 # \u8BED\u4E49\u817F\u4F59\u5F26\uFF1B0.5 \u53EA\u5BF9 bge-base-zh-v1.5/768\uFF08mean pooling\uFF09\u6807\u5B9A\u8FC7\uFF0C\u6362\u6A21\u578B\u8981\u91CD\u6807\n# min_fts_terms: 2 # FTS \u817F\uFF1A\u8FD9\u4E00\u884C\u547D\u4E2D\u51E0\u4E2A\u4E0D\u540C\u7684\u67E5\u8BE2\u8BCD\u5143\uFF08\u62C9\u4E01\u8BCD\u22655\u5B57\u7B26 + CJK 3-gram\uFF09\n# min_jaccard: 0.2 # \u5B9E\u4F53\u817F Jaccard \u6BD4\u503C\n\n# lifecycle:\n# purge_after_archived_days: 365 # \u5F52\u6863\u4E8B\u5B9E\u7684\u7269\u7406\u6E05\u7406\u7A97\u53E3\uFF08\u6D3B\u8DC3\u65E5\uFF09\n# contradiction_threshold: 0.6\n\n# trust:\n# decay_per_day: 0.0055\u2026\n# presence:\n# heartbeat_minutes: 60 # \u5E38\u9A7B\u8FDB\u7A0B\u7684\u5FC3\u8DF3\uFF08\u6D3B\u8DC3\u65E5\u65F6\u949F\u4E0E\u751F\u547D\u5468\u671F\u626B\u63CF\uFF09\n\n# tools: # \u5916\u90E8\u4E8C\u8FDB\u5236\uFF08pandoc\uFF09\u7684\u53D7\u7BA1\u76EE\u5F55\n# dir: '' # \u7559\u7A7A = \u5BB6\u65CF\u6839 tools\uFF08~/.avantf/env/tools\uFF09\uFF1BAVANTF_TOOLS_DIR \u53EF\u8986\u76D6\n# auto_install: true\n";
19
19
  /**
20
20
  * Ensure the common config exists with content, and hand back the path either way.
21
21
  *
@@ -100,6 +100,18 @@ export declare class FactsDao {
100
100
  semantic_vector: Uint8Array;
101
101
  embedding_model: string | null;
102
102
  }[];
103
+ /**
104
+ * Width + recorded space of every ACTIVE persisted vector, WITHOUT reading the bytes.
105
+ *
106
+ * `length(blob)` is the byte count, so `bytes / 4` is the float width — enough to detect a
107
+ * changed `semantic.dim` without decoding a corpus. This is the cheap detection the startup
108
+ * warning and the `/mem` status surface read (see `MemoryStore.vectorSpaceHealth`).
109
+ */
110
+ activeVectorSpaces(): {
111
+ fact_id: number;
112
+ bytes: number;
113
+ embedding_model: string | null;
114
+ }[];
103
115
  /**
104
116
  * Facts per archive reason — the trust diagnostics breakdown. One grouped scan rather than
105
117
  * a count per reason, because the reasons are few but the table is not.
@@ -23,13 +23,19 @@ export declare function vectorSpaceId(backend: string, model: string, dim: numbe
23
23
  * The rows come from the owning store's DAO (DESIGN §19): this helper decodes and
24
24
  * filters them, it never runs SQL itself.
25
25
  *
26
+ * A row whose width does not fit the store is skipped (never `add()`ed — the store
27
+ * would throw). The OWNING store reports that condition itself, as one actionable
28
+ * line with the count, the reason and the manual re-encode entry (`reportStaleVectors`),
29
+ * so this helper stays quiet: two warnings for one condition read as two problems.
30
+ *
26
31
  * @param rows - `id` (fact/chunk id) + `vec` (BLOB), as read back from the store.
27
- * @param label - store name used in the dim-mismatch warning.
32
+ * @param _label - store name; unused here (the owning store owns the warning), kept so call sites read
33
+ * as "which store is reloading".
28
34
  */
29
35
  export declare function reloadVectorIndex(vstore: VectorStore, rows: readonly {
30
36
  id: number;
31
37
  vec: Uint8Array | null;
32
- }[], label: string): {
38
+ }[], _label: string): {
33
39
  loaded: number;
34
40
  skipped: number;
35
41
  };
@@ -4,7 +4,7 @@
4
4
  * Small on purpose: `MemoryStore` and `KnowledgeStore` are separate stores over separate databases
5
5
  * and should stay that way (AGENTS.md). What they must NOT have is two copies of the same
6
6
  * three-line helper, because a fix then lands in one of them — which is exactly what happened to the
7
- * retrieval orchestration (see `store/hybrid.ts`) and to the foreign-vector-space warning below: the
7
+ * retrieval orchestration (see `store/hybrid.ts`) and to the stale-vector warning below: the
8
8
  * memory store checked whether its persisted vectors came from the CURRENT vector space and told the
9
9
  * operator how to fix it, while the knowledge store checked only the width, so a model swap left two
10
10
  * spaces silently mixed in one index until someone happened to run `kb_reindex`.
@@ -40,19 +40,29 @@ export declare function vectorSpaceOf(semantic: SemanticBackend, model: string):
40
40
  */
41
41
  export declare function evictVectors(vstore: VectorStore, ids: readonly number[]): void;
42
42
  /**
43
- * Tell the operator that persisted vectors came from ANOTHER vector space.
43
+ * Tell the operator that persisted vectors came from ANOTHER vector space — loudly, and with the
44
+ * manual remedy.
44
45
  *
45
- * A dim-valid vector from a different space is still RANKED — the reload cannot tell the difference
46
- * — so a silent model swap would mix two spaces in one index and degrade every semantic query
47
- * without an error anywhere. Said once per process; acting on it is the operator's choice because
48
- * re-encoding is a real cost (and they should see the count first).
46
+ * Changing the embedding space (model, width, pooling, normalization) is a DATA MIGRATION: the
47
+ * persisted bytes stay valid-looking but are no longer comparable with the current query vector, so
48
+ * the semantic leg must not serve them. Measured on the real library after the 512→768 default-model
49
+ * swap: 78 of 80 ACTIVE facts fell out of the semantic leg and retrieval silently degraded to
50
+ * lexical+entity (every probe query answered with the same unrelated short fact), with no error
51
+ * anywhere. The count, the reason and the ONE manual entry have to be visible at start.
52
+ *
53
+ * Both halves are reported in one message because they are one condition seen two ways: a changed
54
+ * WIDTH (`stale`, whose bytes cannot even be decoded into the current store) and a changed SPACE at
55
+ * the same width (`space_stale`, whose bytes decode but rank in another model's coordinates).
49
56
  *
50
57
  * @param kind store label for the message (`memory` / `knowledge`)
51
58
  * @param space the vector space the store writes into now
52
- * @param foreign how many persisted vectors were written in a different one
53
- * @param fix the action that re-encodes them, as the operator would type it
59
+ * @param counts how many persisted vectors are unusable in the current space, by kind
60
+ * @param fix the manual entry that re-encodes them, as the operator would type it
54
61
  */
55
- export declare function reportForeignVectors(kind: string, space: string, foreign: number, fix: string): void;
62
+ export declare function reportStaleVectors(kind: string, space: string, counts: {
63
+ stale: number;
64
+ space_stale: number;
65
+ }, fix: string): void;
56
66
  /**
57
67
  * Hand the event loop back between two synchronous batches.
58
68
  *
@@ -52,7 +52,11 @@
52
52
  * FTS REACHABILITY. `min_fts_terms` counts distinct query terms a row hits, but a query can only
53
53
  * produce so many terms (`relevanceTerms`: latin words ≥5 chars + CJK 3-grams) — a 3-character CJK
54
54
  * query yields exactly ONE trigram, so a configured bar of 2 is unreachable and that leg is empty
55
- * for it by construction. The effective bar is therefore `min(configured, termCount)`.
55
+ * for it by construction. The effective bar is therefore `min(configured, termCount)`, and
56
+ * `termCount` is that of the text being GRADED — with 方案 A's augmentation the original and the
57
+ * rewrite are different texts with different counts, so each resolves its own bar
58
+ * (`store/hybrid.ts`). Resolving the rewrite from the original's count is the defect that let a
59
+ * single incidental trigram pass a bar it could not reach (2026-10-04).
56
60
  *
57
61
  * @module store/floors
58
62
  */
@@ -117,16 +121,24 @@ export declare function emptyFloorDrops(): RetrievalFloorDrops;
117
121
  * and changing the REPORTED value there would misdescribe what the caller is reading. `0` stays `0`
118
122
  * (the operator's off switch).
119
123
  *
120
- * This is the ONE place the clamp lives; `resolveFloors` (what the result reports) and
121
- * `applyTermFloor` (what the leg does) both call it, so the number the caller reads is the number
122
- * the leg used.
124
+ * This is the ONE place the clamp lives; `resolveFloors` (what the result reports / a run is handed)
125
+ * and `applyTermFloor` (what the leg does) both call it, so the number a run reads is the number its
126
+ * leg used. It is a `min`, i.e. one-directional: it can LOWER an unreachable configured bar but never
127
+ * RAISE a bar inherited from another text, which is why the caller must resolve per graded text
128
+ * rather than reusing one text's resolution for another.
123
129
  */
124
130
  export declare function effectiveTermFloor(floor: number, termCount?: number): number;
125
131
  /** The per-query floor resolution inputs beyond the configuration. */
126
132
  export interface FloorResolution {
127
133
  /** `loose` lowers each configured floor (see {@link LOOSE_FLOORS}); absent means `strict`. */
128
134
  profile?: FloorProfile;
129
- /** This query's distinct term count (`relevanceTerms(query).length`), for the FTS clamp. */
135
+ /**
136
+ * The distinct term count of the text being GRADED (`relevanceTerms(text).length`), for the FTS
137
+ * clamp. It must describe the query the legs will actually run on: under 方案 A's augmentation the
138
+ * rewrite is a DIFFERENT text with its own term count, and resolving both runs from the original's
139
+ * count is the defect that let one incidental trigram through (see `store/hybrid.ts`). Callers with
140
+ * several graded texts resolve once per text.
141
+ */
130
142
  termCount?: number;
131
143
  /**
132
144
  * Which legs a `'loose'` profile may lower, from {@link droppedLegs} of the pass that made the
@@ -1,4 +1,4 @@
1
- import { type Config, type ContradictionRecord, type FactDetail, type FactPage, type FloorProfile, type RecallResult, type RememberResult, type TrustDiagnostic, type VectorsDiagnostic, type VectorsFixReport } from '../../mem-contract/index.js';
1
+ import { type Config, type ContradictionRecord, type FactDetail, type FactPage, type FloorProfile, type RecallResult, type RememberResult, type TrustDiagnostic, type VectorMigrationOutcome, type VectorMigrationProgress, type VectorSpaceHealth, type VectorsDiagnostic, type VectorsFixReport } from '../../mem-contract/index.js';
2
2
  import type { Reranker, SemanticBackend, VectorStore } from '../../mem-retrieval/index.js';
3
3
  import type { Db } from '../db/conn.js';
4
4
  import { type LexicalProbe } from './lexical.js';
@@ -21,6 +21,16 @@ export type { DetectedContradiction, RememberResult as AddResult } from '../../m
21
21
  * until `deferred === 0` instead.
22
22
  */
23
23
  export declare const ENTITY_SWEEP_BATCH = 2000;
24
+ /**
25
+ * Rows one background vector-space migration slice may drop + re-encode.
26
+ *
27
+ * Small on purpose: the point of batching is that a query arriving mid-migration waits for at most
28
+ * one ONNX forward pass batch, not for a whole corpus. At ~30 ms per bge-base-zh encode a batch of 16
29
+ * is well under a second of contested model time, and the event loop is yielded between batches
30
+ * (`yieldToEventLoop`). The value is a default, not a promise — `migrateVectorsBatch(batchSize)` takes
31
+ * whatever the caller needs.
32
+ */
33
+ export declare const DEFAULT_VECTOR_MIGRATION_BATCH = 16;
24
34
  /** What one derived-state sweep pass did (see `MemoryStore.reindexEntities`). */
25
35
  export interface EntitySweepReport {
26
36
  /**
@@ -173,6 +183,16 @@ export declare class MemoryStore {
173
183
  * `archive()` evicts and `restore()` re-adds, keeping the invariant.
174
184
  */
175
185
  private reloadIndex;
186
+ /**
187
+ * Cheapest possible DETECTION of "the embedding space changed under this store": counts of ACTIVE
188
+ * persisted vectors whose width or recorded space id no longer matches the one this store writes
189
+ * into. Reads blob LENGTHS only — never decodes — so the `/mem` status surface and the startup
190
+ * warning can afford it at any corpus size.
191
+ *
192
+ * The counts are the same two `VectorsDiagnostic` already separates (`stale` / `space_stale`); this
193
+ * is the read model those surfaces use instead of the full diagnose pass.
194
+ */
195
+ vectorSpaceHealth(): VectorSpaceHealth;
176
196
  add(content: string, category?: string, ttlDays?: number): Promise<RememberResult>;
177
197
  /**
178
198
  * Write-path contradiction detection (DESIGN §11): check the fact that was just
@@ -317,7 +337,7 @@ export declare class MemoryStore {
317
337
  */
318
338
  private classifyVectors;
319
339
  /**
320
- * Repair the semantic read model:
340
+ * Repair the semantic read model, in ONE bounded slice or in full:
321
341
  * (a) reload persisted, dim-valid vectors missing from the live index
322
342
  * (restart / index loss — needs no model),
323
343
  * (b) drop persisted vectors whose dim no longer matches `semantic.dim` so a
@@ -325,12 +345,46 @@ export declare class MemoryStore {
325
345
  * throws on dim mismatch),
326
346
  * (c) encode active facts that have no vector yet (needs the semantic backend),
327
347
  * (d) re-encode vectors written in ANOTHER vector space (a model swap, or the one-time
328
- * adoption of a store predating the space id). Deliberately here and not at open: this is
329
- * the only path that re-encodes the whole table, so it is an explicit, counted action.
348
+ * adoption of a store predating the space id).
349
+ *
350
+ * `limit` is what makes the SAME logic usable as a background migration: at most that many rows are
351
+ * dropped and at most that many are re-encoded, so one call is a bounded unit of work the caller can
352
+ * yield between. `vectorsFix` passes no limit (the explicit operator action drains everything);
353
+ * `migrateVectorsBatch` passes the migration's batch size.
354
+ */
355
+ private repairVectors;
356
+ /**
357
+ * The explicit, unbounded repair behind `mem_admin vectors_fix` / `avantf-mem vectors --fix`.
330
358
  * `dry_run` previews every count without writing — and without loading the model, which is why
331
359
  * the preview also reports `would_warm` (see the contract's `VectorsFixReport`).
332
360
  */
333
361
  vectorsFix(dryRun?: boolean): Promise<VectorsFixReport>;
362
+ /**
363
+ * ONE bounded slice of the vector-space migration: at most `batchSize` rows are dropped and
364
+ * re-encoded, so the caller keeps control of the event loop between slices. This is the unit the
365
+ * background migration ({@link migrateVectors}) and its tests drive.
366
+ *
367
+ * Resumability is a property of the DATABASE, not of this process: every slice re-derives what is
368
+ * stale from the persisted width/space, so a restart simply continues where the last slice stopped.
369
+ */
370
+ migrateVectorsBatch(batchSize?: number): Promise<VectorMigrationProgress>;
371
+ /**
372
+ * Drive the bounded background migration until the store is current, the model is unavailable, or
373
+ * `shouldStop()` turns true (plugin unmount / process shutdown). A batch FAILURE never rejects: it is
374
+ * logged and reported as `remaining > 0`, which is what makes the next heartbeat a retry (the caller
375
+ * still owns the outer catch for a closed database, e.g. a shutdown racing the first read). Never
376
+ * blocks the event loop for longer than one batch.
377
+ *
378
+ * Honours `semantic.auto_migrate` (default on) — the switch exists so a host that must not spend CPU
379
+ * on background re-encoding can leave the loud warning and the manual entry as the only path.
380
+ */
381
+ migrateVectors(opts?: {
382
+ batchSize?: number;
383
+ shouldStop?: () => boolean;
384
+ onProgress?: (progress: VectorMigrationOutcome) => void;
385
+ }): Promise<VectorMigrationOutcome>;
386
+ /** Active rows the semantic leg still cannot use (old width, old space, or no vector at all). */
387
+ private migrationRemaining;
334
388
  /**
335
389
  * One lifecycle pass, owned by the store so the DB writes and the live-index
336
390
  * invariant (ACTIVE vectors only) are maintained in ONE place — the runtime no
@@ -21,6 +21,7 @@ export declare const ConfigSchema: z.ZodObject<{
21
21
  mirror: z.ZodDefault<z.ZodString>;
22
22
  cache_dir: z.ZodDefault<z.ZodString>;
23
23
  auto_download: z.ZodDefault<z.ZodBoolean>;
24
+ auto_migrate: z.ZodDefault<z.ZodBoolean>;
24
25
  }, z.core.$strip>>;
25
26
  rerank: z.ZodPrefault<z.ZodObject<{
26
27
  backend: z.ZodDefault<z.ZodString>;
@@ -299,11 +299,29 @@ export interface RetrievalHealthSummary {
299
299
  updated_at: string | null;
300
300
  by_kind: Record<string, KindHealth>;
301
301
  }
302
+ /**
303
+ * Persisted-vector health of the ACTIVE corpus, relative to the vector space the store writes into
304
+ * NOW — the DETECTION half of "changing the embedding space is a data migration" (DESIGN §20).
305
+ *
306
+ * Deliberately cheap: it compares the recorded width and space id only, never decoding the blobs, so
307
+ * the `/mem` status surface can read it on every poll without loading a whole corpus.
308
+ */
309
+ export interface VectorSpaceHealth {
310
+ /** Active facts whose persisted vector has a different width than `semantic.dim`. */
311
+ stale: number;
312
+ /** Active facts whose persisted vector is provably from another model space (same width). */
313
+ space_stale: number;
314
+ }
302
315
  /** `mem_admin stats`: store counts plus the retrieval-health counters. */
303
316
  export interface StatsSummary {
304
317
  active: number;
305
318
  archived: number;
306
319
  retrieval: RetrievalHealthSummary;
320
+ /**
321
+ * Vector-space health, so "retrieval silently degraded after a model upgrade" is visible from the
322
+ * status surface instead of only from a one-shot startup warning.
323
+ */
324
+ vectors: VectorSpaceHealth;
307
325
  }
308
326
  /** One page of facts (`mem_admin list`). */
309
327
  export interface FactPage {
@@ -347,10 +365,42 @@ export interface VectorsDiagnostic {
347
365
  space_stale: number;
348
366
  indexed: number;
349
367
  unindexed: number;
368
+ /**
369
+ * Distinct recorded spaces across the WHOLE table (archived rows included), while `stale` /
370
+ * `space_stale` count only ACTIVE rows — an archived row legitimately keeps its old vector, so the
371
+ * two must not be compared directly.
372
+ */
350
373
  models: Record<string, number>;
351
374
  /** Which backend is actually serving reads (`auto:local_numpy` → `auto:hnswlib`). */
352
375
  store: string;
353
376
  }
377
+ /**
378
+ * One BOUNDED slice of the vector-space migration (`MemoryStore.migrateVectorsBatch`).
379
+ *
380
+ * The migration is what turns "the default embedder changed" from a silent degradation into a
381
+ * resumable background repair: each call re-encodes at most `batchSize` rows, and the remaining
382
+ * count is what the next call (or the next process, after a restart) resumes from.
383
+ */
384
+ export interface VectorMigrationProgress {
385
+ /** Active rows still not usable by the semantic leg after this slice (old width/space, or no vector). */
386
+ remaining: number;
387
+ /** Rows re-encoded into the current space by this slice. */
388
+ migrated: number;
389
+ /** Rows whose old-space bytes were dropped so a re-encode could replace them. */
390
+ dropped: number;
391
+ /** Usable persisted vectors re-added to the live index (no model needed). */
392
+ reindexed: number;
393
+ /** `false` when the model was not loaded: nothing could be encoded this slice. */
394
+ semantic_available: boolean;
395
+ }
396
+ /**
397
+ * The outcome of a whole migration drive (`MemoryStore.migrateVectors`): the last slice plus
398
+ * cumulative counters, and whether the `semantic.auto_migrate` switch allowed any work at all.
399
+ */
400
+ export interface VectorMigrationOutcome extends VectorMigrationProgress {
401
+ /** `false` when `semantic.auto_migrate` is off — nothing was migrated on purpose. */
402
+ enabled: boolean;
403
+ }
354
404
  /**
355
405
  * `mem_admin vectors_fix`: what a repair did, or — with `dry_run` — what it WOULD do.
356
406
  *
package/lib/index.js CHANGED
@@ -121,7 +121,19 @@ const semanticSchema = z.object({
121
121
  max_input_tokens: maxInputTokens,
122
122
  mirror: z.string().default("https://hf-mirror.com"),
123
123
  cache_dir: z.string().default(""),
124
- auto_download: z.boolean().default(true)
124
+ auto_download: z.boolean().default(true),
125
+ /**
126
+ * Migrate persisted vectors that belong to an OLDER vector space (a changed model or width) in a
127
+ * bounded background pass. Default ON: changing the embedding space IS a data migration, and
128
+ * without this the semantic leg silently skips every old-space vector while lexical+entity keep
129
+ * answering — measured on the real library after the 512→768 default-model swap, 78 of 80 ACTIVE
130
+ * facts became semantically unreachable with no error anywhere.
131
+ *
132
+ * Off = the loud startup warning and the manual entry (`vectors --fix` / `mem_admin vectors_fix`)
133
+ * stay, but nothing re-encodes on its own. Set to `false` on a metered/embedded host that must not
134
+ * spend CPU on background re-encoding.
135
+ */
136
+ auto_migrate: z.boolean().default(true)
125
137
  });
126
138
  const rerankSchema = z.object({
127
139
  backend: z.string().default("none"),
@@ -1416,6 +1428,7 @@ const DEFAULT_COMMON_CONFIG = `# avantf-mem 公共配置(分层第②层)。
1416
1428
  # local_model: Xenova/bge-base-zh-v1.5 # 必须是 ONNX 仓库
1417
1429
  # dim: 768
1418
1430
  # auto_download: true
1431
+ # auto_migrate: true # 换模型/宽度后自动分批重算旧空间的向量(默认开);关掉只剩启动告警 + vectors --fix
1419
1432
  # ⚠️ cache_dir / mirror 由家族底座 @avantf/dsh-plugin-base 管理:写在这里会被忽略并告警。
1420
1433
  # 缓存目录用环境变量 AVANTF_MEM_MODEL_CACHE,镜像用 AVANTF_MEM_MODEL_MIRROR(或 HF_ENDPOINT)。
1421
1434
 
@@ -6910,9 +6923,11 @@ function emptyFloorDrops() {
6910
6923
  * and changing the REPORTED value there would misdescribe what the caller is reading. `0` stays `0`
6911
6924
  * (the operator's off switch).
6912
6925
  *
6913
- * This is the ONE place the clamp lives; `resolveFloors` (what the result reports) and
6914
- * `applyTermFloor` (what the leg does) both call it, so the number the caller reads is the number
6915
- * the leg used.
6926
+ * This is the ONE place the clamp lives; `resolveFloors` (what the result reports / a run is handed)
6927
+ * and `applyTermFloor` (what the leg does) both call it, so the number a run reads is the number its
6928
+ * leg used. It is a `min`, i.e. one-directional: it can LOWER an unreachable configured bar but never
6929
+ * RAISE a bar inherited from another text, which is why the caller must resolve per graded text
6930
+ * rather than reusing one text's resolution for another.
6916
6931
  */
6917
6932
  function effectiveTermFloor(floor, termCount) {
6918
6933
  if (!(floor > 0)) return floor;
@@ -7128,9 +7143,21 @@ async function hybridSearch(deps, plan) {
7128
7143
  * The corpus writes the user in the third person and the user asks in the first, so a
7129
7144
  * first-person question shares no lexical term with the fact that answers it and the semantic leg
7130
7145
  * has to carry the whole decision. The intent table (`store/self_query.ts`) supplies ONE canonical
7131
- * third-person rewrite; the ORIGINAL query and the rewrite each run the full leg set, and each
7132
- * leg's raw scores are UNIONED (a candidate in both keeps its MAX score — this is not a second
7133
- * leg, so it must not double the leg's weight).
7146
+ * third-person rewrite and the ORIGINAL query and the rewrite each run the full leg set.
7147
+ *
7148
+ * THE FLOOR IS RESOLVED PER RUN (0.4.2). Each run is graded by ITS OWN reachability clamp —
7149
+ * `min(configured, relevanceTerms(variant).length)` — because a variant with MORE terms has a
7150
+ * HIGHER bar, and grading it at the original's lower bar is what let a single incidental trigram
7151
+ * from the rewrite become that leg's head. Measured on the live store: `我是谁?` yields ONE term
7152
+ * (`我是谁`) while its rewrite `用户是谁` yields TWO (`用户是` / `户是谁`), and the rewrite's
7153
+ * `用户是` is a literal substring of an unrelated long note — that note then took the FTS leg's
7154
+ * head (weight × 1.0) and outvoted the identity fact, which the rewrite had correctly put first.
7155
+ * `applyTermFloor` re-clamps to the query it is grading, but the clamp is one-directional
7156
+ * (`min`): it can only LOWER an unreachable bar, never RAISE a bar inherited from another text, so
7157
+ * the resolution has to happen per variant (see {@link resolveFloors}). Each variant's own bar is
7158
+ * then floored at the reported one, so every run is graded at or ABOVE what the envelope says: the
7159
+ * envelope reports the ORIGINAL query's floors (the question the user actually asked) and can never
7160
+ * overstate the bar a run was held to.
7134
7161
  *
7135
7162
  * WHY AUGMENT, NOT REPLACE. The table is a closed cue list over a language with unbounded
7136
7163
  * self-reference variants, so a wrong match is inevitable. Under augmentation a false positive can
@@ -7139,10 +7166,16 @@ async function hybridSearch(deps, plan) {
7139
7166
  * asked. `queries.length === 1` (the overwhelming majority) leaves the run below bit-identical to
7140
7167
  * the pre-A path: one `deps.legs` call, same context, same vector wiring.
7141
7168
  *
7142
- * The floors are resolved ONCE from the ORIGINAL query and applied to both runs. That is what
7143
- * makes "never narrows" structural rather than a promise: the merged per-leg score is
7144
- * `max(original, rewrite) >= original`, and a floor only removes entries, so any candidate the
7145
- * original query cleared its floor with still clears it in the union.
7169
+ * FLOORS, `relaxed` AND `dropped_by_floor` UNDER AUGMENTATION. The PROFILE is resolved once per
7170
+ * pass from the ORIGINAL query and handed to both runs; only the per-variant reachability clamp
7171
+ * above differs, and it can only raise. Every leg of every run is therefore judged by AT LEAST the
7172
+ * numbers the result reports — the envelope can never say "strict" while a rewrite run answered from
7173
+ * the relaxed band (the distortion this design exists to rule out). `relaxed` is decided exactly as
7174
+ * it was: the automatic loose pass fires only when the strict pass returned NOTHING with drops, and
7175
+ * it re-runs BOTH variants under the loosened floors. Because a leg only ever REMOVES entries, the
7176
+ * per-leg merge of the two runs' drop counts uses `max` (summing would count the same candidate
7177
+ * twice) and `capped` uses OR — both conservative, neither inflates the report that feeds the retry
7178
+ * decision.
7146
7179
  */
7147
7180
  const augment = (plan.rewriteQuery ?? selfQueryRewrite)(query);
7148
7181
  const queries = augment !== void 0 && augment !== query ? [query, augment] : [query];
@@ -7181,6 +7214,15 @@ async function hybridSearch(deps, plan) {
7181
7214
  const runs = await Promise.all(queries.map(async (variant, index) => {
7182
7215
  const isOriginal = index === 0;
7183
7216
  const vector = isOriginal ? passVector : augmentVectors.get(variant);
7217
+ const ownFloors = isOriginal ? floors : resolveFloors(retriever, semAvail, {
7218
+ profile,
7219
+ termCount: relevanceTerms(variant).length,
7220
+ ...relaxLegs === void 0 ? {} : { relaxLegs }
7221
+ });
7222
+ const variantFloors = ownFloors === floors ? floors : {
7223
+ ...ownFloors,
7224
+ fts: Math.max(floors.fts, ownFloors.fts)
7225
+ };
7184
7226
  return runLegs(deps, {
7185
7227
  query: variant,
7186
7228
  limit,
@@ -7188,7 +7230,7 @@ async function hybridSearch(deps, plan) {
7188
7230
  legCap,
7189
7231
  semAvail,
7190
7232
  weights,
7191
- floors,
7233
+ floors: variantFloors,
7192
7234
  ...vector === void 0 ? {} : { queryVector: vector },
7193
7235
  onQueryVector: (vec) => {
7194
7236
  if (isOriginal) passVector = vec;
@@ -7363,10 +7405,16 @@ function vectorSpaceId(backend, model, dim) {
7363
7405
  * The rows come from the owning store's DAO (DESIGN §19): this helper decodes and
7364
7406
  * filters them, it never runs SQL itself.
7365
7407
  *
7408
+ * A row whose width does not fit the store is skipped (never `add()`ed — the store
7409
+ * would throw). The OWNING store reports that condition itself, as one actionable
7410
+ * line with the count, the reason and the manual re-encode entry (`reportStaleVectors`),
7411
+ * so this helper stays quiet: two warnings for one condition read as two problems.
7412
+ *
7366
7413
  * @param rows - `id` (fact/chunk id) + `vec` (BLOB), as read back from the store.
7367
- * @param label - store name used in the dim-mismatch warning.
7414
+ * @param _label - store name; unused here (the owning store owns the warning), kept so call sites read
7415
+ * as "which store is reloading".
7368
7416
  */
7369
- function reloadVectorIndex(vstore, rows, label) {
7417
+ function reloadVectorIndex(vstore, rows, _label) {
7370
7418
  const entries = [];
7371
7419
  let skipped = 0;
7372
7420
  for (const r of rows) {
@@ -7377,7 +7425,6 @@ function reloadVectorIndex(vstore, rows, label) {
7377
7425
  });
7378
7426
  else skipped++;
7379
7427
  }
7380
- if (skipped > 0) retrievalLogger().warn(`${label} index reload: skipped ${skipped} vector(s) with dim != ${vstore.dim}`);
7381
7428
  vstore.rebuild(entries);
7382
7429
  return {
7383
7430
  loaded: entries.length,
@@ -7442,21 +7489,32 @@ function evictVectors(vstore, ids) {
7442
7489
  else for (const id of ids) vstore.remove(id);
7443
7490
  }
7444
7491
  /**
7445
- * Tell the operator that persisted vectors came from ANOTHER vector space.
7492
+ * Tell the operator that persisted vectors came from ANOTHER vector space — loudly, and with the
7493
+ * manual remedy.
7494
+ *
7495
+ * Changing the embedding space (model, width, pooling, normalization) is a DATA MIGRATION: the
7496
+ * persisted bytes stay valid-looking but are no longer comparable with the current query vector, so
7497
+ * the semantic leg must not serve them. Measured on the real library after the 512→768 default-model
7498
+ * swap: 78 of 80 ACTIVE facts fell out of the semantic leg and retrieval silently degraded to
7499
+ * lexical+entity (every probe query answered with the same unrelated short fact), with no error
7500
+ * anywhere. The count, the reason and the ONE manual entry have to be visible at start.
7446
7501
  *
7447
- * A dim-valid vector from a different space is still RANKED — the reload cannot tell the difference
7448
- * — so a silent model swap would mix two spaces in one index and degrade every semantic query
7449
- * without an error anywhere. Said once per process; acting on it is the operator's choice because
7450
- * re-encoding is a real cost (and they should see the count first).
7502
+ * Both halves are reported in one message because they are one condition seen two ways: a changed
7503
+ * WIDTH (`stale`, whose bytes cannot even be decoded into the current store) and a changed SPACE at
7504
+ * the same width (`space_stale`, whose bytes decode but rank in another model's coordinates).
7451
7505
  *
7452
7506
  * @param kind store label for the message (`memory` / `knowledge`)
7453
7507
  * @param space the vector space the store writes into now
7454
- * @param foreign how many persisted vectors were written in a different one
7455
- * @param fix the action that re-encodes them, as the operator would type it
7508
+ * @param counts how many persisted vectors are unusable in the current space, by kind
7509
+ * @param fix the manual entry that re-encodes them, as the operator would type it
7456
7510
  */
7457
- function reportForeignVectors(kind, space, foreign, fix) {
7458
- if (foreign <= 0) return;
7459
- retrievalLogger().warn(`${kind} index: ${String(foreign)} vector(s) were written in another space than ${space} — they are still ranked; run ${fix} to re-encode them`);
7511
+ function reportStaleVectors(kind, space, counts, fix) {
7512
+ const total = counts.stale + counts.space_stale;
7513
+ if (total <= 0) return;
7514
+ const reasons = [];
7515
+ if (counts.stale > 0) reasons.push(`${String(counts.stale)} with a width that no longer matches`);
7516
+ if (counts.space_stale > 0) reasons.push(`${String(counts.space_stale)} written by another model`);
7517
+ retrievalLogger().warn(`${kind} index: ${String(total)} persisted vector(s) belong to an OLDER embedding space than ${space} (${reasons.join("; ")}) — the semantic leg cannot use them, so retrieval degrades to lexical+entity (this is a data migration, not a transient error); run ${fix} to re-encode them`);
7460
7518
  }
7461
7519
  /**
7462
7520
  * Hand the event loop back between two synchronous batches.
@@ -8038,6 +8096,17 @@ var FactsDao = class FactsDao {
8038
8096
  WHERE semantic_vector IS NOT NULL AND status = 'active'`).all();
8039
8097
  }
8040
8098
  /**
8099
+ * Width + recorded space of every ACTIVE persisted vector, WITHOUT reading the bytes.
8100
+ *
8101
+ * `length(blob)` is the byte count, so `bytes / 4` is the float width — enough to detect a
8102
+ * changed `semantic.dim` without decoding a corpus. This is the cheap detection the startup
8103
+ * warning and the `/mem` status surface read (see `MemoryStore.vectorSpaceHealth`).
8104
+ */
8105
+ activeVectorSpaces() {
8106
+ return this.db.prepare(`SELECT fact_id, length(semantic_vector) AS bytes, embedding_model FROM facts
8107
+ WHERE semantic_vector IS NOT NULL AND status = 'active'`).all();
8108
+ }
8109
+ /**
8041
8110
  * Facts per archive reason — the trust diagnostics breakdown. One grouped scan rather than
8042
8111
  * a count per reason, because the reasons are few but the table is not.
8043
8112
  */
@@ -9585,13 +9654,38 @@ var MemoryStore = class {
9585
9654
  */
9586
9655
  reloadIndex() {
9587
9656
  const space = this.vectorSpace();
9588
- const persisted = this.facts.activeVectorRows();
9589
- const rows = persisted.map((r) => ({
9657
+ const rows = this.facts.activeVectorRows().map((r) => ({
9590
9658
  id: r.fact_id,
9591
9659
  vec: r.semantic_vector
9592
9660
  }));
9593
9661
  reloadVectorIndex(this.vstore, rows, "memory");
9594
- reportForeignVectors("memory", space, persisted.filter((r) => r.embedding_model !== space).length, "mem_admin vectors_fix (dry_run=true previews the count)");
9662
+ reportStaleVectors("memory", space, this.vectorSpaceHealth(), "`avantf-mem vectors --fix` (MCP/plugin: `mem_admin vectors_fix`)");
9663
+ }
9664
+ /**
9665
+ * Cheapest possible DETECTION of "the embedding space changed under this store": counts of ACTIVE
9666
+ * persisted vectors whose width or recorded space id no longer matches the one this store writes
9667
+ * into. Reads blob LENGTHS only — never decodes — so the `/mem` status surface and the startup
9668
+ * warning can afford it at any corpus size.
9669
+ *
9670
+ * The counts are the same two `VectorsDiagnostic` already separates (`stale` / `space_stale`); this
9671
+ * is the read model those surfaces use instead of the full diagnose pass.
9672
+ */
9673
+ vectorSpaceHealth() {
9674
+ const space = this.vectorSpace();
9675
+ const expectedBytes = this.vstore.dim * 4;
9676
+ let stale = 0;
9677
+ let spaceStale = 0;
9678
+ for (const row of this.facts.activeVectorSpaces()) {
9679
+ if (row.bytes !== expectedBytes) {
9680
+ stale++;
9681
+ continue;
9682
+ }
9683
+ if (row.embedding_model !== space) spaceStale++;
9684
+ }
9685
+ return {
9686
+ stale,
9687
+ space_stale: spaceStale
9688
+ };
9595
9689
  }
9596
9690
  async add(content, category, ttlDays) {
9597
9691
  const normalized = normalizeWrite(content).trim();
@@ -10020,7 +10114,7 @@ var MemoryStore = class {
10020
10114
  };
10021
10115
  }
10022
10116
  /**
10023
- * Repair the semantic read model:
10117
+ * Repair the semantic read model, in ONE bounded slice or in full:
10024
10118
  * (a) reload persisted, dim-valid vectors missing from the live index
10025
10119
  * (restart / index loss — needs no model),
10026
10120
  * (b) drop persisted vectors whose dim no longer matches `semantic.dim` so a
@@ -10028,64 +10122,162 @@ var MemoryStore = class {
10028
10122
  * throws on dim mismatch),
10029
10123
  * (c) encode active facts that have no vector yet (needs the semantic backend),
10030
10124
  * (d) re-encode vectors written in ANOTHER vector space (a model swap, or the one-time
10031
- * adoption of a store predating the space id). Deliberately here and not at open: this is
10032
- * the only path that re-encodes the whole table, so it is an explicit, counted action.
10033
- * `dry_run` previews every count without writing — and without loading the model, which is why
10034
- * the preview also reports `would_warm` (see the contract's `VectorsFixReport`).
10125
+ * adoption of a store predating the space id).
10126
+ *
10127
+ * `limit` is what makes the SAME logic usable as a background migration: at most that many rows are
10128
+ * dropped and at most that many are re-encoded, so one call is a bounded unit of work the caller can
10129
+ * yield between. `vectorsFix` passes no limit (the explicit operator action drains everything);
10130
+ * `migrateVectorsBatch` passes the migration's batch size.
10035
10131
  */
10036
- async vectorsFix(dryRun = false) {
10132
+ async repairVectors(opts) {
10133
+ const limit = opts.limit ?? Number.POSITIVE_INFINITY;
10037
10134
  const c = this.classifyVectors();
10038
10135
  const loadedNow = this.semantic.isAvailable();
10039
10136
  let semAvailable = loadedNow;
10040
- if (!semAvailable && !dryRun) semAvailable = await this.warmupSemantic();
10137
+ if (!semAvailable && !opts.dryRun) semAvailable = await this.warmupSemantic();
10041
10138
  const wouldWarm = !loadedNow;
10042
- if (dryRun) return {
10043
- missing: c.missing,
10044
- stale: c.staleIds.length,
10045
- space_stale: c.spaceStaleIds.length,
10046
- unindexed: c.unindexed.length,
10047
- reindexed: 0,
10048
- dropped: 0,
10049
- fixed: 0,
10050
- semantic_available: semAvailable,
10051
- would_warm: wouldWarm,
10052
- dry_run: true
10139
+ const remainingOf = (fixed) => Math.max(0, c.staleIds.length + c.spaceStaleIds.length + c.missing - fixed);
10140
+ if (opts.dryRun) return {
10141
+ report: {
10142
+ missing: c.missing,
10143
+ stale: c.staleIds.length,
10144
+ space_stale: c.spaceStaleIds.length,
10145
+ unindexed: c.unindexed.length,
10146
+ reindexed: 0,
10147
+ dropped: 0,
10148
+ fixed: 0,
10149
+ semantic_available: semAvailable,
10150
+ would_warm: wouldWarm,
10151
+ dry_run: true
10152
+ },
10153
+ remaining: remainingOf(0)
10053
10154
  };
10054
10155
  let reindexed = 0;
10055
- for (const u of c.unindexed) {
10156
+ for (const u of c.unindexed.slice(0, limit)) {
10056
10157
  this.vstore.add(u.id, u.vec);
10057
10158
  reindexed++;
10058
10159
  }
10059
10160
  let dropped = 0;
10060
10161
  if (semAvailable) {
10061
- const toDrop = [...c.staleIds, ...c.spaceStaleIds];
10162
+ const toDrop = [...c.staleIds, ...c.spaceStaleIds].slice(0, limit);
10062
10163
  if (toDrop.length) {
10164
+ const spaceStale = new Set(c.spaceStaleIds);
10063
10165
  dropped = this.facts.clearVectors(toDrop);
10064
- this.evictVectors(c.spaceStaleIds);
10166
+ this.evictVectors(toDrop.filter((id) => spaceStale.has(id)));
10065
10167
  }
10066
10168
  }
10067
10169
  let fixed = 0;
10068
10170
  if (semAvailable) {
10069
- const missingRows = this.facts.missingVectorRows();
10171
+ const missingRows = this.facts.missingVectorRows().slice(0, limit);
10070
10172
  for (const m of missingRows) {
10071
10173
  if (!await this.maybeIndexSemantic(m.fact_id, m.content)) continue;
10072
10174
  fixed++;
10073
10175
  }
10074
10176
  }
10075
10177
  return {
10076
- missing: c.missing,
10077
- stale: c.staleIds.length,
10078
- space_stale: c.spaceStaleIds.length,
10079
- unindexed: c.unindexed.length,
10080
- reindexed,
10081
- dropped,
10082
- fixed,
10083
- semantic_available: semAvailable,
10084
- would_warm: wouldWarm,
10085
- dry_run: false
10178
+ report: {
10179
+ missing: c.missing,
10180
+ stale: c.staleIds.length,
10181
+ space_stale: c.spaceStaleIds.length,
10182
+ unindexed: c.unindexed.length,
10183
+ reindexed,
10184
+ dropped,
10185
+ fixed,
10186
+ semantic_available: semAvailable,
10187
+ would_warm: wouldWarm,
10188
+ dry_run: false
10189
+ },
10190
+ remaining: remainingOf(fixed)
10086
10191
  };
10087
10192
  }
10088
10193
  /**
10194
+ * The explicit, unbounded repair behind `mem_admin vectors_fix` / `avantf-mem vectors --fix`.
10195
+ * `dry_run` previews every count without writing — and without loading the model, which is why
10196
+ * the preview also reports `would_warm` (see the contract's `VectorsFixReport`).
10197
+ */
10198
+ async vectorsFix(dryRun = false) {
10199
+ return (await this.repairVectors({ dryRun })).report;
10200
+ }
10201
+ /**
10202
+ * ONE bounded slice of the vector-space migration: at most `batchSize` rows are dropped and
10203
+ * re-encoded, so the caller keeps control of the event loop between slices. This is the unit the
10204
+ * background migration ({@link migrateVectors}) and its tests drive.
10205
+ *
10206
+ * Resumability is a property of the DATABASE, not of this process: every slice re-derives what is
10207
+ * stale from the persisted width/space, so a restart simply continues where the last slice stopped.
10208
+ */
10209
+ async migrateVectorsBatch(batchSize = 16) {
10210
+ const size = Math.max(1, Math.floor(batchSize));
10211
+ const { report, remaining } = await this.repairVectors({
10212
+ dryRun: false,
10213
+ limit: size
10214
+ });
10215
+ return {
10216
+ remaining,
10217
+ migrated: report.fixed,
10218
+ dropped: report.dropped,
10219
+ reindexed: report.reindexed,
10220
+ semantic_available: report.semantic_available
10221
+ };
10222
+ }
10223
+ /**
10224
+ * Drive the bounded background migration until the store is current, the model is unavailable, or
10225
+ * `shouldStop()` turns true (plugin unmount / process shutdown). A batch FAILURE never rejects: it is
10226
+ * logged and reported as `remaining > 0`, which is what makes the next heartbeat a retry (the caller
10227
+ * still owns the outer catch for a closed database, e.g. a shutdown racing the first read). Never
10228
+ * blocks the event loop for longer than one batch.
10229
+ *
10230
+ * Honours `semantic.auto_migrate` (default on) — the switch exists so a host that must not spend CPU
10231
+ * on background re-encoding can leave the loud warning and the manual entry as the only path.
10232
+ */
10233
+ async migrateVectors(opts = {}) {
10234
+ const enabled = this.config.semantic.auto_migrate !== false;
10235
+ const snapshot = () => ({
10236
+ enabled,
10237
+ remaining: this.migrationRemaining(),
10238
+ migrated: 0,
10239
+ dropped: 0,
10240
+ reindexed: 0,
10241
+ semantic_available: this.semantic.isAvailable()
10242
+ });
10243
+ if (!enabled) return snapshot();
10244
+ let migrated = 0;
10245
+ let dropped = 0;
10246
+ let reindexed = 0;
10247
+ let last = snapshot();
10248
+ if (last.remaining === 0) return last;
10249
+ for (;;) {
10250
+ if (opts.shouldStop?.() === true) break;
10251
+ let step;
10252
+ try {
10253
+ step = await this.migrateVectorsBatch(opts.batchSize);
10254
+ } catch (error) {
10255
+ retrievalLogger().warn(`memory vector migration: batch failed (${describeError$2(error)}) — ${String(this.migrationRemaining())} vector(s) still belong to an older embedding space; the next pass retries`);
10256
+ break;
10257
+ }
10258
+ migrated += step.migrated;
10259
+ dropped += step.dropped;
10260
+ reindexed += step.reindexed;
10261
+ last = {
10262
+ ...step,
10263
+ enabled,
10264
+ migrated,
10265
+ dropped,
10266
+ reindexed
10267
+ };
10268
+ opts.onProgress?.(last);
10269
+ if (step.migrated === 0 && step.dropped === 0 && step.reindexed === 0) break;
10270
+ if (last.remaining === 0) break;
10271
+ await yieldToEventLoop();
10272
+ }
10273
+ return last;
10274
+ }
10275
+ /** Active rows the semantic leg still cannot use (old width, old space, or no vector at all). */
10276
+ migrationRemaining() {
10277
+ const health = this.vectorSpaceHealth();
10278
+ return health.stale + health.space_stale + this.facts.vectorCounts().missing;
10279
+ }
10280
+ /**
10089
10281
  * One lifecycle pass, owned by the store so the DB writes and the live-index
10090
10282
  * invariant (ACTIVE vectors only) are maintained in ONE place — the runtime no
10091
10283
  * longer has to patch the index behind the store's back.
@@ -12417,7 +12609,20 @@ var KnowledgeStore = class {
12417
12609
  const space = this.vectorSpace();
12418
12610
  const persisted = this.chunks.vectorRows();
12419
12611
  reloadVectorIndex(this.vstore, persisted, "knowledge");
12420
- reportForeignVectors("knowledge", space, persisted.filter((r) => r.embedding_model !== space).length, "kb_reindex (dry_run=true previews the count)");
12612
+ let stale = 0;
12613
+ let spaceStale = 0;
12614
+ const expectedBytes = this.vstore.dim * 4;
12615
+ for (const row of persisted) {
12616
+ if (!row.vec || row.vec.byteLength !== expectedBytes) {
12617
+ stale++;
12618
+ continue;
12619
+ }
12620
+ if (row.embedding_model !== space) spaceStale++;
12621
+ }
12622
+ reportStaleVectors("knowledge", space, {
12623
+ stale,
12624
+ space_stale: spaceStale
12625
+ }, "`avantf-mem kb reindex` (MCP/plugin: `kb_reindex`)");
12421
12626
  }
12422
12627
  /**
12423
12628
  * The write-side domain allowlist AND the 知识页 picker's data, in one place: the configured
@@ -13698,7 +13903,8 @@ function buildRuntime(opts) {
13698
13903
  switch (req.action) {
13699
13904
  case "stats": return {
13700
13905
  ...this.memory.countByStatus(),
13701
- retrieval: retrievalHealthSummary()
13906
+ retrieval: retrievalHealthSummary(),
13907
+ vectors: this.memory.vectorSpaceHealth()
13702
13908
  };
13703
13909
  case "list": return this.memory.list(req.category, req.status ?? "active", req.limit ?? 50, req.offset ?? 0);
13704
13910
  case "detail": return this.memory.get(req.fact_id) ?? { error: `fact_id=${req.fact_id} not found` };
@@ -15397,6 +15603,58 @@ function openDocumentPath(documentPath, target, configured, deps = {}) {
15397
15603
  throw new Error(`无法启动编辑器(试过:${tried.join("、")}${detail})`);
15398
15604
  }
15399
15605
  //#endregion
15606
+ //#region lib/types/vectorMigration.js
15607
+ function createVectorMigration(opts) {
15608
+ const { rt, logger } = opts;
15609
+ let running = false;
15610
+ let stopped = false;
15611
+ let warnedDisabled = false;
15612
+ const run = async () => {
15613
+ const health = rt.memory.vectorSpaceHealth();
15614
+ const pending = health.stale + health.space_stale;
15615
+ if (pending === 0) return;
15616
+ if (rt.closed || !opts.isActive()) return;
15617
+ if (rt.config.common.semantic.auto_migrate === false) {
15618
+ if (!warnedDisabled) {
15619
+ warnedDisabled = true;
15620
+ logger.warn(`vector migration: ${String(pending)} ACTIVE vector(s) belong to an older embedding space and \`semantic.auto_migrate\` is off — the semantic leg stays blind to them; run \`avantf-mem vectors --fix\` (MCP/plugin: \`mem_admin vectors_fix\`) or set \`semantic.auto_migrate: true\``);
15621
+ }
15622
+ return;
15623
+ }
15624
+ const startedAt = Date.now();
15625
+ logger.info(`vector migration: ${String(pending)} ACTIVE vector(s) belong to an older embedding space (${String(health.space_stale)} same-width other-model, ${String(health.stale)} wrong-width) — re-encoding in the background`);
15626
+ const outcome = await rt.memory.migrateVectors({
15627
+ ...opts.batchSize === void 0 ? {} : { batchSize: opts.batchSize },
15628
+ shouldStop: () => stopped || rt.closed || !opts.isActive(),
15629
+ onProgress: (progress) => {
15630
+ logger.info(`vector migration: re-encoded ${String(progress.migrated)}/${String(pending)} (${String(progress.remaining)} left)`);
15631
+ }
15632
+ });
15633
+ if (outcome.remaining === 0) {
15634
+ logger.info(`vector migration: complete — ${String(outcome.migrated)} vector(s) re-encoded in ${String(Date.now() - startedAt)}ms; the semantic leg is current`);
15635
+ return;
15636
+ }
15637
+ if (!stopped && !rt.closed && opts.isActive()) logger.warn(`vector migration: ${String(outcome.remaining)} vector(s) still belong to an older embedding space (the embedding model may be unavailable) — the next heartbeat retries; manual entry \`avantf-mem vectors --fix\``);
15638
+ };
15639
+ return {
15640
+ start() {
15641
+ if (stopped || running) return;
15642
+ running = true;
15643
+ run().catch((error) => {
15644
+ logger.warn(`vector migration failed: ${error instanceof Error ? error.message : String(error)}`);
15645
+ }).finally(() => {
15646
+ running = false;
15647
+ });
15648
+ },
15649
+ stop() {
15650
+ stopped = true;
15651
+ },
15652
+ get running() {
15653
+ return running;
15654
+ }
15655
+ };
15656
+ }
15657
+ //#endregion
15400
15658
  //#region lib/types/tool_schema.js
15401
15659
  /**
15402
15660
  * zod → DSH parameter schema derivation for the agent tools.
@@ -16143,6 +16401,14 @@ async function apply(ctx, config) {
16143
16401
  ...waitFor === void 0 ? {} : { waitFor },
16144
16402
  ...verdict === void 0 ? {} : { dshCompat: verdict }
16145
16403
  });
16404
+ const vectorMigration = createVectorMigration({
16405
+ rt,
16406
+ logger,
16407
+ isActive: () => stillActive(ctx)
16408
+ });
16409
+ ctx.effect(() => () => {
16410
+ vectorMigration.stop();
16411
+ });
16146
16412
  const sweepEntities = () => {
16147
16413
  rt.memory.reindexEntities(ENTITY_SWEEP_PER_HEARTBEAT).then((swept) => {
16148
16414
  if (swept.rebuilt > 0) logger.info(`entity sweep: rebuilt ${swept.rebuilt} fact(s) written by older rules (${swept.deferred} still stale)`);
@@ -16154,7 +16420,10 @@ async function apply(ctx, config) {
16154
16420
  ...waitFor === void 0 ? {} : { waitFor },
16155
16421
  deferTokenizerUntilIdle: true
16156
16422
  }).then(() => {
16157
- if (!rt.closed && stillActive(ctx)) sweepEntities();
16423
+ if (!rt.closed && stillActive(ctx)) {
16424
+ sweepEntities();
16425
+ vectorMigration.start();
16426
+ }
16158
16427
  }).catch((error) => {
16159
16428
  logger.warn(`entity sweep gate failed: ${error instanceof Error ? error.message : String(error)}`);
16160
16429
  });
@@ -16167,6 +16436,7 @@ async function apply(ctx, config) {
16167
16436
  logger.warn(`trust tick failed: ${error instanceof Error ? error.message : String(error)}`);
16168
16437
  }
16169
16438
  sweepEntities();
16439
+ vectorMigration.start();
16170
16440
  }, heartbeatMinutes * 6e4);
16171
16441
  const unref = timer.unref;
16172
16442
  if (typeof unref === "function") unref.call(timer);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAwGA,wDAAwD;AACxD,UAAU,aAAa;IACrB,KAAK,EAAE;QACL,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;QAChD,QAAQ,CACN,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,MAAM,CAAA;SAAE,EACzE,SAAS,EAAE,MAAM,OAAO,GACvB,IAAI,CAAA;KACR,CAAA;IACD,MAAM,EAAE;QACN,wEAAwE;QACxE,MAAM,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;KAC5D,CAAA;IACD;;;;;OAKG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CAC3B;AA8gDD,2DAA2D;AAC3D,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAEvC;;;;GAIG;AACH,eAAO,MAAM,MAAM,UAAsB,CAAA;AAEzC,mGAAmG;AACnG,wBAAsB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC7D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAwGA,wDAAwD;AACxD,UAAU,aAAa;IACrB,KAAK,EAAE;QACL,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;QAChD,QAAQ,CACN,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,MAAM,CAAA;SAAE,EACzE,SAAS,EAAE,MAAM,OAAO,GACvB,IAAI,CAAA;KACR,CAAA;IACD,MAAM,EAAE;QACN,wEAAwE;QACxE,MAAM,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;KAC5D,CAAA;IACD;;;;;OAKG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CAC3B;AAyhDD,2DAA2D;AAC3D,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAEvC;;;;GAIG;AACH,eAAO,MAAM,MAAM,UAAsB,CAAA;AAEzC,mGAAmG;AACnG,wBAAsB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAQlD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAExC,OAAO,EAAE,mBAAmB,EAAU,MAAM,kCAAkC,CAAA;AAE9E,OAAO,EAQL,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,WAAW,EAEhB,KAAK,WAAW,EACjB,MAAM,aAAa,CAAA;AACpB,OAAO,EAeL,KAAK,aAAa,EAClB,KAAK,WAAW,EAEhB,KAAK,YAAY,EACjB,KAAK,cAAc,EAEnB,KAAK,oBAAoB,EAG1B,MAAM,sBAAsB,CAAA;AAoB7B,eAAO,MAAM,IAAI,eAAe,CAAA;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGpC,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAQ7E,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,WAAW,GACjB,MAAM,aAAa,CAAA;AAEpB,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AACD,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAO3B,CAAA;AA6CF,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAOvC;;;;;;;;;;GAUG;AACH,qBAAa,gBAAiB,SAAQ,mBAAmB;IACvD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAe;gBACtB,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,aAAa;IAK3C;;;;;;;;;OASG;YACW,IAAI;IAclB;;;;OAIG;IAEH,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;IAMlF,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;IAMnF,KAAK,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAKhF;;;;;OAKG;IAEH,EAAE,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IAOpD,KAAK,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IAI5D;;;;OAIG;IAEG,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,oBAAoB,CAAC,CAAC;IAUnF,qFAAqF;IAE/E,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAUvE;;;;OAIG;IAEG,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC,CAAC;IAQvG;;;;OAIG;IAEG,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC,CAAC;IAUpG;;;;;OAKG;IAEG,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;CAiBpE;AAiID,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4WvE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAQlD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAExC,OAAO,EAAE,mBAAmB,EAAU,MAAM,kCAAkC,CAAA;AAE9E,OAAO,EAQL,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,WAAW,EAEhB,KAAK,WAAW,EACjB,MAAM,aAAa,CAAA;AACpB,OAAO,EAeL,KAAK,aAAa,EAClB,KAAK,WAAW,EAEhB,KAAK,YAAY,EACjB,KAAK,cAAc,EAEnB,KAAK,oBAAoB,EAG1B,MAAM,sBAAsB,CAAA;AAqB7B,eAAO,MAAM,IAAI,eAAe,CAAA;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGpC,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAQ7E,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,WAAW,GACjB,MAAM,aAAa,CAAA;AAEpB,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AACD,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAO3B,CAAA;AA6CF,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAOvC;;;;;;;;;;GAUG;AACH,qBAAa,gBAAiB,SAAQ,mBAAmB;IACvD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAe;gBACtB,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,aAAa;IAK3C;;;;;;;;;OASG;YACW,IAAI;IAclB;;;;OAIG;IAEH,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;IAMlF,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;IAMnF,KAAK,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAKhF;;;;;OAKG;IAEH,EAAE,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IAOpD,KAAK,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IAI5D;;;;OAIG;IAEG,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,oBAAoB,CAAC,CAAC;IAUnF,qFAAqF;IAE/E,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAUvE;;;;OAIG;IAEG,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC,CAAC;IAQvG;;;;OAIG;IAEG,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC,CAAC;IAUpG;;;;;OAKG;IAEG,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;CAiBpE;AAiID,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuYvE"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The plugin's driver for the bounded, resumable background migration of persisted vectors into the
3
+ * current embedding space.
4
+ *
5
+ * Changing the embedding space (default model, width, pooling, normalization) is a DATA MIGRATION,
6
+ * not a config tweak: every persisted vector still decodes, but in another model's coordinates, so
7
+ * the semantic leg must not serve it. Measured on the real library after the 512→768 default-model
8
+ * swap, 78 of 80 ACTIVE facts fell out of the semantic leg and retrieval degraded to lexical+entity
9
+ * with no error anywhere — the store's open-time warning now says so, and this module is what
10
+ * repairs it without the operator having to know `vectors --fix` exists.
11
+ *
12
+ * It sits BEHIND the startup gate (`awaitStartupGate`, see index.ts): the model warm and this first
13
+ * batch must not land in the boot window (DESIGN §20.14), and the tokenizer/model are the expensive
14
+ * resources the gate exists to move out. The loop itself lives in the store
15
+ * (`MemoryStore.migrateVectors`), which owns the batch size, the per-batch error handling and the
16
+ * event-loop yield; this module owns the LOGGING (start / progress / end) and the lifecycle
17
+ * (`shouldStop` = plugin disposed or runtime closed).
18
+ *
19
+ * Deliberately no new state in this file: what still needs migrating is derived from the database on
20
+ * every pass, which is exactly what makes the migration resumable across a restart.
21
+ */
22
+ import { type AvantfRuntime } from '../engine/mem/index.js';
23
+ import type { AvantfLogger } from '../engine/mem-contract/index.js';
24
+ export interface VectorMigrationHandle {
25
+ /**
26
+ * Run one migration pass. Quiet no-op when the store is already current, when a pass is already
27
+ * running, or after {@link stop}. Failures never escape: they are logged and left for the next
28
+ * heartbeat to retry.
29
+ */
30
+ start(): void;
31
+ /** Stop a running pass at its next batch boundary; later `start()` calls are no-ops. */
32
+ stop(): void;
33
+ /** `true` while a pass is in flight. */
34
+ readonly running: boolean;
35
+ }
36
+ export declare function createVectorMigration(opts: {
37
+ rt: AvantfRuntime;
38
+ logger: AvantfLogger;
39
+ /** False once the plugin's fiber is disposed (profile reload / unload). */
40
+ isActive: () => boolean;
41
+ /** Rows per batch; the store's `DEFAULT_VECTOR_MIGRATION_BATCH` when omitted. */
42
+ batchSize?: number;
43
+ }): VectorMigrationHandle;
44
+ //# sourceMappingURL=vectorMigration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vectorMigration.d.ts","sourceRoot":"","sources":["../../src/vectorMigration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAExD,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,KAAK,IAAI,IAAI,CAAA;IACb,wFAAwF;IACxF,IAAI,IAAI,IAAI,CAAA;IACZ,wCAAwC;IACxC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,EAAE,EAAE,aAAa,CAAA;IACjB,MAAM,EAAE,YAAY,CAAA;IACpB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,OAAO,CAAA;IACvB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,qBAAqB,CAsExB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avantf/dsh-mem",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "avantf-mem DSH native Cordis plugin: host service + tools + RPC + client tabs 记忆/知识",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -78,10 +78,10 @@
78
78
  },
79
79
  "devDependencies": {
80
80
  "@avantf/dsh-plugin-base": ">=0.3.0 <1.0.0",
81
- "@avantf/mem": "0.4.1",
82
- "@avantf/mem-contract": "0.4.1",
83
- "@avantf/mem-convert": "0.4.1",
84
- "@avantf/mem-provision": "0.4.1",
81
+ "@avantf/mem": "0.4.2",
82
+ "@avantf/mem-contract": "0.4.2",
83
+ "@avantf/mem-convert": "0.4.2",
84
+ "@avantf/mem-provision": "0.4.2",
85
85
  "@types/node": "^22.0.0",
86
86
  "@types/react": "^18.3.0",
87
87
  "lightningcss": "^1.32.0",