@modusensus/dsh-mneme 0.8.0 → 0.8.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/README.md +15 -5
- package/lib/api-standalone.js +12 -0
- package/lib/api.js +52 -14
- package/lib/client.js +130 -8
- package/lib/config.js +8 -0
- package/lib/dream/decisions.js +55 -8
- package/lib/dream/sleep.js +79 -17
- package/lib/dream.js +61 -9
- package/lib/entities/extractor.js +2 -2
- package/lib/index.js +6 -0
- package/lib/lang.js +13 -2
- package/lib/local-embedder.js +9 -1
- package/lib/reranker.js +38 -8
- package/lib/scope.js +39 -4
- package/lib/service.js +121 -18
- package/lib/store.js +243 -40
- package/lib/summarize.js +2 -2
- package/lib/tools.js +64 -30
- package/lib/vector-index.js +2 -8
- package/lib/version-check.js +122 -0
- package/package.json +2 -1
- package/scripts/sync-test-badge.mjs +61 -0
- package/src/api-standalone.js +12 -0
- package/src/api.js +52 -14
- package/src/config.js +8 -0
- package/src/dream/decisions.js +55 -8
- package/src/dream/sleep.js +79 -17
- package/src/dream.js +61 -9
- package/src/entities/extractor.js +2 -2
- package/src/index.js +6 -0
- package/src/lang.js +13 -2
- package/src/local-embedder.js +9 -1
- package/src/reranker.js +38 -8
- package/src/scope.js +39 -4
- package/src/service.js +121 -18
- package/src/store.js +243 -40
- package/src/summarize.js +2 -2
- package/src/tools.js +64 -30
- package/src/vector-index.js +2 -8
- package/src/version-check.js +122 -0
- package/test/api.test.js +5 -0
- package/test/conflict-freeze.test.js +15 -5
- package/test/dream-archive-guardrail.test.js +116 -0
- package/test/dream-coverage-degrade.test.js +86 -0
- package/test/dream.test.js +15 -11
- package/test/inject-first-round-bm25.test.js +89 -0
- package/test/llm-message-source.test.js +177 -0
- package/test/reranker-single-logit.test.js +73 -0
- package/test/scope-candidates.test.js +239 -0
- package/test/scope-explicit.test.js +452 -0
- package/test/scope-storage.test.js +13 -5
- package/test/scope-strict.test.js +107 -41
- package/test/skip-invalid-restore.test.js +7 -5
- package/test/sleep-scheduler-rearm.test.js +89 -0
- package/test/sleep.test.js +2 -2
- package/test/standalone-api.test.js +34 -1
- package/test/store-vector-cache.test.js +98 -0
- package/test/tools-memory-get-schema.test.js +65 -0
- package/test/version-check.test.js +152 -0
package/lib/store.js
CHANGED
|
@@ -22,6 +22,9 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
22
22
|
);
|
|
23
23
|
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
24
24
|
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
25
|
+
-- Issue #202:all()/list() 按 updated_at 排序,无索引时走临时 B 树全表排序
|
|
26
|
+
-- (5k 行实测 all() 231ms → 135ms)。IF NOT EXISTS 幂等,存量库启动即建。
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at);
|
|
25
28
|
|
|
26
29
|
-- autoDream audit trail: one row per consolidation run, capturing the exact
|
|
27
30
|
-- input snapshot digest + the LLM decision list + per-id outcome + a compact
|
|
@@ -148,6 +151,25 @@ CREATE TABLE IF NOT EXISTS conflict_pending (
|
|
|
148
151
|
);
|
|
149
152
|
CREATE INDEX IF NOT EXISTS idx_conflict_pending_unresolved ON conflict_pending(resolved_at);
|
|
150
153
|
|
|
154
|
+
-- scope_changes: v0.8.1 底座(issue #170)scope 归属的人工修正审计。显式声明
|
|
155
|
+
-- (memory_save/memory_update 的 scope 参数、面板编辑)每次改变某条记忆的
|
|
156
|
+
-- agent/workspace 归属都落一行:改动前后值 + 来源 + actor(tool=模型侧 /
|
|
157
|
+
-- panel=人工侧)。与 conflict_pending 同款的 bookkeeping 表:只做审计,
|
|
158
|
+
-- 不触发 write hooks。放宽可见性(label→global)的候选筛选与回放都靠它。
|
|
159
|
+
CREATE TABLE IF NOT EXISTS scope_changes (
|
|
160
|
+
id TEXT PRIMARY KEY,
|
|
161
|
+
memory_id TEXT NOT NULL,
|
|
162
|
+
actor TEXT NOT NULL,
|
|
163
|
+
prev_agent_scope TEXT,
|
|
164
|
+
prev_workspace_scope TEXT,
|
|
165
|
+
next_agent_scope TEXT,
|
|
166
|
+
next_workspace_scope TEXT,
|
|
167
|
+
agent_scope_source TEXT,
|
|
168
|
+
workspace_scope_source TEXT,
|
|
169
|
+
decided_at TEXT NOT NULL
|
|
170
|
+
);
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_scope_changes_memory ON scope_changes(memory_id);
|
|
172
|
+
|
|
151
173
|
-- llm_audit_logs: every background LLM call (autoDream consolidation + summary,
|
|
152
174
|
-- autoSummarize compression) is recorded here — tokens in/out, duration, status
|
|
153
175
|
-- and the trigger that caused it (Bug8). Failures are captured as status='error'
|
|
@@ -344,6 +366,13 @@ function normalizeScopeText(raw) {
|
|
|
344
366
|
return s ? s : null;
|
|
345
367
|
}
|
|
346
368
|
|
|
369
|
+
// scope 来源列(issue #170 底座):'auto'=载体自动标注,'explicit'=显式声明。
|
|
370
|
+
// 只认这两个枚举值,其余(含 NULL)落 NULL——NULL 语义是「未标注,或 0.8.1
|
|
371
|
+
// 之前落库的存量标注(视为 auto 的软效力,第 3 步语义收窄时按此口径读)」。
|
|
372
|
+
function normalizeScopeSource(raw) {
|
|
373
|
+
return raw === "auto" || raw === "explicit" ? raw : null;
|
|
374
|
+
}
|
|
375
|
+
|
|
347
376
|
// occurred_at:事件发生时间(区别于 created_at 的入库时间)。可解析的时间戳
|
|
348
377
|
// 统一归一到 UTC ISO(A2 的时间过滤要按字典序直接比较);解析失败落 NULL,
|
|
349
378
|
// 同样不阻塞写入。
|
|
@@ -371,6 +400,9 @@ function toRow(row) {
|
|
|
371
400
|
epistemic_status: row.epistemic_status ?? "subjective",
|
|
372
401
|
agent_scope: row.agent_scope ?? undefined,
|
|
373
402
|
workspace_scope: row.workspace_scope ?? undefined,
|
|
403
|
+
agent_scope_source: row.agent_scope_source ?? undefined,
|
|
404
|
+
workspace_scope_source: row.workspace_scope_source ?? undefined,
|
|
405
|
+
scope_decided_at: row.scope_decided_at ?? undefined,
|
|
374
406
|
sensitivity: row.sensitivity ?? undefined,
|
|
375
407
|
occurred_at: row.occurred_at ?? undefined,
|
|
376
408
|
created_at: row.created_at,
|
|
@@ -438,6 +470,22 @@ function toConflictPending(row) {
|
|
|
438
470
|
};
|
|
439
471
|
}
|
|
440
472
|
|
|
473
|
+
function toScopeChange(row) {
|
|
474
|
+
if (!row) return undefined;
|
|
475
|
+
return {
|
|
476
|
+
id: row.id,
|
|
477
|
+
memory_id: row.memory_id,
|
|
478
|
+
actor: row.actor,
|
|
479
|
+
prev_agent_scope: row.prev_agent_scope ?? undefined,
|
|
480
|
+
prev_workspace_scope: row.prev_workspace_scope ?? undefined,
|
|
481
|
+
next_agent_scope: row.next_agent_scope ?? undefined,
|
|
482
|
+
next_workspace_scope: row.next_workspace_scope ?? undefined,
|
|
483
|
+
agent_scope_source: row.agent_scope_source ?? undefined,
|
|
484
|
+
workspace_scope_source: row.workspace_scope_source ?? undefined,
|
|
485
|
+
decided_at: row.decided_at
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
441
489
|
function toRecallRun(row) {
|
|
442
490
|
if (!row) return undefined;
|
|
443
491
|
return {
|
|
@@ -631,6 +679,16 @@ export function createStore(path) {
|
|
|
631
679
|
addColumn("memories", "sensitivity", "ALTER TABLE memories ADD COLUMN sensitivity TEXT");
|
|
632
680
|
addColumn("memories", "occurred_at", "ALTER TABLE memories ADD COLUMN occurred_at TEXT");
|
|
633
681
|
|
|
682
|
+
// v0.8.1 底座(issue #170):scope 标注的来源与决策时间。三列全部可空不带
|
|
683
|
+
// DEFAULT(与 A1 同款零重写迁移)。来源拆成 agent/workspace 各一列而非单列
|
|
684
|
+
// scope_source:混合来源(agent 维显式 + workspace 维自动)必须可表达——否则
|
|
685
|
+
// 「提升回全局」之后遗留的自动 workspace 标签会在效力收窄时被一并当成显式,
|
|
686
|
+
// 恰好复活 issue #170 抱怨的「自动标签吃硬过滤」问题。存量标注行三列皆 NULL,
|
|
687
|
+
// 读侧按 auto(软效力)口径解释。
|
|
688
|
+
addColumn("memories", "agent_scope_source", "ALTER TABLE memories ADD COLUMN agent_scope_source TEXT");
|
|
689
|
+
addColumn("memories", "workspace_scope_source", "ALTER TABLE memories ADD COLUMN workspace_scope_source TEXT");
|
|
690
|
+
addColumn("memories", "scope_decided_at", "ALTER TABLE memories ADD COLUMN scope_decided_at TEXT");
|
|
691
|
+
|
|
634
692
|
// Legacy dream_runs without policy_epoch → backfill with the default epoch.
|
|
635
693
|
addColumn("dream_runs", "policy_epoch", "ALTER TABLE dream_runs ADD COLUMN policy_epoch INTEGER NOT NULL DEFAULT 0");
|
|
636
694
|
addColumn("dream_runs", "run_type", "ALTER TABLE dream_runs ADD COLUMN run_type TEXT NOT NULL DEFAULT 'auto'");
|
|
@@ -662,11 +720,51 @@ export function createStore(path) {
|
|
|
662
720
|
}
|
|
663
721
|
}
|
|
664
722
|
|
|
723
|
+
// Issue #202:embedding 解析缓存。检索路径此前每次调用都对全部带向量行
|
|
724
|
+
// JSON.parse(993 × 512 维 ≈ 48 ms/次,且 SQL 拖着 10 MB 的 TEXT),余弦本身
|
|
725
|
+
// 只占 3%。向量只经 setEmbedding 写入(单一失效点),把解析结果按 id 缓存,
|
|
726
|
+
// 命中即零解析;上限 FIFO 驱逐防大库内存无界(4000 × 512 维 double ≈ 16 MB)。
|
|
727
|
+
const embeddingCache = new Map();
|
|
728
|
+
const EMBEDDING_CACHE_MAX = 4000;
|
|
729
|
+
|
|
730
|
+
function cacheEmbedding(id, vector) {
|
|
731
|
+
if (embeddingCache.size >= EMBEDDING_CACHE_MAX && !embeddingCache.has(id)) {
|
|
732
|
+
const oldest = embeddingCache.keys().next().value;
|
|
733
|
+
embeddingCache.delete(oldest);
|
|
734
|
+
}
|
|
735
|
+
embeddingCache.set(id, vector);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Parsed embedding for one id (cache-first); undefined when none/parse
|
|
739
|
+
* failure. Single read path shared by searchVector / getEmbedding(s). */
|
|
740
|
+
function getParsedEmbedding(id) {
|
|
741
|
+
const cached = embeddingCache.get(id);
|
|
742
|
+
if (cached) return cached;
|
|
743
|
+
const row = db.prepare("SELECT embedding FROM memories WHERE id = ?").get(id);
|
|
744
|
+
if (!row?.embedding) return undefined;
|
|
745
|
+
try {
|
|
746
|
+
const v = JSON.parse(row.embedding);
|
|
747
|
+
if (Array.isArray(v) && v.length) {
|
|
748
|
+
cacheEmbedding(id, v);
|
|
749
|
+
return v;
|
|
750
|
+
}
|
|
751
|
+
} catch { /* corrupt embedding text: treated as absent */ }
|
|
752
|
+
return undefined;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// all()/list() 的显式列清单:从实际 schema 派生(迁移后),只排除 embedding
|
|
756
|
+
// 列——consolidation/sleep/inject 的消费方都不读向量,而它是全表里最重的一列
|
|
757
|
+
// (5k 行库 ≈ 9.4 MB TEXT),每次全表调用都白搬。经 PRAGMA 派生而非硬编码,
|
|
758
|
+
// schema 演进时自动跟随。
|
|
759
|
+
const memoryColumns = db.prepare("PRAGMA table_info(memories)").all()
|
|
760
|
+
.map((c) => c.name)
|
|
761
|
+
.filter((n) => n !== "embedding");
|
|
762
|
+
const memoryColumnList = memoryColumns.join(", ");
|
|
763
|
+
|
|
665
764
|
// Per-instance monotonic timestamp guard: consecutive writes within the same
|
|
666
765
|
// millisecond must still produce strictly increasing timestamps (test asserts
|
|
667
766
|
// updated_at != created_at). State lives in the store closure, not module scope.
|
|
668
|
-
let lastTs = "";
|
|
669
|
-
function nowIso() {
|
|
767
|
+
let lastTs = ""; function nowIso() {
|
|
670
768
|
let ts = new Date().toISOString();
|
|
671
769
|
if (lastTs && ts <= lastTs) {
|
|
672
770
|
const d = new Date(lastTs);
|
|
@@ -718,17 +816,20 @@ export function createStore(path) {
|
|
|
718
816
|
// occurred_at 闭区间:与 list() 同过滤(visibility 同口径),memory_list
|
|
719
817
|
// 的 total 才能和过滤后的行保持一致。
|
|
720
818
|
if (visibility) {
|
|
819
|
+
// v0.8.1 第 3 步(issue #170 4.3):硬过滤只认显式声明——与
|
|
820
|
+
// service.isVisibleInScope 同口径。IS NOT 是 NULL 安全比较:来源为
|
|
821
|
+
// auto/NULL(v0.8.0 存量自动标注)的行不吃硬墙,只走 A2 软加权。
|
|
721
822
|
if (visibility.agentScope != null) {
|
|
722
|
-
clauses.push("(agent_scope IS NULL OR agent_scope = ?)");
|
|
823
|
+
clauses.push("(agent_scope IS NULL OR agent_scope_source IS NOT 'explicit' OR agent_scope = ?)");
|
|
723
824
|
params.push(visibility.agentScope);
|
|
724
825
|
} else {
|
|
725
|
-
clauses.push("agent_scope IS NULL");
|
|
826
|
+
clauses.push("(agent_scope IS NULL OR agent_scope_source IS NOT 'explicit')");
|
|
726
827
|
}
|
|
727
828
|
if (visibility.workspaceScope != null) {
|
|
728
|
-
clauses.push("(workspace_scope IS NULL OR workspace_scope = ?)");
|
|
829
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope_source IS NOT 'explicit' OR workspace_scope = ?)");
|
|
729
830
|
params.push(visibility.workspaceScope);
|
|
730
831
|
} else {
|
|
731
|
-
clauses.push("workspace_scope IS NULL");
|
|
832
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope_source IS NOT 'explicit')");
|
|
732
833
|
}
|
|
733
834
|
}
|
|
734
835
|
if (!includeForgotten) {
|
|
@@ -775,8 +876,8 @@ export function createStore(path) {
|
|
|
775
876
|
: inferEpistemicStatus(memory);
|
|
776
877
|
runAtomically(() => {
|
|
777
878
|
db.prepare(
|
|
778
|
-
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, source, content_history, quality_score, embedding, epistemic_status, agent_scope, workspace_scope, sensitivity, occurred_at, created_at, updated_at)
|
|
779
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
879
|
+
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, source, content_history, quality_score, embedding, epistemic_status, agent_scope, workspace_scope, agent_scope_source, workspace_scope_source, scope_decided_at, sensitivity, occurred_at, created_at, updated_at)
|
|
880
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
780
881
|
).run(
|
|
781
882
|
id,
|
|
782
883
|
type,
|
|
@@ -792,6 +893,9 @@ export function createStore(path) {
|
|
|
792
893
|
epistemicStatus,
|
|
793
894
|
normalizeScopeText(memory.agent_scope),
|
|
794
895
|
normalizeScopeText(memory.workspace_scope),
|
|
896
|
+
normalizeScopeSource(memory.agent_scope_source),
|
|
897
|
+
normalizeScopeSource(memory.workspace_scope_source),
|
|
898
|
+
normalizeOccurredAt(memory.scope_decided_at),
|
|
795
899
|
normalizeScopeText(memory.sensitivity),
|
|
796
900
|
normalizeOccurredAt(memory.occurred_at),
|
|
797
901
|
now,
|
|
@@ -825,9 +929,26 @@ export function createStore(path) {
|
|
|
825
929
|
const qualityScore = patch.quality_score !== undefined && Number.isFinite(patch.quality_score)
|
|
826
930
|
? patch.quality_score
|
|
827
931
|
: (existing.quality_score ?? null);
|
|
932
|
+
// v0.8.1 底座(issue #170):scope 列只在本 patch 显式携带该键时才改写
|
|
933
|
+
// (undefined=不动;null=清空为未标注/global)。来源列只认 auto|explicit。
|
|
934
|
+
const nextAgentScope = patch.agent_scope !== undefined
|
|
935
|
+
? normalizeScopeText(patch.agent_scope)
|
|
936
|
+
: (existing.agent_scope ?? null);
|
|
937
|
+
const nextWorkspaceScope = patch.workspace_scope !== undefined
|
|
938
|
+
? normalizeScopeText(patch.workspace_scope)
|
|
939
|
+
: (existing.workspace_scope ?? null);
|
|
940
|
+
const nextAgentSource = patch.agent_scope_source !== undefined
|
|
941
|
+
? normalizeScopeSource(patch.agent_scope_source)
|
|
942
|
+
: (existing.agent_scope_source ?? null);
|
|
943
|
+
const nextWorkspaceSource = patch.workspace_scope_source !== undefined
|
|
944
|
+
? normalizeScopeSource(patch.workspace_scope_source)
|
|
945
|
+
: (existing.workspace_scope_source ?? null);
|
|
946
|
+
const nextDecidedAt = patch.scope_decided_at !== undefined
|
|
947
|
+
? normalizeOccurredAt(patch.scope_decided_at)
|
|
948
|
+
: (existing.scope_decided_at ?? null);
|
|
828
949
|
runAtomically(() => {
|
|
829
950
|
db.prepare(
|
|
830
|
-
`UPDATE memories SET type=?, title=?, content=?, tags=?, importance=?, source=?, content_history=?, quality_score=?, embedding=?, epistemic_status=?, updated_at=? WHERE id=?`
|
|
951
|
+
`UPDATE memories SET type=?, title=?, content=?, tags=?, importance=?, source=?, content_history=?, quality_score=?, embedding=?, epistemic_status=?, agent_scope=?, workspace_scope=?, agent_scope_source=?, workspace_scope_source=?, scope_decided_at=?, updated_at=? WHERE id=?`
|
|
831
952
|
).run(
|
|
832
953
|
type,
|
|
833
954
|
patch.title ?? existing.title,
|
|
@@ -839,9 +960,18 @@ export function createStore(path) {
|
|
|
839
960
|
qualityScore,
|
|
840
961
|
embedding,
|
|
841
962
|
epistemicStatus,
|
|
963
|
+
nextAgentScope,
|
|
964
|
+
nextWorkspaceScope,
|
|
965
|
+
nextAgentSource,
|
|
966
|
+
nextWorkspaceSource,
|
|
967
|
+
nextDecidedAt,
|
|
842
968
|
now,
|
|
843
969
|
id
|
|
844
970
|
);
|
|
971
|
+
// Issue #202:update 可直写 embedding(patch.embedding),缓存条目失效,
|
|
972
|
+
// 下次读取按库值重解析。宁滥勿缺——即便本 patch 未含 embedding,丢一条
|
|
973
|
+
// 缓存只多一次点查重解析。
|
|
974
|
+
embeddingCache.delete(id);
|
|
845
975
|
// Desired generation bumped in the same transaction as the update (peer
|
|
846
976
|
// blocker 1: crash between write and sync must still be recoverable).
|
|
847
977
|
incrementGeneration();
|
|
@@ -912,6 +1042,8 @@ export function createStore(path) {
|
|
|
912
1042
|
expectedUpdatedAt
|
|
913
1043
|
);
|
|
914
1044
|
if (result.changes === 0) return; // CAS miss: a concurrent write won
|
|
1045
|
+
// Issue #202:同 update——CAS 命中即可能改写 embedding,失效缓存条目。
|
|
1046
|
+
embeddingCache.delete(id);
|
|
915
1047
|
// Only bump desired generation on a successful CAS — a miss writes nothing.
|
|
916
1048
|
incrementGeneration();
|
|
917
1049
|
applied = true;
|
|
@@ -1042,21 +1174,25 @@ export function createStore(path) {
|
|
|
1042
1174
|
params.push(occurred.to);
|
|
1043
1175
|
}
|
|
1044
1176
|
// v0.8.0 A3(issue #17):strictScope 硬过滤(memory_list 分页路径)。
|
|
1045
|
-
// SQL 与 service.isVisibleInScope 同口径:未标注(NULL)
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1177
|
+
// SQL 与 service.isVisibleInScope 同口径:未标注(NULL)恒可见;v0.8.1 起
|
|
1178
|
+
// 硬墙只认 explicit 来源(4.3),当前维度解析不到时该维度只放行未标注/
|
|
1179
|
+
// 非显式行(fail-closed 收窄)。只有显式传 visibility 的用户面调用才
|
|
1180
|
+
// 过滤——dream/质量过滤等内部 store.list 调用不受影响。
|
|
1048
1181
|
if (visibility) {
|
|
1182
|
+
// v0.8.1 第 3 步(issue #170 4.3):硬过滤只认显式声明——与
|
|
1183
|
+
// service.isVisibleInScope 同口径。IS NOT 是 NULL 安全比较:来源为
|
|
1184
|
+
// auto/NULL(v0.8.0 存量自动标注)的行不吃硬墙,只走 A2 软加权。
|
|
1049
1185
|
if (visibility.agentScope != null) {
|
|
1050
|
-
clauses.push("(agent_scope IS NULL OR agent_scope = ?)");
|
|
1186
|
+
clauses.push("(agent_scope IS NULL OR agent_scope_source IS NOT 'explicit' OR agent_scope = ?)");
|
|
1051
1187
|
params.push(visibility.agentScope);
|
|
1052
1188
|
} else {
|
|
1053
|
-
clauses.push("agent_scope IS NULL");
|
|
1189
|
+
clauses.push("(agent_scope IS NULL OR agent_scope_source IS NOT 'explicit')");
|
|
1054
1190
|
}
|
|
1055
1191
|
if (visibility.workspaceScope != null) {
|
|
1056
|
-
clauses.push("(workspace_scope IS NULL OR workspace_scope = ?)");
|
|
1192
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope_source IS NOT 'explicit' OR workspace_scope = ?)");
|
|
1057
1193
|
params.push(visibility.workspaceScope);
|
|
1058
1194
|
} else {
|
|
1059
|
-
clauses.push("workspace_scope IS NULL");
|
|
1195
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope_source IS NOT 'explicit')");
|
|
1060
1196
|
}
|
|
1061
1197
|
}
|
|
1062
1198
|
if (!includeForgotten) {
|
|
@@ -1090,7 +1226,9 @@ export function createStore(path) {
|
|
|
1090
1226
|
}
|
|
1091
1227
|
|
|
1092
1228
|
function all() {
|
|
1093
|
-
|
|
1229
|
+
// Issue #202:显式列清单排除 embedding——toRow 本就不输出向量,SELECT *
|
|
1230
|
+
// 纯属把全表最重的列(9.4 MB TEXT/5k 行)拖进内存再丢弃。
|
|
1231
|
+
const rows = db.prepare(`SELECT ${memoryColumnList} FROM memories ORDER BY updated_at DESC`).all();
|
|
1094
1232
|
return rows.map(toRow);
|
|
1095
1233
|
}
|
|
1096
1234
|
|
|
@@ -1098,6 +1236,17 @@ export function createStore(path) {
|
|
|
1098
1236
|
function setEmbedding(id, vector) {
|
|
1099
1237
|
const json = Array.isArray(vector) && vector.length ? JSON.stringify(vector) : null;
|
|
1100
1238
|
db.prepare("UPDATE memories SET embedding = ? WHERE id = ?").run(json, id);
|
|
1239
|
+
// Issue #202:单一写入口即单一失效点——重写缓存条目(向量在手上不必重解析),
|
|
1240
|
+
// 清空则移除,删除过的 id 不会被下一次 searchVector 的 id 集合选中。
|
|
1241
|
+
if (json) {
|
|
1242
|
+
try {
|
|
1243
|
+
const parsed = JSON.parse(json);
|
|
1244
|
+
if (Array.isArray(parsed) && parsed.length) cacheEmbedding(id, parsed);
|
|
1245
|
+
else embeddingCache.delete(id);
|
|
1246
|
+
} catch { embeddingCache.delete(id); }
|
|
1247
|
+
} else {
|
|
1248
|
+
embeddingCache.delete(id);
|
|
1249
|
+
}
|
|
1101
1250
|
}
|
|
1102
1251
|
|
|
1103
1252
|
/** Batch fetch stored embeddings by id (v0.5.0 search-time semantic dedup).
|
|
@@ -1106,19 +1255,10 @@ export function createStore(path) {
|
|
|
1106
1255
|
function getEmbeddings(ids) {
|
|
1107
1256
|
const out = new Map();
|
|
1108
1257
|
const list = (Array.isArray(ids) ? ids : []).filter(Boolean);
|
|
1109
|
-
for (
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
WHERE embedding IS NOT NULL AND embedding != ''
|
|
1114
|
-
AND id IN (${chunk.map(() => "?").join(",")})`
|
|
1115
|
-
).all(...chunk);
|
|
1116
|
-
for (const row of rows) {
|
|
1117
|
-
try {
|
|
1118
|
-
const vec = JSON.parse(row.embedding);
|
|
1119
|
-
if (Array.isArray(vec) && vec.length) out.set(row.id, vec);
|
|
1120
|
-
} catch { /* corrupt row: skip */ }
|
|
1121
|
-
}
|
|
1258
|
+
for (const id of list) {
|
|
1259
|
+
// Issue #202:走同一解析缓存(语义去重的批量读也免重复 parse)。
|
|
1260
|
+
const vec = getParsedEmbedding(id);
|
|
1261
|
+
if (vec) out.set(id, vec);
|
|
1122
1262
|
}
|
|
1123
1263
|
return out;
|
|
1124
1264
|
}
|
|
@@ -1193,24 +1333,30 @@ export function createStore(path) {
|
|
|
1193
1333
|
function searchVector(vector, { limit = 20, includeArchived = false, threshold = 0 } = {}) {
|
|
1194
1334
|
if (!Array.isArray(vector) || !vector.length) return [];
|
|
1195
1335
|
const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
|
|
1336
|
+
// Issue #202:只取 id 列(~40 KB)而非 SELECT *(~11 MB)——向量从缓存取,
|
|
1337
|
+
// 缓存未命中时按 id 点查 embedding 再解析回填。SQL 里保留 embedding 的
|
|
1338
|
+
// IS NOT NULL 过滤,保证「有向量的行」集合始终以库为准(删除/清空即时生效)。
|
|
1196
1339
|
const rows = db.prepare(
|
|
1197
|
-
`SELECT
|
|
1340
|
+
`SELECT id FROM memories
|
|
1198
1341
|
WHERE ${archivedFilter}forgotten = 0 AND embedding IS NOT NULL AND embedding != ''`
|
|
1199
1342
|
).all();
|
|
1200
1343
|
const scored = [];
|
|
1201
1344
|
for (const row of rows) {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
v = JSON.parse(row.embedding);
|
|
1205
|
-
} catch {
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1345
|
+
const v = getParsedEmbedding(row.id);
|
|
1346
|
+
if (!v) continue;
|
|
1208
1347
|
const score = cosine(vector, v);
|
|
1209
|
-
if (score >= threshold) scored.push({ row, score });
|
|
1348
|
+
if (score >= threshold) scored.push({ row: { id: row.id }, score });
|
|
1210
1349
|
}
|
|
1211
1350
|
scored.sort((a, b) => b.score - a.score);
|
|
1212
1351
|
const { limit: lim } = sanitizePage(limit, 0, 20);
|
|
1213
|
-
|
|
1352
|
+
// Top-N 回表:只对入选行取完整字段(主键点查 ≤ lim 行),评分别拖着正文走。
|
|
1353
|
+
const getFull = db.prepare("SELECT * FROM memories WHERE id = ?");
|
|
1354
|
+
const out = [];
|
|
1355
|
+
for (const { row, score } of scored.slice(0, lim)) {
|
|
1356
|
+
const full = getFull.get(row.id);
|
|
1357
|
+
if (full) out.push({ ...toRow(full), score });
|
|
1358
|
+
}
|
|
1359
|
+
return out;
|
|
1214
1360
|
}
|
|
1215
1361
|
|
|
1216
1362
|
// --- autoDream audit trail ----------------------------------------------
|
|
@@ -1615,7 +1761,14 @@ export function createStore(path) {
|
|
|
1615
1761
|
* order is normalized (sorted by id) so the same two memories are only ever
|
|
1616
1762
|
* pending once — a re-detection in a later dream run is a no-op, never a
|
|
1617
1763
|
* duplicate queue entry. Returns the pending row (freshly inserted, or the
|
|
1618
|
-
* existing unresolved row when the pair is already pending)
|
|
1764
|
+
* existing unresolved row when the pair is already pending), or undefined
|
|
1765
|
+
* when the re-detection is suppressed.
|
|
1766
|
+
*
|
|
1767
|
+
* v0.8.1(issue #170 复核项 4):人工已裁决过的对(resolved_at 非空)不再
|
|
1768
|
+
* 反复入队——除非任一侧在裁决之后又被改过(updated_at 晚于 resolved_at)。
|
|
1769
|
+
* 否则跨 scope 对每轮 sleep 都原样回来,把「保留双方」的决定变成噪声。
|
|
1770
|
+
* updated_at 是宽松代理(任何字段更新都会触发重新评审)——宁可多看一眼,
|
|
1771
|
+
* 不静音真冲突。
|
|
1619
1772
|
*/
|
|
1620
1773
|
function saveConflictPending({ run_id, memory_a, memory_b, reason }) {
|
|
1621
1774
|
const [a, b] = [memory_a, memory_b].sort();
|
|
@@ -1623,6 +1776,15 @@ export function createStore(path) {
|
|
|
1623
1776
|
"SELECT * FROM conflict_pending WHERE memory_a = ? AND memory_b = ? AND resolved_at IS NULL LIMIT 1"
|
|
1624
1777
|
).get(a, b);
|
|
1625
1778
|
if (existing) return toConflictPending(existing);
|
|
1779
|
+
const reviewed = db.prepare(
|
|
1780
|
+
"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"
|
|
1781
|
+
).get(a, b);
|
|
1782
|
+
if (reviewed) {
|
|
1783
|
+
const touched = db.prepare(
|
|
1784
|
+
"SELECT count(*) AS c FROM memories WHERE id IN (?, ?) AND updated_at > ?"
|
|
1785
|
+
).get(a, b, reviewed.resolved_at).c;
|
|
1786
|
+
if (touched === 0) return undefined;
|
|
1787
|
+
}
|
|
1626
1788
|
const id = randomUUID();
|
|
1627
1789
|
const now = nowIso();
|
|
1628
1790
|
db.prepare(
|
|
@@ -1668,6 +1830,44 @@ export function createStore(path) {
|
|
|
1668
1830
|
).get().c;
|
|
1669
1831
|
}
|
|
1670
1832
|
|
|
1833
|
+
/**
|
|
1834
|
+
* v0.8.1 底座(issue #170):scope 归属人工修正的审计行。record 描述「一次
|
|
1835
|
+
* 决策后的完整状态」(prev_* 为改前、next_* 为改后,NULL=global/未标注),
|
|
1836
|
+
* 由调用方(service.updateMemory)在 store.update 成功后写入;本函数不做
|
|
1837
|
+
* 业务校验,只保证审计落库失败不反噬主写入(异常上抛由调用方吞掉记 warn)。
|
|
1838
|
+
*/
|
|
1839
|
+
function saveScopeChange({ memory_id, actor, prev_agent_scope, prev_workspace_scope, next_agent_scope, next_workspace_scope, agent_scope_source, workspace_scope_source, decided_at }) {
|
|
1840
|
+
const id = randomUUID();
|
|
1841
|
+
db.prepare(
|
|
1842
|
+
`INSERT INTO scope_changes (id, memory_id, actor, prev_agent_scope, prev_workspace_scope, next_agent_scope, next_workspace_scope, agent_scope_source, workspace_scope_source, decided_at)
|
|
1843
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1844
|
+
).run(
|
|
1845
|
+
id,
|
|
1846
|
+
memory_id,
|
|
1847
|
+
actor === "panel" ? "panel" : "tool",
|
|
1848
|
+
normalizeScopeText(prev_agent_scope),
|
|
1849
|
+
normalizeScopeText(prev_workspace_scope),
|
|
1850
|
+
normalizeScopeText(next_agent_scope),
|
|
1851
|
+
normalizeScopeText(next_workspace_scope),
|
|
1852
|
+
normalizeScopeSource(agent_scope_source),
|
|
1853
|
+
normalizeScopeSource(workspace_scope_source),
|
|
1854
|
+
normalizeOccurredAt(decided_at) ?? nowIso()
|
|
1855
|
+
);
|
|
1856
|
+
return toScopeChange(db.prepare("SELECT * FROM scope_changes WHERE id = ?").get(id));
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/** Scope 修正审计(单条记忆,新→旧)。审计回放与候选复核用。 */
|
|
1860
|
+
function listScopeChanges(memoryId, { limit = 50 } = {}) {
|
|
1861
|
+
const lim = Number.isInteger(limit) && limit > 0 ? Math.min(limit, 200) : 50;
|
|
1862
|
+
// tiebreaker 用 rowid(追加写入的单调序)而非 id:decided_at 只有毫秒精度,
|
|
1863
|
+
// 连续两次修正落在同一毫秒时 UUID 排序是随机的(CI windows/node24 抓到的
|
|
1864
|
+
// 真实 flake)——审计回放要求「最新一条」确定。
|
|
1865
|
+
const rows = db.prepare(
|
|
1866
|
+
"SELECT * FROM scope_changes WHERE memory_id = ? ORDER BY decided_at DESC, rowid DESC LIMIT ?"
|
|
1867
|
+
).all(memoryId, lim);
|
|
1868
|
+
return rows.map(toScopeChange);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1671
1871
|
function getFailureStats({ since } = {}) {
|
|
1672
1872
|
const clause = since ? "WHERE created_at >= ?" : "";
|
|
1673
1873
|
const params = since ? [since] : [];
|
|
@@ -2104,6 +2304,7 @@ export function createStore(path) {
|
|
|
2104
2304
|
search,
|
|
2105
2305
|
setEmbedding,
|
|
2106
2306
|
getEmbeddings,
|
|
2307
|
+
getParsedEmbedding,
|
|
2107
2308
|
embeddedCount,
|
|
2108
2309
|
needsEmbedding,
|
|
2109
2310
|
searchVector,
|
|
@@ -2133,6 +2334,8 @@ export function createStore(path) {
|
|
|
2133
2334
|
listConflictPending,
|
|
2134
2335
|
resolveConflictPending,
|
|
2135
2336
|
countConflictPending,
|
|
2337
|
+
saveScopeChange,
|
|
2338
|
+
listScopeChanges,
|
|
2136
2339
|
createEntity,
|
|
2137
2340
|
findEntityByName,
|
|
2138
2341
|
findEntityById,
|
package/lib/summarize.js
CHANGED
|
@@ -144,7 +144,7 @@ function collectMessages(session, maxChars = 8000, language = "zh") {
|
|
|
144
144
|
break;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
-
return lines.length ? [createUserMessage({ content: [{ type: "text", text: trim(lines.join("\n"), maxChars) }] })] : [];
|
|
147
|
+
return lines.length ? [createUserMessage({ content: [{ type: "text", text: trim(lines.join("\n"), maxChars) }], source: { kind: "plugin", plugin: "dsh-mneme" } })] : [];
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// ── 智能调速器(429 保护)─────────────────────────────────────────────
|
|
@@ -265,7 +265,7 @@ export function createSummarizer(ctx, service, config) {
|
|
|
265
265
|
model: route.model,
|
|
266
266
|
purpose: "summarization",
|
|
267
267
|
messages: [
|
|
268
|
-
{ role: "system", content: [{ type: "text", text: config.codingRetrospect ? STR.prompts.codingSummary[langOf(config)] : STR.prompts.summary[langOf(config)] }] },
|
|
268
|
+
{ role: "system", content: [{ type: "text", text: config.codingRetrospect ? STR.prompts.codingSummary[langOf(config)] : STR.prompts.summary[langOf(config)] }], source: { kind: "plugin", plugin: "dsh-mneme" } },
|
|
269
269
|
...messages
|
|
270
270
|
],
|
|
271
271
|
signal: controller.signal
|
package/lib/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
-
import { createScopeResolver } from "./scope.js";
|
|
2
|
+
import { createScopeResolver, normalizeExplicitScope } from "./scope.js";
|
|
3
3
|
import { describeLocalRuntime, resolveRuntimeEntry } from "./runtime/loader.js";
|
|
4
4
|
import { defaultRuntimeDir } from "./runtime/layout.js";
|
|
5
5
|
import { hostModulesDir, loadRuntimeManifest, provisionRuntime } from "./runtime/provision.js";
|
|
@@ -27,14 +27,20 @@ const MEMORY_ITEM_SCHEMA = {
|
|
|
27
27
|
importance: { type: "integer", required: true },
|
|
28
28
|
source: { type: "string" },
|
|
29
29
|
// v0.8.0 A2:scope 标注与事件发生时间随行透出(未标注时缺省,可选属性)。
|
|
30
|
+
// v0.8.1 底座:scope 来源(auto/explicit)与决策时间随行透出(同样可选)。
|
|
30
31
|
agent_scope: { type: "string" },
|
|
31
32
|
workspace_scope: { type: "string" },
|
|
33
|
+
agent_scope_source: { type: "string" },
|
|
34
|
+
workspace_scope_source: { type: "string" },
|
|
35
|
+
scope_decided_at: { type: "string" },
|
|
32
36
|
sensitivity: { type: "string" },
|
|
33
37
|
occurred_at: { type: "string" },
|
|
34
38
|
created_at: { type: "string", required: true },
|
|
35
39
|
updated_at: { type: "string", required: true }
|
|
36
40
|
}
|
|
37
41
|
};
|
|
42
|
+
// 导出供回归测试断言 memory_get 与共享 schema 同源(#184 防漂移护栏)。
|
|
43
|
+
export { MEMORY_ITEM_SCHEMA };
|
|
38
44
|
|
|
39
45
|
// v0.8.0 A2:occurred 时间窗参数 → store 过滤选项(list/count 同口径);
|
|
40
46
|
// 未传参返回空对象,既有调用方不受影响。(A3 复核:此处入参仅含 occurred 边界。)
|
|
@@ -75,6 +81,26 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
75
81
|
// flag 关闭时恒返回 null——写入不标注,检索不加权,行为与 A1 前完全一致。
|
|
76
82
|
// logger 透传:registry 反查失败时 warnOnce 才有出口(否则静默降级无观测)。
|
|
77
83
|
const resolveSessionScope = createScopeResolver({ ctx, config, logger: ctx.logger });
|
|
84
|
+
|
|
85
|
+
// 复核项 4(issue #170):strictScope 下他 scope(explicit)的行对工具侧按
|
|
86
|
+
// 「不存在」处理——update/delete 与 memory_get 同款无存在性泄漏。strictScope
|
|
87
|
+
// 关、或会话身份取不到(scope 为 null)时恒可见(与 get 的短路语义一致)。
|
|
88
|
+
const visibleUnderStrictScope = (memory, exec) => {
|
|
89
|
+
if (config?.strictScope !== true) return true;
|
|
90
|
+
const scope = resolveSessionScope(exec);
|
|
91
|
+
return !scope || service.isVisibleInScope(memory, scope);
|
|
92
|
+
};
|
|
93
|
+
// 在调用点以「展开恒为 undefined」的形态前置校验(抛错即中止写入)。
|
|
94
|
+
const assertVisibleForTool = (id, exec) => {
|
|
95
|
+
const memory = id ? service.getById(id) : undefined;
|
|
96
|
+
if (!visibleUnderStrictScope(memory, exec)) throw new Error("memory not found");
|
|
97
|
+
return undefined;
|
|
98
|
+
};
|
|
99
|
+
// delete 侧需要目标行本身判断存在性(不可见 = 视作不存在,不抛错)。
|
|
100
|
+
const visibleForTool = (id, exec) => {
|
|
101
|
+
const memory = id ? service.getById(id) : undefined;
|
|
102
|
+
return visibleUnderStrictScope(memory, exec) ? memory : undefined;
|
|
103
|
+
};
|
|
78
104
|
const tools = [
|
|
79
105
|
defineTool({
|
|
80
106
|
name: "memory_save",
|
|
@@ -90,7 +116,9 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
90
116
|
importance: { type: "integer", description: "1-5; >= threshold auto-injects into future sessions" },
|
|
91
117
|
source: { type: "string", description: "Optional provenance" },
|
|
92
118
|
sensitivity: { type: "string", description: "Optional sensitivity label (free-form, e.g. personal). Same-title entries with different sensitivity stay separate instead of merging." },
|
|
93
|
-
occurred_at: { type: "string", description: "Optional ISO-8601 instant the remembered event happened (differs from write time). Invalid values are ignored." }
|
|
119
|
+
occurred_at: { type: "string", description: "Optional ISO-8601 instant the remembered event happened (differs from write time). Invalid values are ignored." },
|
|
120
|
+
agent_scope: { type: "string", description: "Optional explicit agent-scope declaration (issue #170): 'global' or '*' makes this memory visible to every agent; any other value narrows it to that label. Overrides the automatic carrier label for this write; honored even when automatic scope labeling is disabled." },
|
|
121
|
+
workspace_scope: { type: "string", description: "Optional explicit workspace-scope declaration: 'global' or '*' makes this memory visible in every workspace; any other value narrows it to that label. Overrides the automatic carrier label for this write; honored even when automatic scope labeling is disabled." }
|
|
94
122
|
},
|
|
95
123
|
output: {
|
|
96
124
|
schema: {
|
|
@@ -104,10 +132,22 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
104
132
|
render: (_args, value) => TEXT_OUTPUT(`memory ${value.action}: ${value.id}`)
|
|
105
133
|
},
|
|
106
134
|
async execute(args, exec) {
|
|
107
|
-
// scope
|
|
108
|
-
//
|
|
109
|
-
//
|
|
135
|
+
// scope 标注(v0.8.1 底座,issue #170):默认由会话身份解析(载体自动
|
|
136
|
+
// 标注,盖 auto 章);显式 agent_scope/workspace_scope 参数逐维覆盖并盖
|
|
137
|
+
// explicit 章。解析绝不抛错、取不到落 NULL。
|
|
138
|
+
// 复核项 2(#170):scopeEnabled 只门控「自动标注」分支——显式声明与
|
|
139
|
+
// 读取身份(search/list/get/注入经同一解析器)不受 flag 限制,否则
|
|
140
|
+
// strictScope 硬墙会被 flag 静默拆掉(fail-open)。
|
|
141
|
+
// (存储 NULL,由 source 列与「从未标注」区分)。非字符串参数在工具
|
|
142
|
+
// 入参 schema 层即被拒(type: "string")——脏值到不了归一化,无静默放宽。
|
|
110
143
|
const scope = resolveSessionScope(exec);
|
|
144
|
+
const autoStamping = config?.scopeEnabled === true;
|
|
145
|
+
const agentLabel = args.agent_scope !== undefined
|
|
146
|
+
? { value: normalizeExplicitScope(args.agent_scope), source: "explicit" }
|
|
147
|
+
: autoStamping && scope ? { value: scope.agent_scope, source: "auto" } : null;
|
|
148
|
+
const workspaceLabel = args.workspace_scope !== undefined
|
|
149
|
+
? { value: normalizeExplicitScope(args.workspace_scope), source: "explicit" }
|
|
150
|
+
: autoStamping && scope ? { value: scope.workspace_scope, source: "auto" } : null;
|
|
111
151
|
const { action, memory } = service.saveWithDedupe({
|
|
112
152
|
type: args.type,
|
|
113
153
|
title: args.title,
|
|
@@ -115,7 +155,8 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
115
155
|
tags: args.tags ?? [],
|
|
116
156
|
importance: args.importance ?? 3,
|
|
117
157
|
source: args.source ?? "tool",
|
|
118
|
-
...(
|
|
158
|
+
...(agentLabel ? { agent_scope: agentLabel.value, agent_scope_source: agentLabel.source } : {}),
|
|
159
|
+
...(workspaceLabel ? { workspace_scope: workspaceLabel.value, workspace_scope_source: workspaceLabel.source } : {}),
|
|
119
160
|
...(args.sensitivity !== undefined ? { sensitivity: args.sensitivity } : {}),
|
|
120
161
|
...(args.occurred_at !== undefined ? { occurred_at: args.occurred_at } : {})
|
|
121
162
|
});
|
|
@@ -238,25 +279,10 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
238
279
|
type: "object",
|
|
239
280
|
additionalProperties: false,
|
|
240
281
|
properties: {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
id: { type: "string", required: true },
|
|
246
|
-
title: { type: "string", required: true },
|
|
247
|
-
type: { type: "string", required: true },
|
|
248
|
-
importance: { type: "integer", required: true },
|
|
249
|
-
tags: { type: "array", items: { type: "string" } },
|
|
250
|
-
content: { type: "string", required: true },
|
|
251
|
-
source: { type: "string" },
|
|
252
|
-
agent_scope: { type: "string" },
|
|
253
|
-
workspace_scope: { type: "string" },
|
|
254
|
-
sensitivity: { type: "string" },
|
|
255
|
-
occurred_at: { type: "string" },
|
|
256
|
-
created_at: { type: "string" },
|
|
257
|
-
updated_at: { type: "string" }
|
|
258
|
-
}
|
|
259
|
-
}
|
|
282
|
+
// 与 memory_search / memory_list 共用同一份 item schema——DTO 由
|
|
283
|
+
// toApiList 统一产出,内联副本曾漏掉 v0.8.1 的 scope 来源三键,
|
|
284
|
+
// 任何被标注过的行都过不了 in-process 校验(#184)。
|
|
285
|
+
memory: MEMORY_ITEM_SCHEMA
|
|
260
286
|
}
|
|
261
287
|
},
|
|
262
288
|
render: (_args, value) => {
|
|
@@ -288,7 +314,9 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
288
314
|
type: { type: "string", enum: ["preference", "project", "decision", "history", "rejected_solution", "pitfall", "constraint"] },
|
|
289
315
|
tags: { type: "array", items: { type: "string" } },
|
|
290
316
|
importance: { type: "integer", description: "1-5" },
|
|
291
|
-
reason: { type: "string", description: "Optional context for the correction (what the user actually said/wanted), recorded for reflection" }
|
|
317
|
+
reason: { type: "string", description: "Optional context for the correction (what the user actually said/wanted), recorded for reflection" },
|
|
318
|
+
agent_scope: { type: "string", description: "Optional explicit agent-scope correction (issue #170): 'global' or '*' widens visibility to every agent; any other value narrows it to that label. Omit to keep the current agent scope. Recorded as an explicit scope decision (audited)." },
|
|
319
|
+
workspace_scope: { type: "string", description: "Optional explicit workspace-scope correction: 'global' or '*' widens visibility to every workspace; any other value narrows it to that label. Omit to keep the current workspace scope. Recorded as an explicit scope decision (audited)." }
|
|
292
320
|
},
|
|
293
321
|
output: {
|
|
294
322
|
schema: {
|
|
@@ -314,7 +342,12 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
314
342
|
content: args.content,
|
|
315
343
|
type: args.type,
|
|
316
344
|
tags: args.tags,
|
|
317
|
-
importance: args.importance
|
|
345
|
+
importance: args.importance,
|
|
346
|
+
// 复核项 4(#170):strictScope 下他 scope 的行按「不存在」处理(与 get 对齐),
|
|
347
|
+
// 展开恒为 undefined,仅承担写入前校验。
|
|
348
|
+
...(assertVisibleForTool(args.id, arguments[1])),
|
|
349
|
+
...(args.agent_scope !== undefined ? { agent_scope: args.agent_scope } : {}),
|
|
350
|
+
...(args.workspace_scope !== undefined ? { workspace_scope: args.workspace_scope } : {})
|
|
318
351
|
}, { query: args.reason });
|
|
319
352
|
return { memory: { id: memory.id, title: memory.title, content: memory.content } };
|
|
320
353
|
}
|
|
@@ -335,9 +368,10 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
335
368
|
render: (_args, value) => TEXT_OUTPUT(value.deleted ? "Memory deleted." : "Memory not found.")
|
|
336
369
|
},
|
|
337
370
|
async execute(args) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
371
|
+
// 复核项 4(#170):不可见(explicit 他 scope)= 视作不存在,无存在性泄漏。
|
|
372
|
+
const target = visibleForTool(args.id, arguments[1]);
|
|
373
|
+
if (target) service.remove(args.id);
|
|
374
|
+
return { deleted: !!target };
|
|
341
375
|
}
|
|
342
376
|
}),
|
|
343
377
|
|