@modusensus/dsh-mneme 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -21
- package/docs/CONFIGURATION.md +45 -4
- package/docs/DIRECTORIES.md +114 -0
- package/docs/DISTILL.md +64 -0
- package/docs/DSH-GO.md +97 -0
- package/docs/ENTITIES.md +5 -3
- package/docs/FORGET.md +77 -0
- package/docs/KB.md +110 -0
- package/docs/SLEEP.md +28 -3
- package/docs/STORAGE.md +23 -0
- package/lib/api-standalone.js +4 -1
- package/lib/api.js +744 -26
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +3752 -819
- package/lib/config.js +171 -2
- package/lib/continuity.js +7 -6
- package/lib/diary/auto.js +190 -0
- package/lib/diary/compose.js +440 -0
- package/lib/diary/dir.js +59 -0
- package/lib/diary/export.js +97 -0
- package/lib/diary/index.js +26 -0
- package/lib/diary/library.js +189 -0
- package/lib/diary/qq-inbox.js +142 -0
- package/lib/diary/rollover.js +61 -0
- package/lib/diary/session-provider.js +122 -0
- package/lib/document.js +9 -3
- package/lib/dream/clustering.js +6 -1
- package/lib/dream/decisions.js +56 -5
- package/lib/dream/sleep.js +616 -15
- package/lib/dream.js +45 -13
- package/lib/dsh-go/host.js +162 -0
- package/lib/dsh-go/panel.js +2091 -0
- package/lib/dsh-go/text.js +342 -0
- package/lib/dsh-go/views/common.js +68 -0
- package/lib/dsh-go/views/entity.js +48 -0
- package/lib/dsh-go/views/memory.js +198 -0
- package/lib/dsh-go/views/root.js +30 -0
- package/lib/dsh-go/views/settings.js +398 -0
- package/lib/dsh-go/views/status.js +51 -0
- package/lib/embedding.js +42 -8
- package/lib/entities/extractor.js +36 -7
- package/lib/hot-memory.js +8 -9
- package/lib/index.js +131 -5
- package/lib/inject.js +192 -19
- package/lib/kb/index.js +309 -0
- package/lib/kb/routes.js +220 -0
- package/lib/kb/tools.js +148 -0
- package/lib/lang.js +115 -4
- package/lib/peak-hours.js +57 -2
- package/lib/profile-facts.js +356 -0
- package/lib/semantic.js +347 -60
- package/lib/service.js +302 -70
- package/lib/settings.js +117 -14
- package/lib/store.js +830 -55
- package/lib/summarize.js +38 -11
- package/lib/tools.js +173 -25
- package/lib/user-settings.js +175 -23
- package/lib/vector-index.js +76 -6
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +264 -10
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +7 -1
- package/package.json +25 -1
- package/scripts/sync-test-badge.mjs +1 -1
- package/scripts/test-count-sync.mjs +9 -5
- package/src/api-standalone.js +4 -1
- package/src/api.js +744 -26
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +171 -2
- package/src/continuity.js +7 -6
- package/src/diary/auto.js +190 -0
- package/src/diary/compose.js +440 -0
- package/src/diary/dir.js +59 -0
- package/src/diary/export.js +97 -0
- package/src/diary/index.js +26 -0
- package/src/diary/library.js +189 -0
- package/src/diary/qq-inbox.js +142 -0
- package/src/diary/rollover.js +61 -0
- package/src/diary/session-provider.js +122 -0
- package/src/document.js +9 -3
- package/src/dream/clustering.js +6 -1
- package/src/dream/decisions.js +56 -5
- package/src/dream/sleep.js +616 -15
- package/src/dream.js +45 -13
- package/src/dsh-go/host.js +162 -0
- package/src/dsh-go/panel.js +2091 -0
- package/src/dsh-go/text.js +342 -0
- package/src/dsh-go/views/common.js +68 -0
- package/src/dsh-go/views/entity.js +48 -0
- package/src/dsh-go/views/memory.js +198 -0
- package/src/dsh-go/views/root.js +30 -0
- package/src/dsh-go/views/settings.js +398 -0
- package/src/dsh-go/views/status.js +51 -0
- package/src/embedding.js +42 -8
- package/src/entities/extractor.js +36 -7
- package/src/hot-memory.js +8 -9
- package/src/index.js +131 -5
- package/src/inject.js +192 -19
- package/src/kb/index.js +309 -0
- package/src/kb/routes.js +220 -0
- package/src/kb/tools.js +148 -0
- package/src/lang.js +115 -4
- package/src/peak-hours.js +57 -2
- package/src/profile-facts.js +356 -0
- package/src/semantic.js +347 -60
- package/src/service.js +302 -70
- package/src/settings.js +117 -14
- package/src/store.js +830 -55
- package/src/summarize.js +38 -11
- package/src/tools.js +173 -25
- package/src/user-settings.js +175 -23
- package/src/vector-index.js +76 -6
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +264 -10
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +7 -1
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +377 -4
- package/test/attachment-text.test.js +140 -0
- package/test/bm25-corpus-cache.test.js +145 -0
- package/test/capacity-warning.test.js +262 -0
- package/test/client.test.js +908 -138
- package/test/config.test.js +79 -0
- package/test/conflict-freeze.test.js +45 -1
- package/test/continuity.test.js +8 -1
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/document.test.js +32 -0
- package/test/dream-maxtokens-fallback.test.js +3 -3
- package/test/dream.test.js +62 -1
- package/test/dsh-go-host.test.js +134 -0
- package/test/dsh-go-panel-actions.test.js +854 -0
- package/test/dsh-go-panel.test.js +251 -0
- package/test/embedding-cache-f32.test.js +276 -0
- package/test/embedding-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/image-memory.test.js +171 -0
- package/test/inject-pin-explicit.test.js +300 -0
- package/test/inject-pin-vector-crowd.test.js +225 -0
- package/test/issue-617-step1-query-and-fusion.test.js +361 -0
- package/test/kb-api-wiring.test.js +128 -0
- package/test/kb-collections.test.js +272 -0
- package/test/kb-routes.test.js +379 -0
- package/test/mint-source.test.js +26 -0
- package/test/peak-hours-observability.test.js +70 -0
- package/test/persona-api.test.js +3 -3
- package/test/persona-by-session.test.js +568 -0
- package/test/persona-settings.test.js +38 -7
- package/test/profile-facts.test.js +492 -0
- package/test/reflection-update-gate.test.js +129 -0
- package/test/reindex-backfill.test.js +334 -0
- package/test/reindex-scheduler.test.js +369 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/session-gate.test.js +1 -1
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +598 -0
- package/test/standalone-api.test.js +24 -0
- package/test/store.test.js +20 -0
- package/test/summarize.test.js +26 -0
- package/test/test-count-sync.test.js +14 -3
- package/test/tool-state-ignore.test.js +65 -0
- package/test/tools-dto-schema-matrix.test.js +23 -2
- package/test/tools.test.js +144 -7
- package/test/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +476 -1
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-queries.test.js +91 -1
- package/test/wiki-t1-jobs.test.js +125 -0
- package/test/wiki-tools.test.js +131 -76
package/src/store.js
CHANGED
|
@@ -287,6 +287,29 @@ CREATE TABLE IF NOT EXISTS scope_state (
|
|
|
287
287
|
-- 它同时是 setScopeState 那条 UPSERT 的冲突目标,所以不能省。
|
|
288
288
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_scope_state_key ON scope_state(dimension, scope_value);
|
|
289
289
|
|
|
290
|
+
-- pinned_memories: #249 第二批,显式 pin。旧口径把「类型是 constraint/preference」
|
|
291
|
+
-- 当成 pin(隐含假设),于是 1163 条自动变「每轮必进」而预算只有 5——池子本身
|
|
292
|
+
-- 就太大,靠相关性排序永远轮不到「署名」「拒绝谄媚」这类稳定偏好。本表把
|
|
293
|
+
-- 「哪些是用户亲手钉的」变成一条显式事实:注入侧只认这张表,不再认类型。
|
|
294
|
+
--
|
|
295
|
+
-- 为什么是独立表而不是 memories 的布尔列:回退只需一条 DROP,不碰 31 列的主表;
|
|
296
|
+
-- 且「pin 是用户与记忆之间的一条关系」天然是关系表——created_at 还能给出
|
|
297
|
+
-- pin 顺序(按 pin 先后注入,而不是按「最近改过」)。外泄面天然为零:
|
|
298
|
+
-- mirror/share 都不 SELECT 本表,pin 标记不会出现在导出物里。
|
|
299
|
+
--
|
|
300
|
+
-- 幂等靠 CREATE TABLE IF NOT EXISTS(本文件其余建表同款),不登记
|
|
301
|
+
-- applied_migrations——那是给「跑完恒不命中却仍要全表扫一遍」的 data migration
|
|
302
|
+
-- 用的,本表没有回填步骤。存量库升级后本表为空,注入侧据此回落到旧的类型口径
|
|
303
|
+
-- (见 service.injectCandidates 的 type-fallback 分支),升级即等价。
|
|
304
|
+
CREATE TABLE IF NOT EXISTS pinned_memories (
|
|
305
|
+
memory_id TEXT PRIMARY KEY,
|
|
306
|
+
created_at TEXT NOT NULL,
|
|
307
|
+
note TEXT
|
|
308
|
+
);
|
|
309
|
+
-- 按 pin 顺序注入要读 created_at 全序;本表行数以「人手钉的条数」计(个位到几十),
|
|
310
|
+
-- 索引只为把排序的临时 B 树换成一次有序扫,不承担过滤。
|
|
311
|
+
CREATE INDEX IF NOT EXISTS idx_pinned_created ON pinned_memories(created_at);
|
|
312
|
+
|
|
290
313
|
-- llm_audit_logs: every background LLM call (autoDream consolidation + summary,
|
|
291
314
|
-- autoSummarize compression) is recorded here — tokens in/out, duration, status
|
|
292
315
|
-- and the trigger that caused it (Bug8). Failures are captured as status='error'
|
|
@@ -409,7 +432,10 @@ CREATE INDEX IF NOT EXISTS idx_distill_runs_created ON distill_runs(created_at);
|
|
|
409
432
|
-- · 本表 = 行级,「这条记忆出生自哪一轮」,覆盖没有窗口的铸造路径。
|
|
410
433
|
-- 只填 **create** 一种动作:merge/conflict/update/supersede 对既有行的改动,receipt_chain
|
|
411
434
|
-- 已经按 (run_id, record_id) 记过了——再造一份就是第二套账。create 是那边唯一的洞
|
|
412
|
-
-- (
|
|
435
|
+
-- (buildRecordReceipts 不给 create/create_merged 产行:新行没有「改动前后的账」可记,
|
|
436
|
+
-- 只有「出生自哪一轮」,那正是本表的事)。#619 起 buildOutcome 会给 create 派
|
|
437
|
+
-- disposition(created / created-refreshed),那是审计面读的结果,与 receipt_chain
|
|
438
|
+
-- 产不产行是两件事;created-refreshed 是原地刷新,没有出生,本表也不记。
|
|
413
439
|
-- run_type / trigger_source 就地冗余:run_id 在 dream_runs 与 distill_runs 两张表里
|
|
414
440
|
-- 各有自己的主键域,读侧不该为了显示一个类型去 union 两张审计表。
|
|
415
441
|
-- ON DELETE CASCADE:记忆行没了,归因没有指代对象;remove() 仍显式清(与
|
|
@@ -603,14 +629,19 @@ CREATE TABLE IF NOT EXISTS choro_claims (
|
|
|
603
629
|
);
|
|
604
630
|
CREATE INDEX IF NOT EXISTS idx_choro_claims_page ON choro_claims(page_id);
|
|
605
631
|
|
|
606
|
-
--
|
|
607
|
-
--
|
|
608
|
-
--
|
|
632
|
+
-- 向量壳(收尾档 P36/P38 落地):owner_id 指向 choro_pages(v1 只做页级;
|
|
633
|
+
-- claims 级留待真有精排需求)。向量与记忆向量不共表、不共查询——两边换模型
|
|
634
|
+
-- 版本的节奏不同(V4 的答案在此:model/dim 记行内,**不锁死**——dim 失配 =
|
|
635
|
+
-- 惰性全量重嵌,代价只是几十页重嵌而不是作废记忆向量)。
|
|
636
|
+
-- vector 存 JSON 数组(与 memories.embedding 同款 TEXT 形态);source_generated_at
|
|
637
|
+
-- 记嵌入时刻页面的 generated_at——页面富化/重建后该值变化,检索侧据此判 staleness。
|
|
609
638
|
CREATE TABLE IF NOT EXISTS choro_vectors (
|
|
610
|
-
owner_id
|
|
611
|
-
dim
|
|
612
|
-
model
|
|
613
|
-
|
|
639
|
+
owner_id INTEGER NOT NULL,
|
|
640
|
+
dim INTEGER NOT NULL,
|
|
641
|
+
model TEXT NOT NULL,
|
|
642
|
+
vector TEXT,
|
|
643
|
+
source_generated_at TEXT,
|
|
644
|
+
created_at TEXT NOT NULL
|
|
614
645
|
);
|
|
615
646
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_choro_vectors_owner_model ON choro_vectors(owner_id, model);
|
|
616
647
|
|
|
@@ -626,6 +657,111 @@ CREATE TABLE IF NOT EXISTS choro_edges (
|
|
|
626
657
|
);
|
|
627
658
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_choro_edges_triple ON choro_edges(repo_id, from_slug, to_slug);
|
|
628
659
|
CREATE INDEX IF NOT EXISTS idx_choro_edges_to ON choro_edges(repo_id, to_slug);
|
|
660
|
+
|
|
661
|
+
-- profile_facts:「Agent 眼中的你」自动事实(2026-11-06 需求,对标 DS Bot 每 bot
|
|
662
|
+
-- 一份 MEMORY.md 事实清单的设计)。与手填画像(user_settings.profile)**物理分离**
|
|
663
|
+
-- ——那是用户的原文,模型只有读权(profile-extract #497 的「只出草稿不落库」口径
|
|
664
|
+
-- 不变);本表是 agent 侧对用户的独立认知,模型可在 dream 轮里增改、用户可在面板
|
|
665
|
+
-- 上逐条修正。agent_key='' 是 default 卡(解析不到会话身份的会话),非空值是会话
|
|
666
|
+
-- 身份,与 persona byAgent 同一取值域(session.header.agentPreset)。
|
|
667
|
+
-- 输入行严格分区不互串:agent 卡只吃 agent_scope=自己 的记忆行,default 卡只吃
|
|
668
|
+
-- agent_scope IS NULL 的行;外来行(source=imported)整体排除(memory-source.js
|
|
669
|
+
-- 那条「本库不巩固别人导入的原文」延伸到画像事实:导入的包不该改写本库对用户的认知)。
|
|
670
|
+
CREATE TABLE IF NOT EXISTS profile_facts (
|
|
671
|
+
id TEXT PRIMARY KEY,
|
|
672
|
+
agent_key TEXT NOT NULL DEFAULT '',
|
|
673
|
+
content TEXT NOT NULL,
|
|
674
|
+
status TEXT NOT NULL DEFAULT 'active', -- active | archived
|
|
675
|
+
source_memory_ids TEXT NOT NULL DEFAULT '[]', -- JSON:这条事实的出处记忆 id
|
|
676
|
+
created_at TEXT NOT NULL,
|
|
677
|
+
updated_at TEXT NOT NULL
|
|
678
|
+
);
|
|
679
|
+
CREATE INDEX IF NOT EXISTS idx_profile_facts_card ON profile_facts(agent_key, status);
|
|
680
|
+
|
|
681
|
+
-- 逐 op 账本(scope_changes 式):谁(auto 轮 / 面板人工)、何时、把哪条事实从什么
|
|
682
|
+
-- 改成了什么。审计纪律同 dream_runs / llm_audit_logs:只记账,不触发任何 hook。
|
|
683
|
+
CREATE TABLE IF NOT EXISTS profile_fact_journal (
|
|
684
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
685
|
+
at TEXT NOT NULL,
|
|
686
|
+
agent_key TEXT NOT NULL DEFAULT '',
|
|
687
|
+
run_id TEXT, -- auto 轮的回执 id;人工操作为 NULL
|
|
688
|
+
fact_id TEXT,
|
|
689
|
+
op TEXT NOT NULL, -- add | update | archive | restore | delete
|
|
690
|
+
before TEXT,
|
|
691
|
+
after TEXT,
|
|
692
|
+
actor TEXT NOT NULL DEFAULT 'auto' -- auto | manual
|
|
693
|
+
);
|
|
694
|
+
CREATE INDEX IF NOT EXISTS idx_profile_fact_journal_card ON profile_fact_journal(agent_key, id);
|
|
695
|
+
|
|
696
|
+
-- 每轮回执(dream_runs 式):游标(该卡消费到 memories.updated_at 的哪一刻)、
|
|
697
|
+
-- 输入条数、落地 op 数。游标从最近一次 ok 轮读取——failed 轮不推进游标,下次
|
|
698
|
+
-- 重跑同一窗口(与蒸馏游标同款纪律:失败不消费)。
|
|
699
|
+
CREATE TABLE IF NOT EXISTS profile_fact_runs (
|
|
700
|
+
id TEXT PRIMARY KEY,
|
|
701
|
+
agent_key TEXT NOT NULL DEFAULT '',
|
|
702
|
+
trigger_source TEXT NOT NULL, -- auto | manual
|
|
703
|
+
status TEXT NOT NULL, -- ok | noop | failed
|
|
704
|
+
cursor TEXT, -- 本轮消费到的 memories.updated_at
|
|
705
|
+
input_count INTEGER NOT NULL DEFAULT 0,
|
|
706
|
+
ops_applied INTEGER NOT NULL DEFAULT 0,
|
|
707
|
+
detail TEXT,
|
|
708
|
+
started_at TEXT NOT NULL,
|
|
709
|
+
finished_at TEXT
|
|
710
|
+
);
|
|
711
|
+
CREATE INDEX IF NOT EXISTS idx_profile_fact_runs_card ON profile_fact_runs(agent_key, started_at);
|
|
712
|
+
|
|
713
|
+
-- ============================================================
|
|
714
|
+
-- knowledge base(知识库第二层,spec: docs/plans/2026-11-08-kb-qoder-parity.md §3.3)。
|
|
715
|
+
-- 与记忆行物理隔离的硬边界同 choro 那组表:kb 的行永不进 memories /
|
|
716
|
+
-- saveWithDedupe / dream / 质量过滤的任何池子。这里存的是「用户/agent 主动
|
|
717
|
+
-- 收进知识库的资料」——文件、网页、仓库地志页的**副本**与索引,天生属于外来
|
|
718
|
+
-- 原文,让巩固流程够得着它就会把用户刚存的手册自己归档掉。
|
|
719
|
+
-- 幂等键是 (collection_id, rel_key):同一来源重入库按 sha256 判 no-op,
|
|
720
|
+
-- 变了才替换 chunks(替换语义在 replaceKbDoc 里,事务包住删除与插入)。
|
|
721
|
+
-- rel_key 是来源的稳定相对键(本地文件=绝对路径、网页=URL、地志页=repo#slug),
|
|
722
|
+
-- 不是展示名——展示名在 kb_docs.title。
|
|
723
|
+
CREATE TABLE IF NOT EXISTS kb_collections (
|
|
724
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
725
|
+
key TEXT NOT NULL,
|
|
726
|
+
title TEXT NOT NULL,
|
|
727
|
+
description TEXT,
|
|
728
|
+
created_at TEXT NOT NULL,
|
|
729
|
+
updated_at TEXT NOT NULL
|
|
730
|
+
);
|
|
731
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_kb_collections_key ON kb_collections(key);
|
|
732
|
+
|
|
733
|
+
-- kind 限定三类(§3.3):file | url | choro_page。sha256 是**正文内容**的哈希,
|
|
734
|
+
-- 不是文件 stat 的指纹——文件 mtime 变了内容没变时重入库必须是 no-op,
|
|
735
|
+
-- 否则每次保存键盘敲一下都全量重切块。
|
|
736
|
+
CREATE TABLE IF NOT EXISTS kb_docs (
|
|
737
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
738
|
+
collection_id INTEGER NOT NULL,
|
|
739
|
+
rel_key TEXT NOT NULL,
|
|
740
|
+
kind TEXT NOT NULL,
|
|
741
|
+
source_path TEXT,
|
|
742
|
+
title TEXT NOT NULL,
|
|
743
|
+
bytes INTEGER NOT NULL DEFAULT 0,
|
|
744
|
+
sha256 TEXT,
|
|
745
|
+
indexed_at TEXT NOT NULL,
|
|
746
|
+
status TEXT NOT NULL DEFAULT 'ok',
|
|
747
|
+
error TEXT
|
|
748
|
+
);
|
|
749
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_kb_docs_collection_relkey ON kb_docs(collection_id, rel_key);
|
|
750
|
+
|
|
751
|
+
-- 分块正文。heading 是块所属的二级标题节(无节则空串),ordinal 是块在
|
|
752
|
+
-- 文档内的序号——检索命中要能回指「第几块、哪一节」,面板/工具靠它跳段落。
|
|
753
|
+
-- text 存原文切片(含分块时带的标题行?不含:标题在 heading 里,text 只放正文,
|
|
754
|
+
-- 免得同一条命中把标题算两遍)。
|
|
755
|
+
CREATE TABLE IF NOT EXISTS kb_chunks (
|
|
756
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
757
|
+
doc_id INTEGER NOT NULL,
|
|
758
|
+
ordinal INTEGER NOT NULL,
|
|
759
|
+
heading TEXT NOT NULL DEFAULT '',
|
|
760
|
+
text TEXT NOT NULL,
|
|
761
|
+
created_at TEXT NOT NULL
|
|
762
|
+
);
|
|
763
|
+
CREATE INDEX IF NOT EXISTS idx_kb_chunks_doc_ordinal ON kb_chunks(doc_id, ordinal);
|
|
764
|
+
|
|
629
765
|
`;
|
|
630
766
|
|
|
631
767
|
// Exported for API-layer type validation (standalone API POST /memories and
|
|
@@ -880,6 +1016,10 @@ function toConflictPending(row) {
|
|
|
880
1016
|
memory_a: row.memory_a,
|
|
881
1017
|
memory_b: row.memory_b,
|
|
882
1018
|
reason: row.reason ?? undefined,
|
|
1019
|
+
// Issue #603 第 2 层:入队时的相似度。存量行与调用方没给的行是 NULL,
|
|
1020
|
+
// 这里如实透出 undefined(条件展开:带 undefined 键会让 in-process schema
|
|
1021
|
+
// 校验判违规,同 toApiList 的处理)。
|
|
1022
|
+
...(typeof row.similarity === "number" ? { similarity: row.similarity } : {}),
|
|
883
1023
|
created_at: row.created_at,
|
|
884
1024
|
resolved_at: row.resolved_at ?? undefined,
|
|
885
1025
|
resolved_winner: row.resolved_winner ?? undefined
|
|
@@ -930,6 +1070,17 @@ function toScopeState(row) {
|
|
|
930
1070
|
};
|
|
931
1071
|
}
|
|
932
1072
|
|
|
1073
|
+
// pinned_memories 行 → 读侧对象(#249 第二批)。NULL note 落 undefined,
|
|
1074
|
+
// 与 toScopeState 同口径。
|
|
1075
|
+
function toPinned(row) {
|
|
1076
|
+
if (!row) return undefined;
|
|
1077
|
+
return {
|
|
1078
|
+
memory_id: row.memory_id,
|
|
1079
|
+
created_at: row.created_at,
|
|
1080
|
+
note: row.note ?? undefined
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
933
1084
|
function toRecallRun(row) {
|
|
934
1085
|
if (!row) return undefined;
|
|
935
1086
|
return {
|
|
@@ -1141,6 +1292,10 @@ export function createStore(path) {
|
|
|
1141
1292
|
// chorography(M2,实现计划 P17):T1 生成轮的页级失败计数。T0 建的表没有它
|
|
1142
1293
|
// (M0 只写 skeleton),存量库经 PRAGMA 检查后补列,幂等。
|
|
1143
1294
|
addColumn("choro_pages", "fail_count", "ALTER TABLE choro_pages ADD COLUMN fail_count INTEGER NOT NULL DEFAULT 0");
|
|
1295
|
+
// chorography 收尾档(P36):v0.11.0 的 choro_vectors 是空壳表(spec §5 漏了
|
|
1296
|
+
// 向量本体列),存量库经 PRAGMA 检查补列。
|
|
1297
|
+
addColumn("choro_vectors", "vector", "ALTER TABLE choro_vectors ADD COLUMN vector TEXT");
|
|
1298
|
+
addColumn("choro_vectors", "source_generated_at", "ALTER TABLE choro_vectors ADD COLUMN source_generated_at TEXT");
|
|
1144
1299
|
|
|
1145
1300
|
// v0.8.0 A1(issue #17)scope 隔离存储层:四列全部可空、不带 DEFAULT——存量行
|
|
1146
1301
|
// 零重写,读侧把 NULL 视为未标注(= 全局可见)。是否落值由 scopeEnabled 门控
|
|
@@ -1398,6 +1553,18 @@ export function createStore(path) {
|
|
|
1398
1553
|
addColumn("memory_provenance", "distill_run_id", "ALTER TABLE memory_provenance ADD COLUMN distill_run_id TEXT");
|
|
1399
1554
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memory_provenance_run ON memory_provenance(distill_run_id);");
|
|
1400
1555
|
|
|
1556
|
+
// Issue #603 第 2 层:冲突待审队列此前只能按 created_at 倒序排。一次 sleep 轮次
|
|
1557
|
+
// 里产出的对全部落在同一毫秒(实测 60 条挤在 60ms 内),排序键全同,队列在面板
|
|
1558
|
+
// 上看起来是随机的——而「哪一对最该先看」的唯一客观信号是相似度,此前只存在
|
|
1559
|
+
// reason 文本里(「相似度 0.87」),前端靠正则回收。落成列之后排序键是数据,
|
|
1560
|
+
// 不是解析结果。
|
|
1561
|
+
//
|
|
1562
|
+
// 可空且不带 DEFAULT:存量行没有这个信息,NULL = 「入队时没记」(比填 0 诚实——
|
|
1563
|
+
// 0 会被排到最后,等于替用户断言「这两条毫不相似」)。排序用 NULLS LAST。
|
|
1564
|
+
addColumn("conflict_pending", "similarity", "ALTER TABLE conflict_pending ADD COLUMN similarity REAL");
|
|
1565
|
+
// 索引建在加列之后(同 idx_memory_provenance_run 的理由):老库打开时列才刚加上。
|
|
1566
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_conflict_pending_similarity ON conflict_pending(similarity DESC, created_at DESC);");
|
|
1567
|
+
|
|
1401
1568
|
// Audit peer F: a legacy DB may hold a non-integer generation/applied_generation
|
|
1402
1569
|
// (pre-v0.3.9 the JS gate truncated with Math.trunc and SQLite's CHECK only
|
|
1403
1570
|
// enforced >= 0). Such a value is ambiguous — it cannot map to a real applied
|
|
@@ -1419,28 +1586,71 @@ export function createStore(path) {
|
|
|
1419
1586
|
// Issue #202:embedding 解析缓存。检索路径此前每次调用都对全部带向量行
|
|
1420
1587
|
// JSON.parse(993 × 512 维 ≈ 48 ms/次,且 SQL 拖着 10 MB 的 TEXT),余弦本身
|
|
1421
1588
|
// 只占 3%。向量只经 setEmbedding 写入(单一失效点),把解析结果按 id 缓存,
|
|
1422
|
-
//
|
|
1589
|
+
// 命中即零解析。
|
|
1590
|
+
//
|
|
1591
|
+
// 上限为什么不能是固定常量(2026-11-05 实测):上限 4000、活跃带向量行 6685,
|
|
1592
|
+
// 扫到第 4001 条开始驱逐自己刚填的条目,扫完只剩最后 4000 条,下一轮又从第
|
|
1593
|
+
// 一条 miss——四轮实测命中率恒 0.0%,缓存等于不存在,每次检索都全量重解析
|
|
1594
|
+
// (实测 624 ms)。固定调大只是把复发推到下个月(活跃行近 30 天净增 6156 条,
|
|
1595
|
+
// 上限 20000 也只撑到 2027-01-09),所以上限跟工作集自适应:
|
|
1596
|
+
// · 每次全量扫(searchVector / vectorDimensionState)在循环前把上限抬到
|
|
1597
|
+
// 「本次扫到的行数 ×1.25」,一次扫完不会被自己的驱逐打断;
|
|
1598
|
+
// · 内存预算封顶(默认 256 MB),超预算的行仍按 FIFO 驱逐——大库退化回
|
|
1599
|
+
// 「部分命中」,不会无界吃内存。
|
|
1600
|
+
// 存储用 Float32Array。同一口径(GC 后 heapUsed + arrayBuffers,6730 条
|
|
1601
|
+
// 1024 维全常驻)实测:number[] 56.4MB → Float32Array 29.7MB(heap 1.9 +
|
|
1602
|
+
// arrayBuffers 27.8),省 26.7MB / 47%。注意 heapUsed 不统计 TypedArray 的
|
|
1603
|
+
// 底层 ArrayBuffer,只看 heapUsed 会读成「省 96%」的假象;旧实现若保持上限
|
|
1604
|
+
// 4000 则装不下工作集(常驻 31.9MB 但命中率 0%),两者不可比,所以对照组是
|
|
1605
|
+
// 把旧上限抬到 20000 让工作集全常驻。cosine 全量 25.4ms → 18.0ms,前 50 条
|
|
1606
|
+
// 排序精度差 0.00e+0。对外契约不变:
|
|
1607
|
+
// getParsedEmbedding / getEmbeddings 仍返回 number[],只有内部热路径
|
|
1608
|
+
// (readVector)拿 Float32Array。
|
|
1423
1609
|
const embeddingCache = new Map();
|
|
1424
|
-
const
|
|
1610
|
+
const EMBEDDING_CACHE_FLOOR = 4000;
|
|
1611
|
+
const EMBEDDING_CACHE_BUDGET_BYTES = 256 * 1024 * 1024;
|
|
1612
|
+
let embeddingCacheCap = EMBEDDING_CACHE_FLOOR;
|
|
1613
|
+
let embeddingCacheHits = 0;
|
|
1614
|
+
let embeddingCacheMisses = 0;
|
|
1615
|
+
|
|
1616
|
+
/** Vector-like = number[] or Float32Array (typed). All cosine guards use this
|
|
1617
|
+
* instead of Array.isArray, which is false for typed arrays — with the old
|
|
1618
|
+
* guard every cached vector would silently score 0. */
|
|
1619
|
+
function isVectorLike(v) {
|
|
1620
|
+
return Array.isArray(v) || (ArrayBuffer.isView(v) && !(v instanceof DataView));
|
|
1621
|
+
}
|
|
1425
1622
|
|
|
1426
1623
|
function cacheEmbedding(id, vector) {
|
|
1427
|
-
if (embeddingCache.size >=
|
|
1624
|
+
if (embeddingCache.size >= embeddingCacheCap && !embeddingCache.has(id)) {
|
|
1428
1625
|
const oldest = embeddingCache.keys().next().value;
|
|
1429
1626
|
embeddingCache.delete(oldest);
|
|
1430
1627
|
}
|
|
1431
1628
|
embeddingCache.set(id, vector);
|
|
1432
1629
|
}
|
|
1433
1630
|
|
|
1434
|
-
/**
|
|
1435
|
-
*
|
|
1436
|
-
|
|
1631
|
+
/** Raise the cap to fit a sweep of `rows` rows (×1.25 headroom), clamped by
|
|
1632
|
+
* the memory budget. Called before a full scan so the scan itself does not
|
|
1633
|
+
* evict what it just cached. */
|
|
1634
|
+
function noteSweep(rows, avgDim) {
|
|
1635
|
+
if (!rows || !avgDim) return;
|
|
1636
|
+
const perVector = avgDim * Float32Array.BYTES_PER_ELEMENT;
|
|
1637
|
+
const budgetRows = Math.floor(EMBEDDING_CACHE_BUDGET_BYTES / perVector);
|
|
1638
|
+
const want = Math.ceil(rows * 1.25);
|
|
1639
|
+
embeddingCacheCap = Math.max(EMBEDDING_CACHE_FLOOR, Math.min(budgetRows, want));
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/** Cache-first read returning Float32Array (internal hot path only).
|
|
1643
|
+
* undefined when none/parse failure. */
|
|
1644
|
+
function readVector(id) {
|
|
1437
1645
|
const cached = embeddingCache.get(id);
|
|
1438
|
-
if (cached) return cached;
|
|
1646
|
+
if (cached) { embeddingCacheHits += 1; return cached; }
|
|
1647
|
+
embeddingCacheMisses += 1;
|
|
1439
1648
|
const row = db.prepare("SELECT embedding FROM memories WHERE id = ?").get(id);
|
|
1440
1649
|
if (!row?.embedding) return undefined;
|
|
1441
1650
|
try {
|
|
1442
|
-
const
|
|
1443
|
-
if (Array.isArray(
|
|
1651
|
+
const parsed = JSON.parse(row.embedding);
|
|
1652
|
+
if (Array.isArray(parsed) && parsed.length) {
|
|
1653
|
+
const v = new Float32Array(parsed);
|
|
1444
1654
|
cacheEmbedding(id, v);
|
|
1445
1655
|
return v;
|
|
1446
1656
|
}
|
|
@@ -1448,6 +1658,27 @@ export function createStore(path) {
|
|
|
1448
1658
|
return undefined;
|
|
1449
1659
|
}
|
|
1450
1660
|
|
|
1661
|
+
/** Parsed embedding for one id (cache-first); undefined when none/parse
|
|
1662
|
+
* failure. Single read path shared by searchVector / getEmbedding(s).
|
|
1663
|
+
* Public contract: returns number[] (callers guard with Array.isArray). */
|
|
1664
|
+
function getParsedEmbedding(id) {
|
|
1665
|
+
const v = readVector(id);
|
|
1666
|
+
return v ? Array.from(v) : undefined;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/** Cache instrumentation for the regression lock (hit rate must not collapse
|
|
1670
|
+
* back to 0 when the working set outgrows a fixed cap). */
|
|
1671
|
+
function embeddingCacheStats() {
|
|
1672
|
+
const total = embeddingCacheHits + embeddingCacheMisses;
|
|
1673
|
+
return {
|
|
1674
|
+
size: embeddingCache.size,
|
|
1675
|
+
cap: embeddingCacheCap,
|
|
1676
|
+
hits: embeddingCacheHits,
|
|
1677
|
+
misses: embeddingCacheMisses,
|
|
1678
|
+
hitRate: total ? embeddingCacheHits / total : 0
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1451
1682
|
// all()/list() 的显式列清单:从实际 schema 派生(迁移后),只排除 embedding
|
|
1452
1683
|
// 列——consolidation/sleep/inject 的消费方都不读向量,而它是全表里最重的一列
|
|
1453
1684
|
// (5k 行库 ≈ 9.4 MB TEXT),每次全表调用都白搬。经 PRAGMA 派生而非硬编码,
|
|
@@ -1555,13 +1786,12 @@ export function createStore(path) {
|
|
|
1555
1786
|
return db.prepare("DELETE FROM distill_parse_failures WHERE session_id = ?").run(sessionId).changes;
|
|
1556
1787
|
}
|
|
1557
1788
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
}
|
|
1789
|
+
// #597:count() 与 countByType() 共用的 WHERE 构造——两张 HTTP 面(宿主内 /list
|
|
1790
|
+
// 与 Bearer /memories)的 total 与 typeCounts 必须同口径,分布的合计才等于总数。
|
|
1791
|
+
// type 刻意不在这里:count() 按调用方带,countByType() 刻意不带(它要的就是
|
|
1792
|
+
// 同一份筛选下、**不带类型**的全库分布——面板分类栏被 type 筛过的页上数不出它)。
|
|
1793
|
+
function pushCountFilters(opts, clauses, params) {
|
|
1794
|
+
const { minImportance = null, source = null, includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, updatedFrom = null, updatedTo = null, occurredFrom = null, occurredTo = null, visibility = null } = opts ?? {};
|
|
1565
1795
|
// Same filters as list() so a paged caller's total matches its rows.
|
|
1566
1796
|
if (minImportance != null) {
|
|
1567
1797
|
clauses.push("importance >= ?");
|
|
@@ -1627,10 +1857,32 @@ export function createStore(path) {
|
|
|
1627
1857
|
"(id IN (SELECT record_id FROM receipt_chain WHERE kind IN ('merge', 'update') AND verdict = 'live') OR source = 'dream')"
|
|
1628
1858
|
);
|
|
1629
1859
|
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function count(type, opts = {}) {
|
|
1863
|
+
const clauses = [];
|
|
1864
|
+
const params = [];
|
|
1865
|
+
if (type !== undefined) {
|
|
1866
|
+
clauses.push("type = ?");
|
|
1867
|
+
params.push(type);
|
|
1868
|
+
}
|
|
1869
|
+
pushCountFilters(opts, clauses, params);
|
|
1630
1870
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1631
1871
|
return db.prepare(`SELECT count(*) AS c FROM memories ${where}`).get(...params).c;
|
|
1632
1872
|
}
|
|
1633
1873
|
|
|
1874
|
+
// #597(/list 的 typeCounts):同一份筛选、不带 type,一次 GROUP BY 拿全库分布。
|
|
1875
|
+
// 面板分类栏的数字源:被 type 筛过又分过页的已加载行上数不出真分布。GROUP BY
|
|
1876
|
+
// 只回存在的类,缺类即 0;分布合计恒等于同筛选的 count(undefined)。
|
|
1877
|
+
function countByType(opts = {}) {
|
|
1878
|
+
const clauses = [];
|
|
1879
|
+
const params = [];
|
|
1880
|
+
pushCountFilters(opts, clauses, params);
|
|
1881
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1882
|
+
const rows = db.prepare(`SELECT type, count(*) AS c FROM memories ${where} GROUP BY type`).all(...params);
|
|
1883
|
+
return Object.fromEntries(rows.map((r) => [r.type, r.c]));
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1634
1886
|
function getById(id) {
|
|
1635
1887
|
const row = db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
|
|
1636
1888
|
return toRow(row);
|
|
@@ -1883,6 +2135,11 @@ export function createStore(path) {
|
|
|
1883
2135
|
db.prepare("DELETE FROM memory_provenance WHERE memory_id = ?").run(id);
|
|
1884
2136
|
// #512:铸造记录同理——它也只是指着记忆行的索引,行没了索引就没有指代对象。
|
|
1885
2137
|
db.prepare("DELETE FROM memory_mints WHERE memory_id = ?").run(id);
|
|
2138
|
+
// #249 第二批:pin 行同理,理由与前两条完全一致(本表建表时也没抄
|
|
2139
|
+
// REFERENCES ... ON DELETE CASCADE,清理不挂驱动默认)。注意这一条**只
|
|
2140
|
+
// 防攒垃圾**,不修「回落被卡住」——注入侧判据看的是「有几条能用的」
|
|
2141
|
+
// 而不是「表里有几行」,见 service.js 注入段与 pinnedStats.source 三档。
|
|
2142
|
+
db.prepare("DELETE FROM pinned_memories WHERE memory_id = ?").run(id);
|
|
1886
2143
|
db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
1887
2144
|
// Mirror sync must reflect the deletion; bump desired generation so a
|
|
1888
2145
|
// crash between the delete and syncMirror leaves a recoverable debt.
|
|
@@ -1950,6 +2207,8 @@ export function createStore(path) {
|
|
|
1950
2207
|
const marks = chunk.map(() => "?").join(", ");
|
|
1951
2208
|
db.prepare(`DELETE FROM memory_provenance WHERE memory_id IN (${marks})`).run(...chunk);
|
|
1952
2209
|
db.prepare(`DELETE FROM memory_mints WHERE memory_id IN (${marks})`).run(...chunk);
|
|
2210
|
+
// 同 remove():pin 行防攒垃圾(不修卡回落的那个判据问题)。
|
|
2211
|
+
db.prepare(`DELETE FROM pinned_memories WHERE memory_id IN (${marks})`).run(...chunk);
|
|
1953
2212
|
deleted += db.prepare(`DELETE FROM memories WHERE id IN (${marks})`).run(...chunk).changes;
|
|
1954
2213
|
}
|
|
1955
2214
|
incrementGeneration();
|
|
@@ -2311,7 +2570,10 @@ export function createStore(path) {
|
|
|
2311
2570
|
* name; omitting modelHash leaves the column untouched (callers that
|
|
2312
2571
|
* predate the column still work, their rows simply read as unknown). */
|
|
2313
2572
|
function setEmbedding(id, vector, modelHash) {
|
|
2314
|
-
|
|
2573
|
+
// isVectorLike 而非 Array.isArray:调用方可能递进 Float32Array(本模块缓存的
|
|
2574
|
+
// 就是它)。旧守卫对 typed array 返回 false,会把「设置向量」读成「清空向量」,
|
|
2575
|
+
// 静默把行改成 NULL。
|
|
2576
|
+
const json = isVectorLike(vector) && vector.length ? JSON.stringify(Array.from(vector)) : null;
|
|
2315
2577
|
if (json) {
|
|
2316
2578
|
if (modelHash !== undefined) {
|
|
2317
2579
|
db.prepare("UPDATE memories SET embedding = ?, embedding_model = ? WHERE id = ?")
|
|
@@ -2324,15 +2586,10 @@ export function createStore(path) {
|
|
|
2324
2586
|
}
|
|
2325
2587
|
// Issue #202:单一写入口即单一失效点——重写缓存条目(向量在手上不必重解析),
|
|
2326
2588
|
// 清空则移除,删除过的 id 不会被下一次 searchVector 的 id 集合选中。
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
else embeddingCache.delete(id);
|
|
2332
|
-
} catch { embeddingCache.delete(id); }
|
|
2333
|
-
} else {
|
|
2334
|
-
embeddingCache.delete(id);
|
|
2335
|
-
}
|
|
2589
|
+
// 缓存里存 Float32Array(与 readVector 同一表示),不存 JSON.parse 出来的
|
|
2590
|
+
// number[],否则写后第一次读还要再转一次。
|
|
2591
|
+
if (json) cacheEmbedding(id, new Float32Array(vector));
|
|
2592
|
+
else embeddingCache.delete(id);
|
|
2336
2593
|
}
|
|
2337
2594
|
|
|
2338
2595
|
/** Batch fetch stored embeddings by id (v0.5.0 search-time semantic dedup).
|
|
@@ -2411,6 +2668,30 @@ export function createStore(path) {
|
|
|
2411
2668
|
return needsEmbeddingFor(modelHash, 1).length > 0;
|
|
2412
2669
|
}
|
|
2413
2670
|
|
|
2671
|
+
/**
|
|
2672
|
+
* How many rows needsEmbeddingFor() would pick in total.
|
|
2673
|
+
*
|
|
2674
|
+
* A re-index run is bounded by a limit, so "indexed: 0" and "indexed: 3 of
|
|
2675
|
+
* 300" looked identical to the caller. Counting is cheap (no content is
|
|
2676
|
+
* materialised) and is what lets the caller say how much work is left.
|
|
2677
|
+
*/
|
|
2678
|
+
function countNeedsEmbeddingFor(modelHash) {
|
|
2679
|
+
if (!modelHash) {
|
|
2680
|
+
return db.prepare(
|
|
2681
|
+
`SELECT count(*) AS c FROM memories
|
|
2682
|
+
WHERE (embedding IS NULL OR embedding = '')
|
|
2683
|
+
AND forgotten = 0 AND archived = 0`
|
|
2684
|
+
).get().c;
|
|
2685
|
+
}
|
|
2686
|
+
return db.prepare(
|
|
2687
|
+
`SELECT count(*) AS c FROM memories
|
|
2688
|
+
WHERE ((embedding IS NULL OR embedding = '')
|
|
2689
|
+
OR embedding_model IS NULL
|
|
2690
|
+
OR embedding_model != ?)
|
|
2691
|
+
AND forgotten = 0 AND archived = 0`
|
|
2692
|
+
).get(String(modelHash)).c;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2414
2695
|
/** Per-model provenance counts over active rows ('' = unknown origin). */
|
|
2415
2696
|
function embeddingModelBreakdown() {
|
|
2416
2697
|
return db.prepare(
|
|
@@ -2493,18 +2774,21 @@ export function createStore(path) {
|
|
|
2493
2774
|
*/
|
|
2494
2775
|
function vectorDimensionState(dimension) {
|
|
2495
2776
|
if (!Number.isInteger(dimension) || dimension <= 0) return { state: "invalid" };
|
|
2496
|
-
// 与 searchVector
|
|
2777
|
+
// 与 searchVector 的行集逐条相同(顺序不同、语义等价)——同源行集,同步改。
|
|
2778
|
+
// 同样不带 `embedding != ''`:只取 id,读 embedding 列纯属白搬;空串行由
|
|
2779
|
+
// readVector 返回 undefined,落到下面 sawVector 分支(不影响 unreadable 判定)。
|
|
2497
2780
|
const rows = db.prepare(
|
|
2498
2781
|
`SELECT id FROM memories
|
|
2499
|
-
WHERE embedding IS NOT NULL
|
|
2782
|
+
WHERE embedding IS NOT NULL
|
|
2500
2783
|
AND forgotten = 0 AND archived = 0`
|
|
2501
2784
|
).all();
|
|
2502
2785
|
if (!rows.length) return { state: "none" };
|
|
2503
2786
|
let sawVector = false;
|
|
2787
|
+
noteSweep(rows.length, dimension);
|
|
2504
2788
|
for (const row of rows) {
|
|
2505
|
-
//
|
|
2506
|
-
//
|
|
2507
|
-
const v =
|
|
2789
|
+
// 走同一解析缓存,不自己 JSON.parse——与 searchVector / getEmbedding(s)
|
|
2790
|
+
// 同一条读路径,缓存热时零解析。
|
|
2791
|
+
const v = readVector(row.id);
|
|
2508
2792
|
if (!v || !v.length) continue;
|
|
2509
2793
|
sawVector = true;
|
|
2510
2794
|
if (v.length === dimension) return { state: "match" };
|
|
@@ -2538,7 +2822,7 @@ export function createStore(path) {
|
|
|
2538
2822
|
// --- vector search ------------------------------------------------------
|
|
2539
2823
|
|
|
2540
2824
|
function cosine(a, b) {
|
|
2541
|
-
if (!
|
|
2825
|
+
if (!isVectorLike(a) || !isVectorLike(b) || a.length !== b.length || !a.length) return 0;
|
|
2542
2826
|
let dot = 0;
|
|
2543
2827
|
let na = 0;
|
|
2544
2828
|
let nb = 0;
|
|
@@ -2561,13 +2845,22 @@ export function createStore(path) {
|
|
|
2561
2845
|
// Issue #202:只取 id 列(~40 KB)而非 SELECT *(~11 MB)——向量从缓存取,
|
|
2562
2846
|
// 缓存未命中时按 id 点查 embedding 再解析回填。SQL 里保留 embedding 的
|
|
2563
2847
|
// IS NOT NULL 过滤,保证「有向量的行」集合始终以库为准(删除/清空即时生效)。
|
|
2848
|
+
//
|
|
2849
|
+
// 为什么不带 `embedding != ''`(2026-11-05 实测):`!= ''` 要读 embedding 列
|
|
2850
|
+
// 才能判定,SQLite 就得把 134MB 向量文本整个从磁盘搬一遍——而这里只 SELECT
|
|
2851
|
+
// id,一条文本都不用。真库副本实测:这条 SQL 单独跑 265ms → 96ms;落到
|
|
2852
|
+
// 一轮完整检索上是 138ms → 47ms(省约 90ms,其余是解析/打分开销)。
|
|
2853
|
+
// 空串行(外部直接 UPDATE 造出来的,setEmbedding 写的是 NULL)由 readVector
|
|
2854
|
+
// 兜住:JSON.parse('') 抛错 → 返回 undefined → 跳过,既不计分也不崩。
|
|
2564
2855
|
const rows = db.prepare(
|
|
2565
2856
|
`SELECT id FROM memories
|
|
2566
|
-
WHERE ${archivedFilter}forgotten = 0 AND embedding IS NOT NULL
|
|
2857
|
+
WHERE ${archivedFilter}forgotten = 0 AND embedding IS NOT NULL`
|
|
2567
2858
|
).all();
|
|
2568
2859
|
const scored = [];
|
|
2860
|
+
// 自适应上限:本次要扫这么多行,先让缓存装得下,否则会边扫边驱逐自己。
|
|
2861
|
+
noteSweep(rows.length, vector.length);
|
|
2569
2862
|
for (const row of rows) {
|
|
2570
|
-
const v =
|
|
2863
|
+
const v = readVector(row.id);
|
|
2571
2864
|
if (!v) continue;
|
|
2572
2865
|
// distill-tx-embed-gap 顺带修:维度不匹配的行按维度分流,直接跳过。旧路径
|
|
2573
2866
|
// 喂进 cosine 不等长就 return 0,换了模型/维度的旧向量以 0 分混进结果尾部
|
|
@@ -3474,6 +3767,18 @@ export function createStore(path) {
|
|
|
3474
3767
|
return { path: typeof path === "string" ? path : null, pageSize, pageCount, freelistCount, fileBytes };
|
|
3475
3768
|
}
|
|
3476
3769
|
|
|
3770
|
+
/**
|
|
3771
|
+
* 容量预警的行数口径(#275 空间维度):活跃 / 归档各一格。体积与空闲页在
|
|
3772
|
+
* storageStats(),这里只补行数——两条 COUNT 的成本,挂在 /health 的按需读上,
|
|
3773
|
+
* 没有周期性消费者。归档增速(净增速率)是 recall-stats 的 archive 块(第五
|
|
3774
|
+
* 指标,按精确 archived_at 算),这里刻意不算速率:同一个数字长两处就会分叉。
|
|
3775
|
+
*/
|
|
3776
|
+
function capacityStats() {
|
|
3777
|
+
const active = db.prepare("SELECT COUNT(*) AS n FROM memories WHERE archived = 0").get().n;
|
|
3778
|
+
const archived = db.prepare("SELECT COUNT(*) AS n FROM memories WHERE archived = 1").get().n;
|
|
3779
|
+
return { active, archived };
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3477
3782
|
/**
|
|
3478
3783
|
* 整库 VACUUM(#275 的手动步骤):代价 O(库大小) 且需要排他写锁,所以只在显式
|
|
3479
3784
|
* 入口里跑,绝不挂启动路径、也不开 auto_vacuum。
|
|
@@ -3536,7 +3841,10 @@ export function createStore(path) {
|
|
|
3536
3841
|
* pending once — a re-detection in a later dream run is a no-op, never a
|
|
3537
3842
|
* duplicate queue entry. Returns the pending row (freshly inserted, or the
|
|
3538
3843
|
* existing unresolved row when the pair is already pending), or undefined
|
|
3539
|
-
* when the re-detection is suppressed.
|
|
3844
|
+
* when the re-detection is suppressed. The returned row carries `fresh`:
|
|
3845
|
+
* true only when this call actually inserted a row, false when the pair was
|
|
3846
|
+
* already pending (issue #603 — callers used to read "non-null" as "parked",
|
|
3847
|
+
* so a re-detection re-reported the same pair as a new freeze every run).
|
|
3540
3848
|
*
|
|
3541
3849
|
* v0.8.1(issue #170 复核项 4):人工已裁决过的对(resolved_at 非空)不再
|
|
3542
3850
|
* 反复入队——除非任一侧在裁决之后又被改过(updated_at 晚于 resolved_at)。
|
|
@@ -3544,12 +3852,12 @@ export function createStore(path) {
|
|
|
3544
3852
|
* updated_at 是宽松代理(任何字段更新都会触发重新评审)——宁可多看一眼,
|
|
3545
3853
|
* 不静音真冲突。
|
|
3546
3854
|
*/
|
|
3547
|
-
function saveConflictPending({ run_id, memory_a, memory_b, reason }) {
|
|
3855
|
+
function saveConflictPending({ run_id, memory_a, memory_b, reason, similarity }) {
|
|
3548
3856
|
const [a, b] = [memory_a, memory_b].sort();
|
|
3549
3857
|
const existing = db.prepare(
|
|
3550
3858
|
"SELECT * FROM conflict_pending WHERE memory_a = ? AND memory_b = ? AND resolved_at IS NULL LIMIT 1"
|
|
3551
3859
|
).get(a, b);
|
|
3552
|
-
if (existing) return toConflictPending(existing);
|
|
3860
|
+
if (existing) return { ...toConflictPending(existing), fresh: false };
|
|
3553
3861
|
const reviewed = db.prepare(
|
|
3554
3862
|
"SELECT * FROM conflict_pending WHERE memory_a = ? AND memory_b = ? AND resolved_at IS NOT NULL ORDER BY resolved_at DESC, rowid DESC LIMIT 1"
|
|
3555
3863
|
).get(a, b);
|
|
@@ -3561,16 +3869,29 @@ export function createStore(path) {
|
|
|
3561
3869
|
}
|
|
3562
3870
|
const id = randomUUID();
|
|
3563
3871
|
const now = nowIso();
|
|
3872
|
+
// Issue #603 第 2 层:相似度落列。调用方拿不到(LLM 裁决路径只有理由文本)
|
|
3873
|
+
// 就写 NULL,不猜——面板与排序都按「未知」处理,而不是当成 0。
|
|
3874
|
+
const sim = typeof similarity === "number" && Number.isFinite(similarity)
|
|
3875
|
+
&& similarity >= 0 && similarity <= 1 ? similarity : null;
|
|
3564
3876
|
db.prepare(
|
|
3565
|
-
`INSERT INTO conflict_pending (id, run_id, memory_a, memory_b, reason, created_at)
|
|
3566
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
3567
|
-
).run(id, run_id ?? null, a, b, reason ?? null, now);
|
|
3568
|
-
return toConflictPending(db.prepare("SELECT * FROM conflict_pending WHERE id = ?").get(id));
|
|
3877
|
+
`INSERT INTO conflict_pending (id, run_id, memory_a, memory_b, reason, similarity, created_at)
|
|
3878
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
3879
|
+
).run(id, run_id ?? null, a, b, reason ?? null, sim, now);
|
|
3880
|
+
return { ...toConflictPending(db.prepare("SELECT * FROM conflict_pending WHERE id = ?").get(id)), fresh: true };
|
|
3569
3881
|
}
|
|
3570
3882
|
|
|
3571
3883
|
/**
|
|
3572
|
-
* List pending conflicts,
|
|
3573
|
-
* pass includeResolved to include resolved ones (audit view).
|
|
3884
|
+
* List pending conflicts, most-similar first. Unresolved rows only by
|
|
3885
|
+
* default; pass includeResolved to include resolved ones (audit view).
|
|
3886
|
+
*
|
|
3887
|
+
* Issue #603 第 2 层:排序键从「created_at DESC」改成「相似度 DESC → created_at
|
|
3888
|
+
* DESC」。原键在同一次 sleep 轮次里全同(实测 60 条落在 60ms 内),队列在面板
|
|
3889
|
+
* 上等于随机;相似度是「该先看哪对」的唯一客观信号,现在它是一列而不是 reason
|
|
3890
|
+
* 文本里的正则。
|
|
3891
|
+
*
|
|
3892
|
+
* NULL 排最后而不是当 0:存量行与 LLM 裁决路径没记相似度,把它们混进「低相似
|
|
3893
|
+
* 度」会把「未知」伪装成「已测量」。`(similarity IS NULL)` 前缀让这条不依赖
|
|
3894
|
+
* SQLite 的 NULL 排序约定,两个方向都稳。
|
|
3574
3895
|
*/
|
|
3575
3896
|
function listConflictPending({ limit = 50, offset = 0, includeResolved = false } = {}) {
|
|
3576
3897
|
const { limit: lim, offset: off } = sanitizePage(limit, offset, 50);
|
|
@@ -3579,7 +3900,9 @@ export function createStore(path) {
|
|
|
3579
3900
|
if (!includeResolved) clauses.push("resolved_at IS NULL");
|
|
3580
3901
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
3581
3902
|
const rows = db.prepare(
|
|
3582
|
-
`SELECT * FROM conflict_pending ${where}
|
|
3903
|
+
`SELECT * FROM conflict_pending ${where}
|
|
3904
|
+
ORDER BY (similarity IS NULL), similarity DESC, created_at DESC, id
|
|
3905
|
+
LIMIT ? OFFSET ?`
|
|
3583
3906
|
).all(...params, lim, off);
|
|
3584
3907
|
return rows.map(toConflictPending);
|
|
3585
3908
|
}
|
|
@@ -3804,6 +4127,85 @@ export function createStore(path) {
|
|
|
3804
4127
|
return rows.map(toScopeState);
|
|
3805
4128
|
}
|
|
3806
4129
|
|
|
4130
|
+
// --- pinned_memories: 显式 pin(#249 第二批) ------------------------------
|
|
4131
|
+
// 四个函数只管「哪条被钉住了」这条事实,不回答「该不该注入」——注入侧的
|
|
4132
|
+
// 越权/暂停作用域/遗忘判定一律留在 service(与 strictScope、scopeStateGate
|
|
4133
|
+
// 同处,见 injectCandidates 的位置纪律)。存储层越薄,越不会被绕过。
|
|
4134
|
+
|
|
4135
|
+
/**
|
|
4136
|
+
* 全部 pin 行,按 pin 先后(created_at ASC)——注入就按这个序前置,**先钉的
|
|
4137
|
+
* 排在最前**。预算不够(钉的条数 > pinnedInjectBudget)时,被 eligible.slice
|
|
4138
|
+
* 取走的是最早钉的那几条,最近才钉的排后面、先被挤掉。
|
|
4139
|
+
*
|
|
4140
|
+
* 这个方向和「稳定偏好」的理由是一致的:pin 表达的是「这条我要长期带着」,
|
|
4141
|
+
* 越早做出这个承诺、越久没被撤回,就越该占住名额;若反过来让新钉的优先,
|
|
4142
|
+
* pin 就退化成「最近关注」清单,和 updated_at DESC 那个错误排序键同病。
|
|
4143
|
+
*
|
|
4144
|
+
* 注意排序键是「什么时候钉的」,不是内容的 updated_at——一条 10 月的老记忆
|
|
4145
|
+
* 今天才钉,它在 pin 序里是新的、在内容上是老的,两个轴不要混。
|
|
4146
|
+
* 次键 rowid:同毫秒连钉两条时 UUID 主键排序是随机的,回执要可复现。
|
|
4147
|
+
*/
|
|
4148
|
+
function listPinned() {
|
|
4149
|
+
return db.prepare("SELECT * FROM pinned_memories ORDER BY created_at ASC, rowid ASC").all().map(toPinned);
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
/**
|
|
4153
|
+
* 钉住一条记忆。重复 pin 同一 id 不刷新 created_at(只补 note)——pin 顺序
|
|
4154
|
+
* 是「第一次钉的时间」,重钉不该把老偏好的位置往后推。调用方负责确认目标
|
|
4155
|
+
* 记忆存在;这里不做外键校验(本库其余表同款:关系表不设 FK,删记忆时由
|
|
4156
|
+
* unpinMemory 或下一次读的 forgotten/archived 过滤兜住)。
|
|
4157
|
+
*/
|
|
4158
|
+
function pinMemory(memoryId, { note, created_at } = {}) {
|
|
4159
|
+
if (typeof memoryId !== "string" || !memoryId.trim()) return undefined;
|
|
4160
|
+
const id = memoryId.trim();
|
|
4161
|
+
const at = normalizeOccurredAt(created_at) ?? nowIso();
|
|
4162
|
+
db.prepare(
|
|
4163
|
+
`INSERT INTO pinned_memories (memory_id, created_at, note) VALUES (?, ?, ?)
|
|
4164
|
+
ON CONFLICT(memory_id) DO UPDATE SET note = COALESCE(excluded.note, pinned_memories.note)`
|
|
4165
|
+
).run(id, at, normalizeScopeText(note));
|
|
4166
|
+
return toPinned(db.prepare("SELECT * FROM pinned_memories WHERE memory_id = ?").get(id));
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
/** 松开一条 pin。返回实际删掉的行数(0 = 本来就没钉,不是错误)。 */
|
|
4170
|
+
function unpinMemory(memoryId) {
|
|
4171
|
+
if (typeof memoryId !== "string" || !memoryId.trim()) return 0;
|
|
4172
|
+
return db.prepare("DELETE FROM pinned_memories WHERE memory_id = ?").run(memoryId.trim()).changes;
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
/** pin 行数。注入侧靠它判「表是空的」——空表要回落到旧的类型口径。 */
|
|
4176
|
+
function countPinned() {
|
|
4177
|
+
return db.prepare("SELECT count(*) AS c FROM pinned_memories").get().c;
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
/**
|
|
4181
|
+
* 这条记忆是否被显式钉住(单行点查,读侧)。
|
|
4182
|
+
*
|
|
4183
|
+
* 存在的理由是 #593 的缺口:升格吸收(document.js 的 evidence 归档)只拿得到
|
|
4184
|
+
* 记忆行、拿不到 pin 表,于是它只能按**类型**猜「谁属于保真池」。显式 pin 落地
|
|
4185
|
+
* 后,一条 pattern / decision 被钉住仍然是 pin,按类型猜就把它当普通行收走了。
|
|
4186
|
+
* 谓词必须来自本表,不能从类型推——这正是 #593 把 pin 从类型推断改成显式存储
|
|
4187
|
+
* 的同一件事,别在第二个地方又推回去。
|
|
4188
|
+
*/
|
|
4189
|
+
function isPinned(memoryId) {
|
|
4190
|
+
if (typeof memoryId !== "string" || !memoryId.trim()) return false;
|
|
4191
|
+
return db.prepare("SELECT 1 AS hit FROM pinned_memories WHERE memory_id = ?").get(memoryId.trim()) !== undefined;
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
/**
|
|
4195
|
+
* 按 id 点查注入所需的行(绕开 200 行窗口的唯一通道)。
|
|
4196
|
+
*
|
|
4197
|
+
* 显式列清单复用 memoryColumnList:它经 PRAGMA 派生、已排除 embedding
|
|
4198
|
+
* (1024 维浮点文本 ≈ 34KB/条,5 条就白搬 170KB 而注入渲染根本不用向量)。
|
|
4199
|
+
* 返回顺序不保证(IN 的求值序),调用方按自己的次序(pin created_at)重排。
|
|
4200
|
+
*/
|
|
4201
|
+
function listByIdsForInject(ids) {
|
|
4202
|
+
const wanted = Array.isArray(ids) ? ids.filter((id) => typeof id === "string" && id) : [];
|
|
4203
|
+
if (wanted.length === 0) return [];
|
|
4204
|
+
const placeholders = wanted.map(() => "?").join(", ");
|
|
4205
|
+
return db.prepare(`SELECT ${memoryColumnList} FROM memories WHERE id IN (${placeholders})`)
|
|
4206
|
+
.all(...wanted).map(toRow);
|
|
4207
|
+
}
|
|
4208
|
+
|
|
3807
4209
|
function getFailureStats({ since } = {}) {
|
|
3808
4210
|
const clause = since ? "WHERE created_at >= ?" : "";
|
|
3809
4211
|
const params = since ? [since] : [];
|
|
@@ -4010,6 +4412,26 @@ export function createStore(path) {
|
|
|
4010
4412
|
return result.changes > 0;
|
|
4011
4413
|
}
|
|
4012
4414
|
|
|
4415
|
+
/**
|
|
4416
|
+
* 覆盖一条关系行的 metadata(#619)。关系本身仍按 saveRelation 追加,
|
|
4417
|
+
* 这条只给「同一条边被后续观察补充证据」用——情感相位判重命中时要把
|
|
4418
|
+
* evidence 并集与 sessionCount 写回既有边,否则边上的场合数会永远停在
|
|
4419
|
+
* 第一次写入那一刻。只改 metadata,不动 weight/source/created_at:这条边的
|
|
4420
|
+
* 来源与权重反映的是它第一次被建立时的事实。
|
|
4421
|
+
* 幂等(缺行 changes=0 → false)。
|
|
4422
|
+
*/
|
|
4423
|
+
function updateRelationMetadata(id, metadata) {
|
|
4424
|
+
const metaStr = metadata === undefined
|
|
4425
|
+
? null
|
|
4426
|
+
: typeof metadata === "string"
|
|
4427
|
+
? metadata
|
|
4428
|
+
: JSON.stringify(metadata);
|
|
4429
|
+
const result = db.prepare(
|
|
4430
|
+
"UPDATE entity_relations SET metadata = ? WHERE id = ?"
|
|
4431
|
+
).run(metaStr, id);
|
|
4432
|
+
return result.changes > 0;
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4013
4435
|
/**
|
|
4014
4436
|
* Re-point every attr row whose memory_id is fromMemoryId to toMemoryId
|
|
4015
4437
|
* (autoDream merge migration, v0.3.0 Phase 4 / 4.3.2). When the keeper
|
|
@@ -4246,10 +4668,21 @@ export function createStore(path) {
|
|
|
4246
4668
|
/** All entities (optionally name-filtered, newest first). Used by sleep phase 4
|
|
4247
4669
|
* orphan detection: an entity with zero relations is a candidate for relation
|
|
4248
4670
|
* completion. */
|
|
4249
|
-
function listEntities({ limit = 1000 } = {}) {
|
|
4671
|
+
function listEntities({ limit = 1000, q } = {}) {
|
|
4672
|
+
const needle = typeof q === "string" ? q.trim() : "";
|
|
4673
|
+
if (needle === "") {
|
|
4674
|
+
const rows = db.prepare(
|
|
4675
|
+
"SELECT * FROM entities ORDER BY last_seen DESC, name ASC LIMIT ?"
|
|
4676
|
+
).all(limit);
|
|
4677
|
+
return rows.map(toEntity);
|
|
4678
|
+
}
|
|
4679
|
+
// 名字过滤放在 SQL 侧:调用方若在 LIMIT 之后过滤,排在 limit 之外的实体
|
|
4680
|
+
// 就永远搜不到(面板按 limit=200 取回再本地过滤,1.6 万实体时命中率约 1%)。
|
|
4681
|
+
// LIKE 的 % / _ 用 ESCAPE 转义,否则用户输入一个 "%" 就会把整库匹配出来。
|
|
4682
|
+
const escaped = needle.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
4250
4683
|
const rows = db.prepare(
|
|
4251
|
-
"SELECT * FROM entities ORDER BY last_seen DESC, name ASC LIMIT ?"
|
|
4252
|
-
).all(limit);
|
|
4684
|
+
"SELECT * FROM entities WHERE name LIKE ? ESCAPE '\\' ORDER BY last_seen DESC, name ASC LIMIT ?"
|
|
4685
|
+
).all(`%${escaped}%`, limit);
|
|
4253
4686
|
return rows.map(toEntity);
|
|
4254
4687
|
}
|
|
4255
4688
|
|
|
@@ -4707,9 +5140,309 @@ export function createStore(path) {
|
|
|
4707
5140
|
});
|
|
4708
5141
|
}
|
|
4709
5142
|
|
|
5143
|
+
// --- chorography 向量(收尾档 P36–P38)---------------------------------
|
|
5144
|
+
// 一页一行(saveChoroPageVector 先删后插);dim 失配由检索侧判(查询向量
|
|
5145
|
+
// 的 dim ≠ 行 dim → 该行按 stale 处理,惰性全量重嵌)。
|
|
5146
|
+
function getChoroPageVectors(repoId) {
|
|
5147
|
+
return db.prepare(
|
|
5148
|
+
`SELECT v.owner_id, v.dim, v.model, v.vector, v.source_generated_at,
|
|
5149
|
+
m.slug, m.title, p.generated_at AS page_generated_at
|
|
5150
|
+
FROM choro_vectors v
|
|
5151
|
+
JOIN choro_pages p ON v.owner_id = p.id
|
|
5152
|
+
JOIN choro_modules m ON p.module_id = m.id
|
|
5153
|
+
WHERE m.repo_id = ?`
|
|
5154
|
+
).all(repoId);
|
|
5155
|
+
}
|
|
5156
|
+
|
|
5157
|
+
function saveChoroPageVector(pageId, { vector, dim, model, sourceGeneratedAt, createdAt }) {
|
|
5158
|
+
return runAtomically(() => {
|
|
5159
|
+
db.prepare("DELETE FROM choro_vectors WHERE owner_id = ?").run(pageId);
|
|
5160
|
+
db.prepare(
|
|
5161
|
+
`INSERT INTO choro_vectors (owner_id, dim, model, vector, source_generated_at, created_at)
|
|
5162
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
5163
|
+
).run(pageId, dim, model, JSON.stringify(vector), sourceGeneratedAt, createdAt);
|
|
5164
|
+
return { owner_id: pageId, dim };
|
|
5165
|
+
});
|
|
5166
|
+
}
|
|
5167
|
+
|
|
5168
|
+
// --- 「Agent 眼中的你」自动事实(2026-11-06,profile-facts 域)---
|
|
5169
|
+
// 三张表只服务本域,写路径全部走 runAtomically:facts 与记忆主表零交叉,不需要
|
|
5170
|
+
// service.transaction 的 pendingEmbed/pendingEntity 截断语义(那层是给写记忆行用的)。
|
|
5171
|
+
|
|
5172
|
+
function parseFactIds(text) {
|
|
5173
|
+
if (!text) return [];
|
|
5174
|
+
try {
|
|
5175
|
+
const v = JSON.parse(text);
|
|
5176
|
+
return Array.isArray(v) ? v.map((x) => String(x)) : [];
|
|
5177
|
+
} catch { return []; }
|
|
5178
|
+
}
|
|
5179
|
+
|
|
5180
|
+
function listProfileFacts(agentKey, { includeArchived = false } = {}) {
|
|
5181
|
+
const rows = db.prepare(
|
|
5182
|
+
`SELECT * FROM profile_facts WHERE agent_key = ? ${includeArchived ? "" : "AND status = 'active'"} ORDER BY updated_at DESC, id DESC`
|
|
5183
|
+
).all(agentKey);
|
|
5184
|
+
return rows.map((r) => ({ ...r, source_memory_ids: parseFactIds(r.source_memory_ids) }));
|
|
5185
|
+
}
|
|
5186
|
+
|
|
5187
|
+
/** 全库活跃事实按卡分组(注入 provider 的读口,见 src/profile-facts.js)。 */
|
|
5188
|
+
function listActiveFactsGrouped() {
|
|
5189
|
+
const rows = db.prepare(
|
|
5190
|
+
"SELECT agent_key, content FROM profile_facts WHERE status = 'active' ORDER BY updated_at DESC"
|
|
5191
|
+
).all();
|
|
5192
|
+
const grouped = { default: [], byAgent: {} };
|
|
5193
|
+
for (const r of rows) {
|
|
5194
|
+
if (r.agent_key === "") grouped.default.push(r.content);
|
|
5195
|
+
else (grouped.byAgent[r.agent_key] ??= []).push(r.content);
|
|
5196
|
+
}
|
|
5197
|
+
return grouped;
|
|
5198
|
+
}
|
|
5199
|
+
|
|
5200
|
+
function getProfileFact(id) {
|
|
5201
|
+
const r = db.prepare("SELECT * FROM profile_facts WHERE id = ?").get(id);
|
|
5202
|
+
return r ? { ...r, source_memory_ids: parseFactIds(r.source_memory_ids) } : undefined;
|
|
5203
|
+
}
|
|
5204
|
+
|
|
5205
|
+
function insertProfileFact({ id, agentKey, content, sourceMemoryIds = [], at }) {
|
|
5206
|
+
db.prepare(
|
|
5207
|
+
"INSERT INTO profile_facts (id, agent_key, content, status, source_memory_ids, created_at, updated_at) VALUES (?, ?, ?, 'active', ?, ?, ?)"
|
|
5208
|
+
).run(id, agentKey, String(content), JSON.stringify(sourceMemoryIds ?? []), at, at);
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
/** 返回更新后的行;id 不存在返回 undefined,由调用方记入 failures。 */
|
|
5212
|
+
function updateProfileFact(id, { content, status, sourceMemoryIds, at }) {
|
|
5213
|
+
const cur = getProfileFact(id);
|
|
5214
|
+
if (!cur) return undefined;
|
|
5215
|
+
const next = {
|
|
5216
|
+
content: content ?? cur.content,
|
|
5217
|
+
status: status ?? cur.status,
|
|
5218
|
+
source_memory_ids: sourceMemoryIds ?? cur.source_memory_ids,
|
|
5219
|
+
updated_at: at
|
|
5220
|
+
};
|
|
5221
|
+
db.prepare("UPDATE profile_facts SET content = ?, status = ?, source_memory_ids = ?, updated_at = ? WHERE id = ?")
|
|
5222
|
+
.run(next.content, next.status, JSON.stringify(next.source_memory_ids), next.updated_at, id);
|
|
5223
|
+
return { ...cur, ...next };
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
function insertProfileFactJournal({ at, agentKey, runId = null, factId = null, op, before = null, after = null, actor = "auto" }) {
|
|
5227
|
+
db.prepare(
|
|
5228
|
+
"INSERT INTO profile_fact_journal (at, agent_key, run_id, fact_id, op, before, after, actor) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
5229
|
+
).run(at, agentKey, runId, factId, op, before, after, actor);
|
|
5230
|
+
}
|
|
5231
|
+
|
|
5232
|
+
function listProfileFactJournal(agentKey, { limit = 20 } = {}) {
|
|
5233
|
+
return db.prepare("SELECT * FROM profile_fact_journal WHERE agent_key = ? ORDER BY id DESC LIMIT ?")
|
|
5234
|
+
.all(agentKey, limit);
|
|
5235
|
+
}
|
|
5236
|
+
|
|
5237
|
+
/** 每卡只留最近 keep 条:账本是审计事实不是检索面,无界增长没有读者。 */
|
|
5238
|
+
function pruneProfileFactJournal(agentKey, keep = 200) {
|
|
5239
|
+
return db.prepare(
|
|
5240
|
+
`DELETE FROM profile_fact_journal WHERE agent_key = ? AND id NOT IN (
|
|
5241
|
+
SELECT id FROM profile_fact_journal WHERE agent_key = ? ORDER BY id DESC LIMIT ?)`
|
|
5242
|
+
).run(agentKey, agentKey, keep).changes;
|
|
5243
|
+
}
|
|
5244
|
+
|
|
5245
|
+
function insertProfileFactRun({ id, agentKey, triggerSource, status, cursor = null, inputCount = 0, opsApplied = 0, detail = null, startedAt, finishedAt }) {
|
|
5246
|
+
db.prepare(
|
|
5247
|
+
`INSERT INTO profile_fact_runs (id, agent_key, trigger_source, status, cursor, input_count, ops_applied, detail, started_at, finished_at)
|
|
5248
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
5249
|
+
).run(id, agentKey, triggerSource, status, cursor, inputCount, opsApplied, detail, startedAt, finishedAt);
|
|
5250
|
+
}
|
|
5251
|
+
|
|
5252
|
+
function listProfileFactRuns(agentKey, { limit = 10 } = {}) {
|
|
5253
|
+
return db.prepare("SELECT * FROM profile_fact_runs WHERE agent_key = ? ORDER BY started_at DESC, id DESC LIMIT ?")
|
|
5254
|
+
.all(agentKey, limit);
|
|
5255
|
+
}
|
|
5256
|
+
|
|
5257
|
+
/** 该卡最近一次 ok 轮的游标。failed 轮不消费——下次重跑同一窗口(蒸馏游标同款纪律)。 */
|
|
5258
|
+
function profileFactsCursor(agentKey) {
|
|
5259
|
+
const row = db.prepare(
|
|
5260
|
+
"SELECT cursor FROM profile_fact_runs WHERE agent_key = ? AND status = 'ok' ORDER BY started_at DESC, id DESC LIMIT 1"
|
|
5261
|
+
).get(agentKey);
|
|
5262
|
+
return row ? row.cursor : null;
|
|
5263
|
+
}
|
|
5264
|
+
|
|
5265
|
+
/**
|
|
5266
|
+
* 增量输入行:活跃(未归档未遗忘)+ 排除外来行(source IS NULL OR 那一支是三值
|
|
5267
|
+
* 逻辑必填,与 list() 的 excludeImported 同一个坑)+ 严格 scope 分区——agent 卡
|
|
5268
|
+
* 只吃 agent_scope=自己 的行,default 卡只吃 agent_scope IS NULL 的行。
|
|
5269
|
+
* updated_at 是 ISO 串,字典序即时间序,`>` 即「游标之后」(单调时间戳纪律)。
|
|
5270
|
+
*/
|
|
5271
|
+
function listFactInputRows(agentKey, { since = null, limit = 120 } = {}) {
|
|
5272
|
+
const scopeClause = agentKey ? "agent_scope = ?" : "agent_scope IS NULL";
|
|
5273
|
+
const params = [IMPORTED_SOURCE, ...(agentKey ? [agentKey] : [])];
|
|
5274
|
+
if (since) params.push(since);
|
|
5275
|
+
params.push(limit);
|
|
5276
|
+
return db.prepare(
|
|
5277
|
+
`SELECT id, type, title, content, importance, updated_at FROM memories
|
|
5278
|
+
WHERE forgotten = 0 AND archived = 0 AND (source IS NULL OR source <> ?)
|
|
5279
|
+
AND ${scopeClause}
|
|
5280
|
+
${since ? "AND updated_at > ?" : ""}
|
|
5281
|
+
ORDER BY updated_at DESC LIMIT ?`
|
|
5282
|
+
).all(...params);
|
|
5283
|
+
}
|
|
5284
|
+
|
|
5285
|
+
function countFactInputRows(agentKey, { since = null } = {}) {
|
|
5286
|
+
const scopeClause = agentKey ? "agent_scope = ?" : "agent_scope IS NULL";
|
|
5287
|
+
const params = [IMPORTED_SOURCE, ...(agentKey ? [agentKey] : [])];
|
|
5288
|
+
if (since) params.push(since);
|
|
5289
|
+
return db.prepare(
|
|
5290
|
+
`SELECT COUNT(*) AS n FROM memories
|
|
5291
|
+
WHERE forgotten = 0 AND archived = 0 AND (source IS NULL OR source <> ?)
|
|
5292
|
+
AND ${scopeClause}
|
|
5293
|
+
${since ? "AND updated_at > ?" : ""}`
|
|
5294
|
+
).get(...params).n;
|
|
5295
|
+
}
|
|
5296
|
+
|
|
5297
|
+
/**
|
|
5298
|
+
* 自动轮要维护的卡:有 scope 行的记忆身份 ∪ 存量卡片。存量卡即使输入行被清空
|
|
5299
|
+
* 也保留在名单里(手动轮还可能对它做人工修正后的再维护);'' 由调用方固定追加。
|
|
5300
|
+
*/
|
|
5301
|
+
function listFactCardKeys() {
|
|
5302
|
+
return db.prepare(
|
|
5303
|
+
`SELECT DISTINCT agent_scope AS k FROM memories WHERE agent_scope IS NOT NULL
|
|
5304
|
+
UNION
|
|
5305
|
+
SELECT DISTINCT agent_key FROM profile_facts WHERE agent_key <> ''`
|
|
5306
|
+
).all().map((r) => r.k);
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
/* --------------------- knowledge base(知识库第二层,§3.3) --------------------- */
|
|
5310
|
+
// 与 choro 那组表同一套纪律:本域只读写 kb_* 三张表,绝不进 memories /
|
|
5311
|
+
// saveWithDedupe / dream / 质量过滤的任何池子。时间戳由调用方传(一次 ingest
|
|
5312
|
+
// 的所有行共用一个时刻),store 不自取 now()——两处时钟会把「同一次入库」拆成
|
|
5313
|
+
// 两个版本,sha256 幂等判据也会跟着抖。
|
|
5314
|
+
|
|
5315
|
+
function kbCollectionByKey(key) {
|
|
5316
|
+
return db.prepare("SELECT * FROM kb_collections WHERE key = ?").get(String(key)) ?? undefined;
|
|
5317
|
+
}
|
|
5318
|
+
|
|
5319
|
+
/** 集合列表带计数:面板左栏要的不是空壳 key 列表,docs/chunks 是它的主信息。 */
|
|
5320
|
+
function listKbCollections() {
|
|
5321
|
+
return db.prepare(
|
|
5322
|
+
`SELECT c.*,
|
|
5323
|
+
(SELECT COUNT(*) FROM kb_docs d WHERE d.collection_id = c.id) AS doc_count,
|
|
5324
|
+
(SELECT COUNT(*) FROM kb_chunks k JOIN kb_docs d2 ON k.doc_id = d2.id
|
|
5325
|
+
WHERE d2.collection_id = c.id) AS chunk_count
|
|
5326
|
+
FROM kb_collections c ORDER BY c.key`
|
|
5327
|
+
).all();
|
|
5328
|
+
}
|
|
5329
|
+
|
|
5330
|
+
// key 是外部键(面板/工具用它寻址),title 是展示名。ON CONFLICT 不动 created_at:
|
|
5331
|
+
// 「这个集合哪天建的」是事实,重命名不该改写它。
|
|
5332
|
+
function upsertKbCollection({ key, title, description = null, at }) {
|
|
5333
|
+
db.prepare(
|
|
5334
|
+
`INSERT INTO kb_collections (key, title, description, created_at, updated_at)
|
|
5335
|
+
VALUES (?, ?, ?, ?, ?)
|
|
5336
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
5337
|
+
title = excluded.title,
|
|
5338
|
+
description = excluded.description,
|
|
5339
|
+
updated_at = excluded.updated_at`
|
|
5340
|
+
).run(String(key), String(title), description ?? null, at, at);
|
|
5341
|
+
return kbCollectionByKey(key);
|
|
5342
|
+
}
|
|
5343
|
+
|
|
5344
|
+
/** 删集合连带删它的文档与分块(三张表是一个整体,留孤儿行等于悄悄占空间)。 */
|
|
5345
|
+
function deleteKbCollection(key) {
|
|
5346
|
+
const row = kbCollectionByKey(key);
|
|
5347
|
+
if (!row) return false;
|
|
5348
|
+
return runAtomically(() => {
|
|
5349
|
+
db.prepare(
|
|
5350
|
+
"DELETE FROM kb_chunks WHERE doc_id IN (SELECT id FROM kb_docs WHERE collection_id = ?)"
|
|
5351
|
+
).run(row.id);
|
|
5352
|
+
db.prepare("DELETE FROM kb_docs WHERE collection_id = ?").run(row.id);
|
|
5353
|
+
db.prepare("DELETE FROM kb_collections WHERE id = ?").run(row.id);
|
|
5354
|
+
return true;
|
|
5355
|
+
});
|
|
5356
|
+
}
|
|
5357
|
+
|
|
5358
|
+
function listKbDocs(collectionId) {
|
|
5359
|
+
return db.prepare(
|
|
5360
|
+
"SELECT * FROM kb_docs WHERE collection_id = ? ORDER BY rel_key"
|
|
5361
|
+
).all(collectionId);
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
function kbDocByRelKey(collectionId, relKey) {
|
|
5365
|
+
return db.prepare(
|
|
5366
|
+
"SELECT * FROM kb_docs WHERE collection_id = ? AND rel_key = ?"
|
|
5367
|
+
).get(collectionId, String(relKey)) ?? undefined;
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
function kbChunksByDoc(docId) {
|
|
5371
|
+
return db.prepare(
|
|
5372
|
+
"SELECT * FROM kb_chunks WHERE doc_id = ? ORDER BY ordinal"
|
|
5373
|
+
).all(docId);
|
|
5374
|
+
}
|
|
5375
|
+
|
|
5376
|
+
/**
|
|
5377
|
+
* 入库落库的唯一写口。幂等判据是 **正文 sha256**(不是文件 stat):同 rel_key +
|
|
5378
|
+
* 同 sha256 = 整件事 no-op,回执 skipped 且不动 chunks——否则编辑器保存一次
|
|
5379
|
+
* (mtime 变、内容没变)就要全量重切块,库和索引一起白忙。
|
|
5380
|
+
*
|
|
5381
|
+
* 变了才替换:删旧 chunks 再插新的,事务包住,半截失败整体回滚,库里保持上一版
|
|
5382
|
+
* 完整文档。上一版 status=error 的行不算命中(它没有可分块的正文,重来必须真跑)。
|
|
5383
|
+
*/
|
|
5384
|
+
function replaceKbDoc({ collectionId, relKey, kind, sourcePath = null, title, bytes = 0, sha256 = null, at, chunks = [] }) {
|
|
5385
|
+
return runAtomically(() => {
|
|
5386
|
+
const prev = kbDocByRelKey(collectionId, relKey);
|
|
5387
|
+
if (prev && prev.status === "ok" && sha256 && prev.sha256 === sha256) {
|
|
5388
|
+
return { docId: prev.id, chunks: kbChunksByDoc(prev.id).length, skipped: true };
|
|
5389
|
+
}
|
|
5390
|
+
db.prepare("DELETE FROM kb_chunks WHERE doc_id IN (SELECT id FROM kb_docs WHERE collection_id = ? AND rel_key = ?)")
|
|
5391
|
+
.run(collectionId, String(relKey));
|
|
5392
|
+
let docId;
|
|
5393
|
+
if (prev) {
|
|
5394
|
+
db.prepare(
|
|
5395
|
+
`UPDATE kb_docs SET kind = ?, source_path = ?, title = ?, bytes = ?, sha256 = ?,
|
|
5396
|
+
indexed_at = ?, status = 'ok', error = NULL WHERE id = ?`
|
|
5397
|
+
).run(kind, sourcePath, String(title), bytes, sha256, at, prev.id);
|
|
5398
|
+
docId = prev.id;
|
|
5399
|
+
} else {
|
|
5400
|
+
const info = db.prepare(
|
|
5401
|
+
`INSERT INTO kb_docs (collection_id, rel_key, kind, source_path, title, bytes, sha256, indexed_at, status, error)
|
|
5402
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'ok', NULL)`
|
|
5403
|
+
).run(collectionId, String(relKey), kind, sourcePath, String(title), bytes, sha256, at);
|
|
5404
|
+
docId = Number(info.lastInsertRowid);
|
|
5405
|
+
}
|
|
5406
|
+
const ins = db.prepare(
|
|
5407
|
+
"INSERT INTO kb_chunks (doc_id, ordinal, heading, text, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
5408
|
+
);
|
|
5409
|
+
for (const c of chunks) ins.run(docId, c.ordinal, c.heading ?? "", c.text ?? "", at);
|
|
5410
|
+
return { docId, chunks: chunks.length, skipped: false };
|
|
5411
|
+
});
|
|
5412
|
+
}
|
|
5413
|
+
|
|
5414
|
+
/** 入库失败也要留痕:面板要能显示「这份文档上次为什么没进来」,静默丢来源等于假成功。 */
|
|
5415
|
+
function markKbDocError({ collectionId, relKey, kind, sourcePath = null, title, error, at }) {
|
|
5416
|
+
const prev = kbDocByRelKey(collectionId, relKey);
|
|
5417
|
+
if (prev) {
|
|
5418
|
+
db.prepare(
|
|
5419
|
+
"UPDATE kb_docs SET status = 'error', error = ?, indexed_at = ?, source_path = ?, kind = ?, title = ? WHERE id = ?"
|
|
5420
|
+
).run(String(error ?? ""), at, sourcePath, kind, String(title ?? prev.title), prev.id);
|
|
5421
|
+
return prev.id;
|
|
5422
|
+
}
|
|
5423
|
+
const info = db.prepare(
|
|
5424
|
+
`INSERT INTO kb_docs (collection_id, rel_key, kind, source_path, title, bytes, sha256, indexed_at, status, error)
|
|
5425
|
+
VALUES (?, ?, ?, ?, ?, 0, NULL, ?, 'error', ?)`
|
|
5426
|
+
).run(collectionId, String(relKey), kind, sourcePath, String(title ?? relKey), at, String(error ?? ""));
|
|
5427
|
+
return Number(info.lastInsertRowid);
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
/** 检索候选集:跨集合或单集合,一次取全(BM25 需要全语料算 df,不能分页取)。 */
|
|
5431
|
+
function kbChunksForSearch(collectionId = null) {
|
|
5432
|
+
return db.prepare(
|
|
5433
|
+
`SELECT k.id, k.doc_id, k.ordinal, k.heading, k.text,
|
|
5434
|
+
d.rel_key, d.title, d.kind, c.key AS collection_key
|
|
5435
|
+
FROM kb_chunks k
|
|
5436
|
+
JOIN kb_docs d ON k.doc_id = d.id
|
|
5437
|
+
JOIN kb_collections c ON d.collection_id = c.id
|
|
5438
|
+
WHERE d.status = 'ok' AND (? IS NULL OR d.collection_id = ?)`
|
|
5439
|
+
).all(collectionId, collectionId);
|
|
5440
|
+
}
|
|
5441
|
+
|
|
4710
5442
|
return {
|
|
4711
5443
|
db,
|
|
4712
5444
|
count,
|
|
5445
|
+
countByType,
|
|
4713
5446
|
getById,
|
|
4714
5447
|
// 车道 C:外来行身份的两个读口(幂等键 / 整包清点)。
|
|
4715
5448
|
getByShareExternalId,
|
|
@@ -4749,9 +5482,11 @@ export function createStore(path) {
|
|
|
4749
5482
|
setEmbedding,
|
|
4750
5483
|
getEmbeddings,
|
|
4751
5484
|
getParsedEmbedding,
|
|
5485
|
+
embeddingCacheStats,
|
|
4752
5486
|
embeddedCount,
|
|
4753
5487
|
needsEmbedding,
|
|
4754
5488
|
needsEmbeddingFor,
|
|
5489
|
+
countNeedsEmbeddingFor,
|
|
4755
5490
|
hasStaleEmbedding,
|
|
4756
5491
|
embeddingModelBreakdown,
|
|
4757
5492
|
embeddingDimensionBreakdown,
|
|
@@ -4810,6 +5545,7 @@ export function createStore(path) {
|
|
|
4810
5545
|
archivedEmbeddingStats,
|
|
4811
5546
|
clearArchivedEmbeddings,
|
|
4812
5547
|
storageStats,
|
|
5548
|
+
capacityStats,
|
|
4813
5549
|
vacuum,
|
|
4814
5550
|
saveFailure,
|
|
4815
5551
|
listFailures,
|
|
@@ -4833,6 +5569,15 @@ export function createStore(path) {
|
|
|
4833
5569
|
setScopeState,
|
|
4834
5570
|
getScopeState,
|
|
4835
5571
|
listScopeStates,
|
|
5572
|
+
// #249 第二批:显式 pin(独立表,绕开候选池的通道)。注入侧只读这里,
|
|
5573
|
+
// 不再拿 PINNED_MEMORY_TYPES 当 pin 标记(那个常量在 write-admission 里
|
|
5574
|
+
// 另有写入配额豁免的语义,两件事不再叠在同一个名字上)。
|
|
5575
|
+
listPinned,
|
|
5576
|
+
pinMemory,
|
|
5577
|
+
unpinMemory,
|
|
5578
|
+
countPinned,
|
|
5579
|
+
isPinned,
|
|
5580
|
+
listByIdsForInject,
|
|
4836
5581
|
createEntity,
|
|
4837
5582
|
findEntityByName,
|
|
4838
5583
|
findEntityById,
|
|
@@ -4853,6 +5598,7 @@ export function createStore(path) {
|
|
|
4853
5598
|
listBoostedRelations,
|
|
4854
5599
|
getLinkedMemoryIds,
|
|
4855
5600
|
saveRelation,
|
|
5601
|
+
updateRelationMetadata,
|
|
4856
5602
|
bumpRelationWeight,
|
|
4857
5603
|
migrateAttrsToMemory,
|
|
4858
5604
|
getRelations,
|
|
@@ -4872,6 +5618,17 @@ export function createStore(path) {
|
|
|
4872
5618
|
getContinuityProposal,
|
|
4873
5619
|
listContinuityProposals,
|
|
4874
5620
|
countContinuityProposals,
|
|
5621
|
+
// knowledge base(知识库第二层,§3.3):三张 kb_* 表,与记忆行物理隔离。
|
|
5622
|
+
kbCollectionByKey,
|
|
5623
|
+
listKbCollections,
|
|
5624
|
+
upsertKbCollection,
|
|
5625
|
+
deleteKbCollection,
|
|
5626
|
+
listKbDocs,
|
|
5627
|
+
kbDocByRelKey,
|
|
5628
|
+
kbChunksByDoc,
|
|
5629
|
+
replaceKbDoc,
|
|
5630
|
+
markKbDocError,
|
|
5631
|
+
kbChunksForSearch,
|
|
4875
5632
|
// chorography(仓库地志):M0 只写不读,消费面 M1 接(见上面注释块)。
|
|
4876
5633
|
choroRepoByRoot,
|
|
4877
5634
|
upsertChoroRepo,
|
|
@@ -4880,6 +5637,24 @@ export function createStore(path) {
|
|
|
4880
5637
|
recordChoroPageFailure,
|
|
4881
5638
|
markChoroPageEnriched,
|
|
4882
5639
|
saveChoroPageClaims,
|
|
5640
|
+
getChoroPageVectors,
|
|
5641
|
+
saveChoroPageVector,
|
|
5642
|
+
// 「Agent 眼中的你」自动事实(2026-11-06):读口与写口都只给 domain 层
|
|
5643
|
+
// (src/profile-facts.js)和 API 层用,记忆主表路径不经此。
|
|
5644
|
+
listProfileFacts,
|
|
5645
|
+
listActiveFactsGrouped,
|
|
5646
|
+
getProfileFact,
|
|
5647
|
+
insertProfileFact,
|
|
5648
|
+
updateProfileFact,
|
|
5649
|
+
insertProfileFactJournal,
|
|
5650
|
+
listProfileFactJournal,
|
|
5651
|
+
pruneProfileFactJournal,
|
|
5652
|
+
insertProfileFactRun,
|
|
5653
|
+
listProfileFactRuns,
|
|
5654
|
+
profileFactsCursor,
|
|
5655
|
+
listFactInputRows,
|
|
5656
|
+
countFactInputRows,
|
|
5657
|
+
listFactCardKeys,
|
|
4883
5658
|
close() {
|
|
4884
5659
|
db.close();
|
|
4885
5660
|
}
|