@modusensus/dsh-mneme 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/lib/api.js +40 -12
- package/lib/client.js +130 -8
- package/lib/dream/sleep.js +43 -12
- package/lib/dream.js +43 -2
- package/lib/lang.js +11 -0
- package/lib/scope.js +39 -4
- package/lib/service.js +105 -18
- package/lib/store.js +158 -15
- package/lib/tools.js +58 -11
- package/lib/version-check.js +122 -0
- package/package.json +1 -1
- package/src/api.js +40 -12
- package/src/dream/sleep.js +43 -12
- package/src/dream.js +43 -2
- package/src/lang.js +11 -0
- package/src/scope.js +39 -4
- package/src/service.js +105 -18
- package/src/store.js +158 -15
- package/src/tools.js +58 -11
- package/src/version-check.js +122 -0
- package/test/conflict-freeze.test.js +15 -5
- 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/version-check.test.js +152 -0
package/src/service.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { TYPE_FILE } from "./mirror.js";
|
|
3
3
|
import { updatedAtBounds } from "./store.js";
|
|
4
|
+
import { normalizeExplicitScope, scopeKeyOf } from "./scope.js";
|
|
4
5
|
import { STR, langOf } from "./lang.js";
|
|
5
6
|
import { computeHeat } from "./heat.js";
|
|
6
7
|
import { evaluateMemoryQuality } from "./quality-filter.js";
|
|
@@ -40,12 +41,8 @@ const CONTENT_HISTORY_MAX = 20;
|
|
|
40
41
|
// 清单必须与 quality-filter.js 的写入端对齐(6 个,含 type 自指的 self_referential)。
|
|
41
42
|
const SIGNAL_TAGS = ["low_quality", "duplicate", "meta", "repetitive", "short_content", "self_referential"];
|
|
42
43
|
|
|
43
|
-
// v0.8.
|
|
44
|
-
//
|
|
45
|
-
// 口径一致——两把钥匙只有都过这里再比较,NULL 与 'global' 才不会错配。
|
|
46
|
-
function scopeKeyOf(v) {
|
|
47
|
-
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
48
|
-
}
|
|
44
|
+
// scopeKeyOf 已上提到 ./scope.js(v0.8.1,issue #170 第 2 步):与 sleep 的跨
|
|
45
|
+
// scope 配对共用同一把比较钥匙,避免两处定义漂移。
|
|
49
46
|
|
|
50
47
|
// v0.8.0 A2(issue #17)scope 检索加权系数:两维都无法确立 foreign 的候选
|
|
51
48
|
// (命中行、未标注行、当前侧维度解析不到的行)→ 加成;只有确立了 foreign
|
|
@@ -93,10 +90,18 @@ function inOccurredBounds(m, bounds) {
|
|
|
93
90
|
* 留给后续批次)。store.list 的 SQL 过滤与此谓词同口径(见 store.js visibility)。
|
|
94
91
|
*/
|
|
95
92
|
function isVisibleInScope(m, current) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
// v0.8.1 第 3 步(issue #170 4.3,作者已确认):硬过滤只认显式声明。某维
|
|
94
|
+
// 仅当「该维有标注且来源为 explicit」才构成硬墙;auto(含 v0.8.0 存量的
|
|
95
|
+
// NULL 来源行)只吃 A2 软加权(foreign ×0.5 保留可见),不进硬过滤。
|
|
96
|
+
// 显式全局(值 NULL + source explicit)与未标注同形,天然恒可见。
|
|
97
|
+
if (m.agent_scope_source === "explicit") {
|
|
98
|
+
const agent = scopeKeyOf(m.agent_scope);
|
|
99
|
+
if (agent !== null && (current.agent_scope === null || agent !== current.agent_scope)) return false;
|
|
100
|
+
}
|
|
101
|
+
if (m.workspace_scope_source === "explicit") {
|
|
102
|
+
const ws = scopeKeyOf(m.workspace_scope);
|
|
103
|
+
if (ws !== null && (current.workspace_scope === null || ws !== current.workspace_scope)) return false;
|
|
104
|
+
}
|
|
100
105
|
return true;
|
|
101
106
|
}
|
|
102
107
|
|
|
@@ -1064,17 +1069,19 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1064
1069
|
// 标题变化,若仍按 (type, title) 匹配会出现两个活跃的 summary 同时注入。
|
|
1065
1070
|
// 其余类型维持 (type, title) 匹配不变。
|
|
1066
1071
|
const candidates = store.list({ type: memory.type, limit: 100 });
|
|
1067
|
-
// v0.8.0 A1(issue #17
|
|
1072
|
+
// v0.8.0 A1(issue #17):去重键由 (type, title) 扩展为
|
|
1068
1073
|
// (type, title, agent_scope, workspace_scope, sensitivity)——不同作用域的同
|
|
1069
1074
|
// 标题记忆绝不互相物理合并(旧逻辑跨 agent/workspace 并行,事后无法拆分)。
|
|
1070
1075
|
// NULL 归一为「未标注」:未标注行之间互相匹配(含存量行),与已标注行不
|
|
1071
|
-
// 匹配。
|
|
1072
|
-
|
|
1076
|
+
// 匹配。
|
|
1077
|
+
// v0.8.1(issue #170 复核项 1):比较不再受 scopeEnabled 门控——显式声明
|
|
1078
|
+
// flag 关也生效(工具层明写「读了自动标注也生效」,去重必须同口径),否则
|
|
1079
|
+
// 关自动标注后同标题不同 scope 的行会物理合并进第一行的归属。存量全 NULL
|
|
1080
|
+
// 行互相匹配,A1 前行为不变。
|
|
1073
1081
|
const scopeMatches = (m) =>
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
scopeKeyOf(m.sensitivity) === scopeKeyOf(memory.sensitivity));
|
|
1082
|
+
scopeKeyOf(m.agent_scope) === scopeKeyOf(memory.agent_scope) &&
|
|
1083
|
+
scopeKeyOf(m.workspace_scope) === scopeKeyOf(memory.workspace_scope) &&
|
|
1084
|
+
scopeKeyOf(m.sensitivity) === scopeKeyOf(memory.sensitivity);
|
|
1078
1085
|
// Issue #127:写入端语义去重命中时,调用方用 _mergeInto 指定并入目标——复用
|
|
1079
1086
|
// 下面这段并入逻辑(appendContent + content_history + 质量处置),不另写第二份
|
|
1080
1087
|
// 实现。目标已被并发删除时回落到常规匹配。显式目标同样过 scope 门:跨作用域
|
|
@@ -1097,6 +1104,17 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1097
1104
|
? newContent
|
|
1098
1105
|
: appendContent(existing.content, newContent);
|
|
1099
1106
|
const importance = Math.min(5, Math.max(existing.importance, memory.importance ?? existing.importance));
|
|
1107
|
+
// v0.8.1 底座(issue #170):去重命中=scope 值完全同键,此时显式写入把
|
|
1108
|
+
// 被并入行的同维来源升级为 explicit(用户刚刚显式声明了同一归属),
|
|
1109
|
+
// 并刷新 scope_decided_at。仅升级来源、不改值——值改变会走新行。
|
|
1110
|
+
const scopeUpgrade = {};
|
|
1111
|
+
if (memory.agent_scope_source === "explicit" && existing.agent_scope_source !== "explicit") {
|
|
1112
|
+
scopeUpgrade.agent_scope_source = "explicit";
|
|
1113
|
+
}
|
|
1114
|
+
if (memory.workspace_scope_source === "explicit" && existing.workspace_scope_source !== "explicit") {
|
|
1115
|
+
scopeUpgrade.workspace_scope_source = "explicit";
|
|
1116
|
+
}
|
|
1117
|
+
if (Object.keys(scopeUpgrade).length) scopeUpgrade.scope_decided_at = new Date().toISOString();
|
|
1100
1118
|
const merged = store.update(existing.id, {
|
|
1101
1119
|
content,
|
|
1102
1120
|
importance,
|
|
@@ -1105,8 +1123,30 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1105
1123
|
content_history: pushContentHistory(existing, direct
|
|
1106
1124
|
? (memory._humanEdited === true ? "human_override" : "overwrite")
|
|
1107
1125
|
: "auto_merge"),
|
|
1126
|
+
...scopeUpgrade,
|
|
1108
1127
|
...(quality ? { quality_score: quality.score } : {})
|
|
1109
1128
|
});
|
|
1129
|
+
// v0.8.1(issue #170 review 3):来源升级也是归属性质改变——与
|
|
1130
|
+
// updateMemory 的显式修正同等待遇,落一行 scope_changes(actor=tool:
|
|
1131
|
+
// 只有 memory_save 的显式参数会带 explicit 来源走到这里)。审计失败只
|
|
1132
|
+
// warn 不反噬合并。升级只在来源首次变化时触发,不会每次并入都写。
|
|
1133
|
+
if (Object.keys(scopeUpgrade).length) {
|
|
1134
|
+
try {
|
|
1135
|
+
store.saveScopeChange({
|
|
1136
|
+
memory_id: existing.id,
|
|
1137
|
+
actor: "tool",
|
|
1138
|
+
prev_agent_scope: existing.agent_scope ?? null,
|
|
1139
|
+
prev_workspace_scope: existing.workspace_scope ?? null,
|
|
1140
|
+
next_agent_scope: merged.agent_scope ?? null,
|
|
1141
|
+
next_workspace_scope: merged.workspace_scope ?? null,
|
|
1142
|
+
agent_scope_source: merged.agent_scope_source ?? null,
|
|
1143
|
+
workspace_scope_source: merged.workspace_scope_source ?? null,
|
|
1144
|
+
decided_at: scopeUpgrade.scope_decided_at
|
|
1145
|
+
});
|
|
1146
|
+
} catch (e) {
|
|
1147
|
+
try { logger?.warn?.(`[dsh-mneme] scope upgrade audit failed: ${String(e)}`); } catch { /* 不反噬 */ }
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1110
1150
|
// Bug7: a degraded/archived result is applied on top of the merged row.
|
|
1111
1151
|
const result = applyQualityDisposition(merged, quality, qf);
|
|
1112
1152
|
afterSync("write");
|
|
@@ -1122,8 +1162,12 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1122
1162
|
importance: memory.importance ?? 3,
|
|
1123
1163
|
source: memory.source ?? "manual",
|
|
1124
1164
|
// v0.8.0 A1:scope 标注透传(store 端归一化,未标注落 NULL)。
|
|
1165
|
+
// v0.8.1 底座:来源(auto/explicit)与决策时间随行透传。
|
|
1125
1166
|
agent_scope: memory.agent_scope,
|
|
1126
1167
|
workspace_scope: memory.workspace_scope,
|
|
1168
|
+
agent_scope_source: memory.agent_scope_source,
|
|
1169
|
+
workspace_scope_source: memory.workspace_scope_source,
|
|
1170
|
+
scope_decided_at: memory.scope_decided_at,
|
|
1127
1171
|
sensitivity: memory.sensitivity,
|
|
1128
1172
|
occurred_at: memory.occurred_at,
|
|
1129
1173
|
...(quality ? { quality_score: quality.score } : {})
|
|
@@ -1441,8 +1485,12 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1441
1485
|
// v0.8.0 A2:scope 标注与事件发生时间透出——条件展开(未标注行不带键,
|
|
1442
1486
|
// DTO 与 A2 前逐字节同形;带 undefined 键会被 in-process schema 校验
|
|
1443
1487
|
// 判违规,JSON 序列化虽会丢弃但形状不稳定)。
|
|
1488
|
+
// v0.8.1 底座:scope 来源与决策时间随行透出(同样条件展开)。
|
|
1444
1489
|
...(m.agent_scope !== undefined ? { agent_scope: m.agent_scope } : {}),
|
|
1445
1490
|
...(m.workspace_scope !== undefined ? { workspace_scope: m.workspace_scope } : {}),
|
|
1491
|
+
...(m.agent_scope_source !== undefined ? { agent_scope_source: m.agent_scope_source } : {}),
|
|
1492
|
+
...(m.workspace_scope_source !== undefined ? { workspace_scope_source: m.workspace_scope_source } : {}),
|
|
1493
|
+
...(m.scope_decided_at !== undefined ? { scope_decided_at: m.scope_decided_at } : {}),
|
|
1446
1494
|
...(m.sensitivity !== undefined ? { sensitivity: m.sensitivity } : {}),
|
|
1447
1495
|
...(m.occurred_at !== undefined ? { occurred_at: m.occurred_at } : {})
|
|
1448
1496
|
}));
|
|
@@ -1734,10 +1782,47 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1734
1782
|
// tags——任何带 tags 的更新都会抹掉这条唯一的审计线索(报告实测 150 条低分
|
|
1735
1783
|
// 记忆里恰好缺的 2 条就是被 update 过的,并因此误判过归档来源)。把 existing
|
|
1736
1784
|
// 的系统信号标签并集保留;用户自传的标签照常生效。
|
|
1737
|
-
|
|
1785
|
+
let patch = (p && Array.isArray(p.tags) && old && Array.isArray(old.tags))
|
|
1738
1786
|
? { ...p, tags: [...new Set([...p.tags, ...old.tags.filter((t) => SIGNAL_TAGS.includes(t))])] }
|
|
1739
1787
|
: p;
|
|
1788
|
+
// v0.8.1 底座(issue #170):显式 scope 修正。patch 携带 agent_scope /
|
|
1789
|
+
// workspace_scope 键(undefined=该维不动)时归一化并盖 explicit 章 +
|
|
1790
|
+
// scope_decided_at,随后写 scope_changes 审计行(actor:tool=模型侧 /
|
|
1791
|
+
// panel=人工侧)。审计失败只 warn,不反噬主写入。
|
|
1792
|
+
const nextAgentScope = p && p.agent_scope !== undefined ? normalizeExplicitScope(p.agent_scope) : undefined;
|
|
1793
|
+
const nextWorkspaceScope = p && p.workspace_scope !== undefined ? normalizeExplicitScope(p.workspace_scope) : undefined;
|
|
1794
|
+
const scopeChanged = nextAgentScope !== undefined || nextWorkspaceScope !== undefined;
|
|
1795
|
+
if (scopeChanged) {
|
|
1796
|
+
const decidedAt = new Date().toISOString();
|
|
1797
|
+
patch = { ...patch };
|
|
1798
|
+
if (nextAgentScope !== undefined) {
|
|
1799
|
+
patch.agent_scope = nextAgentScope;
|
|
1800
|
+
patch.agent_scope_source = "explicit";
|
|
1801
|
+
}
|
|
1802
|
+
if (nextWorkspaceScope !== undefined) {
|
|
1803
|
+
patch.workspace_scope = nextWorkspaceScope;
|
|
1804
|
+
patch.workspace_scope_source = "explicit";
|
|
1805
|
+
}
|
|
1806
|
+
patch.scope_decided_at = decidedAt;
|
|
1807
|
+
}
|
|
1740
1808
|
const updated = store.update(id, patch);
|
|
1809
|
+
if (scopeChanged && old && updated) {
|
|
1810
|
+
try {
|
|
1811
|
+
store.saveScopeChange({
|
|
1812
|
+
memory_id: id,
|
|
1813
|
+
actor: ctx.actor === "panel" ? "panel" : "tool",
|
|
1814
|
+
prev_agent_scope: old.agent_scope ?? null,
|
|
1815
|
+
prev_workspace_scope: old.workspace_scope ?? null,
|
|
1816
|
+
next_agent_scope: updated.agent_scope ?? null,
|
|
1817
|
+
next_workspace_scope: updated.workspace_scope ?? null,
|
|
1818
|
+
agent_scope_source: updated.agent_scope_source ?? null,
|
|
1819
|
+
workspace_scope_source: updated.workspace_scope_source ?? null,
|
|
1820
|
+
decided_at: patch.scope_decided_at
|
|
1821
|
+
});
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
try { ctx.logger?.warn?.(`[dsh-mneme] scope change audit failed: ${String(e)}`); } catch { /* 同样不反噬 */ }
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1741
1826
|
// Record a user correction when any meaningful field changed and the
|
|
1742
1827
|
// reflection failure tracker is enabled. expected = what it became,
|
|
1743
1828
|
// actual = what it was before; query (when provided) captures the
|
|
@@ -1843,6 +1928,8 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1843
1928
|
notifyWrite();
|
|
1844
1929
|
},
|
|
1845
1930
|
update: updateMemory,
|
|
1931
|
+
// v0.8.1 底座:scope 归属修正审计(单条记忆,新→旧)。
|
|
1932
|
+
listScopeChanges: (memoryId, opts) => store.listScopeChanges(memoryId, opts),
|
|
1846
1933
|
// Compare-and-set update: applies the patch only when the row still carries
|
|
1847
1934
|
// `expectedUpdatedAt`. Returns undefined on a miss (no write) so the caller
|
|
1848
1935
|
// can re-read and retry — the primitive that prevents lost updates across
|
package/src/store.js
CHANGED
|
@@ -148,6 +148,25 @@ CREATE TABLE IF NOT EXISTS conflict_pending (
|
|
|
148
148
|
);
|
|
149
149
|
CREATE INDEX IF NOT EXISTS idx_conflict_pending_unresolved ON conflict_pending(resolved_at);
|
|
150
150
|
|
|
151
|
+
-- scope_changes: v0.8.1 底座(issue #170)scope 归属的人工修正审计。显式声明
|
|
152
|
+
-- (memory_save/memory_update 的 scope 参数、面板编辑)每次改变某条记忆的
|
|
153
|
+
-- agent/workspace 归属都落一行:改动前后值 + 来源 + actor(tool=模型侧 /
|
|
154
|
+
-- panel=人工侧)。与 conflict_pending 同款的 bookkeeping 表:只做审计,
|
|
155
|
+
-- 不触发 write hooks。放宽可见性(label→global)的候选筛选与回放都靠它。
|
|
156
|
+
CREATE TABLE IF NOT EXISTS scope_changes (
|
|
157
|
+
id TEXT PRIMARY KEY,
|
|
158
|
+
memory_id TEXT NOT NULL,
|
|
159
|
+
actor TEXT NOT NULL,
|
|
160
|
+
prev_agent_scope TEXT,
|
|
161
|
+
prev_workspace_scope TEXT,
|
|
162
|
+
next_agent_scope TEXT,
|
|
163
|
+
next_workspace_scope TEXT,
|
|
164
|
+
agent_scope_source TEXT,
|
|
165
|
+
workspace_scope_source TEXT,
|
|
166
|
+
decided_at TEXT NOT NULL
|
|
167
|
+
);
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_scope_changes_memory ON scope_changes(memory_id);
|
|
169
|
+
|
|
151
170
|
-- llm_audit_logs: every background LLM call (autoDream consolidation + summary,
|
|
152
171
|
-- autoSummarize compression) is recorded here — tokens in/out, duration, status
|
|
153
172
|
-- and the trigger that caused it (Bug8). Failures are captured as status='error'
|
|
@@ -344,6 +363,13 @@ function normalizeScopeText(raw) {
|
|
|
344
363
|
return s ? s : null;
|
|
345
364
|
}
|
|
346
365
|
|
|
366
|
+
// scope 来源列(issue #170 底座):'auto'=载体自动标注,'explicit'=显式声明。
|
|
367
|
+
// 只认这两个枚举值,其余(含 NULL)落 NULL——NULL 语义是「未标注,或 0.8.1
|
|
368
|
+
// 之前落库的存量标注(视为 auto 的软效力,第 3 步语义收窄时按此口径读)」。
|
|
369
|
+
function normalizeScopeSource(raw) {
|
|
370
|
+
return raw === "auto" || raw === "explicit" ? raw : null;
|
|
371
|
+
}
|
|
372
|
+
|
|
347
373
|
// occurred_at:事件发生时间(区别于 created_at 的入库时间)。可解析的时间戳
|
|
348
374
|
// 统一归一到 UTC ISO(A2 的时间过滤要按字典序直接比较);解析失败落 NULL,
|
|
349
375
|
// 同样不阻塞写入。
|
|
@@ -371,6 +397,9 @@ function toRow(row) {
|
|
|
371
397
|
epistemic_status: row.epistemic_status ?? "subjective",
|
|
372
398
|
agent_scope: row.agent_scope ?? undefined,
|
|
373
399
|
workspace_scope: row.workspace_scope ?? undefined,
|
|
400
|
+
agent_scope_source: row.agent_scope_source ?? undefined,
|
|
401
|
+
workspace_scope_source: row.workspace_scope_source ?? undefined,
|
|
402
|
+
scope_decided_at: row.scope_decided_at ?? undefined,
|
|
374
403
|
sensitivity: row.sensitivity ?? undefined,
|
|
375
404
|
occurred_at: row.occurred_at ?? undefined,
|
|
376
405
|
created_at: row.created_at,
|
|
@@ -438,6 +467,22 @@ function toConflictPending(row) {
|
|
|
438
467
|
};
|
|
439
468
|
}
|
|
440
469
|
|
|
470
|
+
function toScopeChange(row) {
|
|
471
|
+
if (!row) return undefined;
|
|
472
|
+
return {
|
|
473
|
+
id: row.id,
|
|
474
|
+
memory_id: row.memory_id,
|
|
475
|
+
actor: row.actor,
|
|
476
|
+
prev_agent_scope: row.prev_agent_scope ?? undefined,
|
|
477
|
+
prev_workspace_scope: row.prev_workspace_scope ?? undefined,
|
|
478
|
+
next_agent_scope: row.next_agent_scope ?? undefined,
|
|
479
|
+
next_workspace_scope: row.next_workspace_scope ?? undefined,
|
|
480
|
+
agent_scope_source: row.agent_scope_source ?? undefined,
|
|
481
|
+
workspace_scope_source: row.workspace_scope_source ?? undefined,
|
|
482
|
+
decided_at: row.decided_at
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
441
486
|
function toRecallRun(row) {
|
|
442
487
|
if (!row) return undefined;
|
|
443
488
|
return {
|
|
@@ -631,6 +676,16 @@ export function createStore(path) {
|
|
|
631
676
|
addColumn("memories", "sensitivity", "ALTER TABLE memories ADD COLUMN sensitivity TEXT");
|
|
632
677
|
addColumn("memories", "occurred_at", "ALTER TABLE memories ADD COLUMN occurred_at TEXT");
|
|
633
678
|
|
|
679
|
+
// v0.8.1 底座(issue #170):scope 标注的来源与决策时间。三列全部可空不带
|
|
680
|
+
// DEFAULT(与 A1 同款零重写迁移)。来源拆成 agent/workspace 各一列而非单列
|
|
681
|
+
// scope_source:混合来源(agent 维显式 + workspace 维自动)必须可表达——否则
|
|
682
|
+
// 「提升回全局」之后遗留的自动 workspace 标签会在效力收窄时被一并当成显式,
|
|
683
|
+
// 恰好复活 issue #170 抱怨的「自动标签吃硬过滤」问题。存量标注行三列皆 NULL,
|
|
684
|
+
// 读侧按 auto(软效力)口径解释。
|
|
685
|
+
addColumn("memories", "agent_scope_source", "ALTER TABLE memories ADD COLUMN agent_scope_source TEXT");
|
|
686
|
+
addColumn("memories", "workspace_scope_source", "ALTER TABLE memories ADD COLUMN workspace_scope_source TEXT");
|
|
687
|
+
addColumn("memories", "scope_decided_at", "ALTER TABLE memories ADD COLUMN scope_decided_at TEXT");
|
|
688
|
+
|
|
634
689
|
// Legacy dream_runs without policy_epoch → backfill with the default epoch.
|
|
635
690
|
addColumn("dream_runs", "policy_epoch", "ALTER TABLE dream_runs ADD COLUMN policy_epoch INTEGER NOT NULL DEFAULT 0");
|
|
636
691
|
addColumn("dream_runs", "run_type", "ALTER TABLE dream_runs ADD COLUMN run_type TEXT NOT NULL DEFAULT 'auto'");
|
|
@@ -718,17 +773,20 @@ export function createStore(path) {
|
|
|
718
773
|
// occurred_at 闭区间:与 list() 同过滤(visibility 同口径),memory_list
|
|
719
774
|
// 的 total 才能和过滤后的行保持一致。
|
|
720
775
|
if (visibility) {
|
|
776
|
+
// v0.8.1 第 3 步(issue #170 4.3):硬过滤只认显式声明——与
|
|
777
|
+
// service.isVisibleInScope 同口径。IS NOT 是 NULL 安全比较:来源为
|
|
778
|
+
// auto/NULL(v0.8.0 存量自动标注)的行不吃硬墙,只走 A2 软加权。
|
|
721
779
|
if (visibility.agentScope != null) {
|
|
722
|
-
clauses.push("(agent_scope IS NULL OR agent_scope = ?)");
|
|
780
|
+
clauses.push("(agent_scope IS NULL OR agent_scope_source IS NOT 'explicit' OR agent_scope = ?)");
|
|
723
781
|
params.push(visibility.agentScope);
|
|
724
782
|
} else {
|
|
725
|
-
clauses.push("agent_scope IS NULL");
|
|
783
|
+
clauses.push("(agent_scope IS NULL OR agent_scope_source IS NOT 'explicit')");
|
|
726
784
|
}
|
|
727
785
|
if (visibility.workspaceScope != null) {
|
|
728
|
-
clauses.push("(workspace_scope IS NULL OR workspace_scope = ?)");
|
|
786
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope_source IS NOT 'explicit' OR workspace_scope = ?)");
|
|
729
787
|
params.push(visibility.workspaceScope);
|
|
730
788
|
} else {
|
|
731
|
-
clauses.push("workspace_scope IS NULL");
|
|
789
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope_source IS NOT 'explicit')");
|
|
732
790
|
}
|
|
733
791
|
}
|
|
734
792
|
if (!includeForgotten) {
|
|
@@ -775,8 +833,8 @@ export function createStore(path) {
|
|
|
775
833
|
: inferEpistemicStatus(memory);
|
|
776
834
|
runAtomically(() => {
|
|
777
835
|
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, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
836
|
+
`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)
|
|
837
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
780
838
|
).run(
|
|
781
839
|
id,
|
|
782
840
|
type,
|
|
@@ -792,6 +850,9 @@ export function createStore(path) {
|
|
|
792
850
|
epistemicStatus,
|
|
793
851
|
normalizeScopeText(memory.agent_scope),
|
|
794
852
|
normalizeScopeText(memory.workspace_scope),
|
|
853
|
+
normalizeScopeSource(memory.agent_scope_source),
|
|
854
|
+
normalizeScopeSource(memory.workspace_scope_source),
|
|
855
|
+
normalizeOccurredAt(memory.scope_decided_at),
|
|
795
856
|
normalizeScopeText(memory.sensitivity),
|
|
796
857
|
normalizeOccurredAt(memory.occurred_at),
|
|
797
858
|
now,
|
|
@@ -825,9 +886,26 @@ export function createStore(path) {
|
|
|
825
886
|
const qualityScore = patch.quality_score !== undefined && Number.isFinite(patch.quality_score)
|
|
826
887
|
? patch.quality_score
|
|
827
888
|
: (existing.quality_score ?? null);
|
|
889
|
+
// v0.8.1 底座(issue #170):scope 列只在本 patch 显式携带该键时才改写
|
|
890
|
+
// (undefined=不动;null=清空为未标注/global)。来源列只认 auto|explicit。
|
|
891
|
+
const nextAgentScope = patch.agent_scope !== undefined
|
|
892
|
+
? normalizeScopeText(patch.agent_scope)
|
|
893
|
+
: (existing.agent_scope ?? null);
|
|
894
|
+
const nextWorkspaceScope = patch.workspace_scope !== undefined
|
|
895
|
+
? normalizeScopeText(patch.workspace_scope)
|
|
896
|
+
: (existing.workspace_scope ?? null);
|
|
897
|
+
const nextAgentSource = patch.agent_scope_source !== undefined
|
|
898
|
+
? normalizeScopeSource(patch.agent_scope_source)
|
|
899
|
+
: (existing.agent_scope_source ?? null);
|
|
900
|
+
const nextWorkspaceSource = patch.workspace_scope_source !== undefined
|
|
901
|
+
? normalizeScopeSource(patch.workspace_scope_source)
|
|
902
|
+
: (existing.workspace_scope_source ?? null);
|
|
903
|
+
const nextDecidedAt = patch.scope_decided_at !== undefined
|
|
904
|
+
? normalizeOccurredAt(patch.scope_decided_at)
|
|
905
|
+
: (existing.scope_decided_at ?? null);
|
|
828
906
|
runAtomically(() => {
|
|
829
907
|
db.prepare(
|
|
830
|
-
`UPDATE memories SET type=?, title=?, content=?, tags=?, importance=?, source=?, content_history=?, quality_score=?, embedding=?, epistemic_status=?, updated_at=? WHERE id=?`
|
|
908
|
+
`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
909
|
).run(
|
|
832
910
|
type,
|
|
833
911
|
patch.title ?? existing.title,
|
|
@@ -839,6 +917,11 @@ export function createStore(path) {
|
|
|
839
917
|
qualityScore,
|
|
840
918
|
embedding,
|
|
841
919
|
epistemicStatus,
|
|
920
|
+
nextAgentScope,
|
|
921
|
+
nextWorkspaceScope,
|
|
922
|
+
nextAgentSource,
|
|
923
|
+
nextWorkspaceSource,
|
|
924
|
+
nextDecidedAt,
|
|
842
925
|
now,
|
|
843
926
|
id
|
|
844
927
|
);
|
|
@@ -1042,21 +1125,25 @@ export function createStore(path) {
|
|
|
1042
1125
|
params.push(occurred.to);
|
|
1043
1126
|
}
|
|
1044
1127
|
// v0.8.0 A3(issue #17):strictScope 硬过滤(memory_list 分页路径)。
|
|
1045
|
-
// SQL 与 service.isVisibleInScope 同口径:未标注(NULL)
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1128
|
+
// SQL 与 service.isVisibleInScope 同口径:未标注(NULL)恒可见;v0.8.1 起
|
|
1129
|
+
// 硬墙只认 explicit 来源(4.3),当前维度解析不到时该维度只放行未标注/
|
|
1130
|
+
// 非显式行(fail-closed 收窄)。只有显式传 visibility 的用户面调用才
|
|
1131
|
+
// 过滤——dream/质量过滤等内部 store.list 调用不受影响。
|
|
1048
1132
|
if (visibility) {
|
|
1133
|
+
// v0.8.1 第 3 步(issue #170 4.3):硬过滤只认显式声明——与
|
|
1134
|
+
// service.isVisibleInScope 同口径。IS NOT 是 NULL 安全比较:来源为
|
|
1135
|
+
// auto/NULL(v0.8.0 存量自动标注)的行不吃硬墙,只走 A2 软加权。
|
|
1049
1136
|
if (visibility.agentScope != null) {
|
|
1050
|
-
clauses.push("(agent_scope IS NULL OR agent_scope = ?)");
|
|
1137
|
+
clauses.push("(agent_scope IS NULL OR agent_scope_source IS NOT 'explicit' OR agent_scope = ?)");
|
|
1051
1138
|
params.push(visibility.agentScope);
|
|
1052
1139
|
} else {
|
|
1053
|
-
clauses.push("agent_scope IS NULL");
|
|
1140
|
+
clauses.push("(agent_scope IS NULL OR agent_scope_source IS NOT 'explicit')");
|
|
1054
1141
|
}
|
|
1055
1142
|
if (visibility.workspaceScope != null) {
|
|
1056
|
-
clauses.push("(workspace_scope IS NULL OR workspace_scope = ?)");
|
|
1143
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope_source IS NOT 'explicit' OR workspace_scope = ?)");
|
|
1057
1144
|
params.push(visibility.workspaceScope);
|
|
1058
1145
|
} else {
|
|
1059
|
-
clauses.push("workspace_scope IS NULL");
|
|
1146
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope_source IS NOT 'explicit')");
|
|
1060
1147
|
}
|
|
1061
1148
|
}
|
|
1062
1149
|
if (!includeForgotten) {
|
|
@@ -1615,7 +1702,14 @@ export function createStore(path) {
|
|
|
1615
1702
|
* order is normalized (sorted by id) so the same two memories are only ever
|
|
1616
1703
|
* pending once — a re-detection in a later dream run is a no-op, never a
|
|
1617
1704
|
* duplicate queue entry. Returns the pending row (freshly inserted, or the
|
|
1618
|
-
* existing unresolved row when the pair is already pending)
|
|
1705
|
+
* existing unresolved row when the pair is already pending), or undefined
|
|
1706
|
+
* when the re-detection is suppressed.
|
|
1707
|
+
*
|
|
1708
|
+
* v0.8.1(issue #170 复核项 4):人工已裁决过的对(resolved_at 非空)不再
|
|
1709
|
+
* 反复入队——除非任一侧在裁决之后又被改过(updated_at 晚于 resolved_at)。
|
|
1710
|
+
* 否则跨 scope 对每轮 sleep 都原样回来,把「保留双方」的决定变成噪声。
|
|
1711
|
+
* updated_at 是宽松代理(任何字段更新都会触发重新评审)——宁可多看一眼,
|
|
1712
|
+
* 不静音真冲突。
|
|
1619
1713
|
*/
|
|
1620
1714
|
function saveConflictPending({ run_id, memory_a, memory_b, reason }) {
|
|
1621
1715
|
const [a, b] = [memory_a, memory_b].sort();
|
|
@@ -1623,6 +1717,15 @@ export function createStore(path) {
|
|
|
1623
1717
|
"SELECT * FROM conflict_pending WHERE memory_a = ? AND memory_b = ? AND resolved_at IS NULL LIMIT 1"
|
|
1624
1718
|
).get(a, b);
|
|
1625
1719
|
if (existing) return toConflictPending(existing);
|
|
1720
|
+
const reviewed = db.prepare(
|
|
1721
|
+
"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"
|
|
1722
|
+
).get(a, b);
|
|
1723
|
+
if (reviewed) {
|
|
1724
|
+
const touched = db.prepare(
|
|
1725
|
+
"SELECT count(*) AS c FROM memories WHERE id IN (?, ?) AND updated_at > ?"
|
|
1726
|
+
).get(a, b, reviewed.resolved_at).c;
|
|
1727
|
+
if (touched === 0) return undefined;
|
|
1728
|
+
}
|
|
1626
1729
|
const id = randomUUID();
|
|
1627
1730
|
const now = nowIso();
|
|
1628
1731
|
db.prepare(
|
|
@@ -1668,6 +1771,44 @@ export function createStore(path) {
|
|
|
1668
1771
|
).get().c;
|
|
1669
1772
|
}
|
|
1670
1773
|
|
|
1774
|
+
/**
|
|
1775
|
+
* v0.8.1 底座(issue #170):scope 归属人工修正的审计行。record 描述「一次
|
|
1776
|
+
* 决策后的完整状态」(prev_* 为改前、next_* 为改后,NULL=global/未标注),
|
|
1777
|
+
* 由调用方(service.updateMemory)在 store.update 成功后写入;本函数不做
|
|
1778
|
+
* 业务校验,只保证审计落库失败不反噬主写入(异常上抛由调用方吞掉记 warn)。
|
|
1779
|
+
*/
|
|
1780
|
+
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 }) {
|
|
1781
|
+
const id = randomUUID();
|
|
1782
|
+
db.prepare(
|
|
1783
|
+
`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)
|
|
1784
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1785
|
+
).run(
|
|
1786
|
+
id,
|
|
1787
|
+
memory_id,
|
|
1788
|
+
actor === "panel" ? "panel" : "tool",
|
|
1789
|
+
normalizeScopeText(prev_agent_scope),
|
|
1790
|
+
normalizeScopeText(prev_workspace_scope),
|
|
1791
|
+
normalizeScopeText(next_agent_scope),
|
|
1792
|
+
normalizeScopeText(next_workspace_scope),
|
|
1793
|
+
normalizeScopeSource(agent_scope_source),
|
|
1794
|
+
normalizeScopeSource(workspace_scope_source),
|
|
1795
|
+
normalizeOccurredAt(decided_at) ?? nowIso()
|
|
1796
|
+
);
|
|
1797
|
+
return toScopeChange(db.prepare("SELECT * FROM scope_changes WHERE id = ?").get(id));
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/** Scope 修正审计(单条记忆,新→旧)。审计回放与候选复核用。 */
|
|
1801
|
+
function listScopeChanges(memoryId, { limit = 50 } = {}) {
|
|
1802
|
+
const lim = Number.isInteger(limit) && limit > 0 ? Math.min(limit, 200) : 50;
|
|
1803
|
+
// tiebreaker 用 rowid(追加写入的单调序)而非 id:decided_at 只有毫秒精度,
|
|
1804
|
+
// 连续两次修正落在同一毫秒时 UUID 排序是随机的(CI windows/node24 抓到的
|
|
1805
|
+
// 真实 flake)——审计回放要求「最新一条」确定。
|
|
1806
|
+
const rows = db.prepare(
|
|
1807
|
+
"SELECT * FROM scope_changes WHERE memory_id = ? ORDER BY decided_at DESC, rowid DESC LIMIT ?"
|
|
1808
|
+
).all(memoryId, lim);
|
|
1809
|
+
return rows.map(toScopeChange);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1671
1812
|
function getFailureStats({ since } = {}) {
|
|
1672
1813
|
const clause = since ? "WHERE created_at >= ?" : "";
|
|
1673
1814
|
const params = since ? [since] : [];
|
|
@@ -2133,6 +2274,8 @@ export function createStore(path) {
|
|
|
2133
2274
|
listConflictPending,
|
|
2134
2275
|
resolveConflictPending,
|
|
2135
2276
|
countConflictPending,
|
|
2277
|
+
saveScopeChange,
|
|
2278
|
+
listScopeChanges,
|
|
2136
2279
|
createEntity,
|
|
2137
2280
|
findEntityByName,
|
|
2138
2281
|
findEntityById,
|
package/src/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,8 +27,12 @@ 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 },
|
|
@@ -75,6 +79,26 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
75
79
|
// flag 关闭时恒返回 null——写入不标注,检索不加权,行为与 A1 前完全一致。
|
|
76
80
|
// logger 透传:registry 反查失败时 warnOnce 才有出口(否则静默降级无观测)。
|
|
77
81
|
const resolveSessionScope = createScopeResolver({ ctx, config, logger: ctx.logger });
|
|
82
|
+
|
|
83
|
+
// 复核项 4(issue #170):strictScope 下他 scope(explicit)的行对工具侧按
|
|
84
|
+
// 「不存在」处理——update/delete 与 memory_get 同款无存在性泄漏。strictScope
|
|
85
|
+
// 关、或会话身份取不到(scope 为 null)时恒可见(与 get 的短路语义一致)。
|
|
86
|
+
const visibleUnderStrictScope = (memory, exec) => {
|
|
87
|
+
if (config?.strictScope !== true) return true;
|
|
88
|
+
const scope = resolveSessionScope(exec);
|
|
89
|
+
return !scope || service.isVisibleInScope(memory, scope);
|
|
90
|
+
};
|
|
91
|
+
// 在调用点以「展开恒为 undefined」的形态前置校验(抛错即中止写入)。
|
|
92
|
+
const assertVisibleForTool = (id, exec) => {
|
|
93
|
+
const memory = id ? service.getById(id) : undefined;
|
|
94
|
+
if (!visibleUnderStrictScope(memory, exec)) throw new Error("memory not found");
|
|
95
|
+
return undefined;
|
|
96
|
+
};
|
|
97
|
+
// delete 侧需要目标行本身判断存在性(不可见 = 视作不存在,不抛错)。
|
|
98
|
+
const visibleForTool = (id, exec) => {
|
|
99
|
+
const memory = id ? service.getById(id) : undefined;
|
|
100
|
+
return visibleUnderStrictScope(memory, exec) ? memory : undefined;
|
|
101
|
+
};
|
|
78
102
|
const tools = [
|
|
79
103
|
defineTool({
|
|
80
104
|
name: "memory_save",
|
|
@@ -90,7 +114,9 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
90
114
|
importance: { type: "integer", description: "1-5; >= threshold auto-injects into future sessions" },
|
|
91
115
|
source: { type: "string", description: "Optional provenance" },
|
|
92
116
|
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." }
|
|
117
|
+
occurred_at: { type: "string", description: "Optional ISO-8601 instant the remembered event happened (differs from write time). Invalid values are ignored." },
|
|
118
|
+
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." },
|
|
119
|
+
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
120
|
},
|
|
95
121
|
output: {
|
|
96
122
|
schema: {
|
|
@@ -104,10 +130,22 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
104
130
|
render: (_args, value) => TEXT_OUTPUT(`memory ${value.action}: ${value.id}`)
|
|
105
131
|
},
|
|
106
132
|
async execute(args, exec) {
|
|
107
|
-
// scope
|
|
108
|
-
//
|
|
109
|
-
//
|
|
133
|
+
// scope 标注(v0.8.1 底座,issue #170):默认由会话身份解析(载体自动
|
|
134
|
+
// 标注,盖 auto 章);显式 agent_scope/workspace_scope 参数逐维覆盖并盖
|
|
135
|
+
// explicit 章。解析绝不抛错、取不到落 NULL。
|
|
136
|
+
// 复核项 2(#170):scopeEnabled 只门控「自动标注」分支——显式声明与
|
|
137
|
+
// 读取身份(search/list/get/注入经同一解析器)不受 flag 限制,否则
|
|
138
|
+
// strictScope 硬墙会被 flag 静默拆掉(fail-open)。
|
|
139
|
+
// (存储 NULL,由 source 列与「从未标注」区分)。非字符串参数在工具
|
|
140
|
+
// 入参 schema 层即被拒(type: "string")——脏值到不了归一化,无静默放宽。
|
|
110
141
|
const scope = resolveSessionScope(exec);
|
|
142
|
+
const autoStamping = config?.scopeEnabled === true;
|
|
143
|
+
const agentLabel = args.agent_scope !== undefined
|
|
144
|
+
? { value: normalizeExplicitScope(args.agent_scope), source: "explicit" }
|
|
145
|
+
: autoStamping && scope ? { value: scope.agent_scope, source: "auto" } : null;
|
|
146
|
+
const workspaceLabel = args.workspace_scope !== undefined
|
|
147
|
+
? { value: normalizeExplicitScope(args.workspace_scope), source: "explicit" }
|
|
148
|
+
: autoStamping && scope ? { value: scope.workspace_scope, source: "auto" } : null;
|
|
111
149
|
const { action, memory } = service.saveWithDedupe({
|
|
112
150
|
type: args.type,
|
|
113
151
|
title: args.title,
|
|
@@ -115,7 +153,8 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
115
153
|
tags: args.tags ?? [],
|
|
116
154
|
importance: args.importance ?? 3,
|
|
117
155
|
source: args.source ?? "tool",
|
|
118
|
-
...(
|
|
156
|
+
...(agentLabel ? { agent_scope: agentLabel.value, agent_scope_source: agentLabel.source } : {}),
|
|
157
|
+
...(workspaceLabel ? { workspace_scope: workspaceLabel.value, workspace_scope_source: workspaceLabel.source } : {}),
|
|
119
158
|
...(args.sensitivity !== undefined ? { sensitivity: args.sensitivity } : {}),
|
|
120
159
|
...(args.occurred_at !== undefined ? { occurred_at: args.occurred_at } : {})
|
|
121
160
|
});
|
|
@@ -288,7 +327,9 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
288
327
|
type: { type: "string", enum: ["preference", "project", "decision", "history", "rejected_solution", "pitfall", "constraint"] },
|
|
289
328
|
tags: { type: "array", items: { type: "string" } },
|
|
290
329
|
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" }
|
|
330
|
+
reason: { type: "string", description: "Optional context for the correction (what the user actually said/wanted), recorded for reflection" },
|
|
331
|
+
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)." },
|
|
332
|
+
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
333
|
},
|
|
293
334
|
output: {
|
|
294
335
|
schema: {
|
|
@@ -314,7 +355,12 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
314
355
|
content: args.content,
|
|
315
356
|
type: args.type,
|
|
316
357
|
tags: args.tags,
|
|
317
|
-
importance: args.importance
|
|
358
|
+
importance: args.importance,
|
|
359
|
+
// 复核项 4(#170):strictScope 下他 scope 的行按「不存在」处理(与 get 对齐),
|
|
360
|
+
// 展开恒为 undefined,仅承担写入前校验。
|
|
361
|
+
...(assertVisibleForTool(args.id, arguments[1])),
|
|
362
|
+
...(args.agent_scope !== undefined ? { agent_scope: args.agent_scope } : {}),
|
|
363
|
+
...(args.workspace_scope !== undefined ? { workspace_scope: args.workspace_scope } : {})
|
|
318
364
|
}, { query: args.reason });
|
|
319
365
|
return { memory: { id: memory.id, title: memory.title, content: memory.content } };
|
|
320
366
|
}
|
|
@@ -335,9 +381,10 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
335
381
|
render: (_args, value) => TEXT_OUTPUT(value.deleted ? "Memory deleted." : "Memory not found.")
|
|
336
382
|
},
|
|
337
383
|
async execute(args) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
384
|
+
// 复核项 4(#170):不可见(explicit 他 scope)= 视作不存在,无存在性泄漏。
|
|
385
|
+
const target = visibleForTool(args.id, arguments[1]);
|
|
386
|
+
if (target) service.remove(args.id);
|
|
387
|
+
return { deleted: !!target };
|
|
341
388
|
}
|
|
342
389
|
}),
|
|
343
390
|
|