dsh-prime-memory 0.11.0 → 0.12.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.
Files changed (47) hide show
  1. package/CHANGELOG.en.md +36 -1
  2. package/CHANGELOG.ja.md +36 -1
  3. package/CHANGELOG.ko.md +36 -1
  4. package/CHANGELOG.md +40 -0
  5. package/README.en.md +28 -0
  6. package/README.ja.md +25 -0
  7. package/README.ko.md +25 -0
  8. package/README.md +22 -0
  9. package/dist/client.js +54 -30
  10. package/dist/config.d.ts +39 -1
  11. package/dist/config.js +16 -0
  12. package/dist/conflict-service.d.ts +38 -0
  13. package/dist/conflict-service.js +65 -0
  14. package/dist/graph/search.d.ts +15 -0
  15. package/dist/graph/search.js +54 -1
  16. package/dist/hooks/recall.js +4 -0
  17. package/dist/index.d.ts +28 -1
  18. package/dist/index.js +12 -3
  19. package/dist/pipeline/l1.d.ts +7 -1
  20. package/dist/pipeline/l1.js +165 -20
  21. package/dist/pipeline/runner.js +6 -1
  22. package/dist/prompts/l1-dedup.d.ts +22 -1
  23. package/dist/prompts/l1-dedup.js +61 -4
  24. package/dist/stats.d.ts +12 -0
  25. package/dist/stats.js +222 -33
  26. package/dist/store/conflicts.d.ts +62 -0
  27. package/dist/store/conflicts.js +69 -0
  28. package/dist/store/graph-store.d.ts +72 -1
  29. package/dist/store/graph-store.js +165 -1
  30. package/dist/store/l1.d.ts +97 -4
  31. package/dist/store/l1.js +163 -21
  32. package/dist/store/receipts.d.ts +157 -0
  33. package/dist/store/receipts.js +139 -0
  34. package/dist/store/search-utils.d.ts +15 -0
  35. package/dist/store/search-utils.js +20 -0
  36. package/dist/store/session-modes.d.ts +7 -0
  37. package/dist/store/session-modes.js +9 -0
  38. package/dist/store/sqlite.d.ts +116 -8
  39. package/dist/store/sqlite.js +366 -46
  40. package/dist/store/vec-utils.d.ts +16 -0
  41. package/dist/store/vec-utils.js +24 -0
  42. package/dist/tools/index.js +204 -31
  43. package/dist/types.d.ts +48 -0
  44. package/dist/types.js +40 -0
  45. package/dist/workspace.d.ts +46 -0
  46. package/dist/workspace.js +105 -0
  47. package/package.json +9 -3
package/dist/config.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import Schema from '@deepseek-ai/schemastery';
8
8
  import type { LayerRouteKey, StaticFallbackEntry } from './contract.js';
9
- import type { ExtractMode } from './types.js';
9
+ import type { ExtractMode, ScopeMode } from './types.js';
10
10
  /**
11
11
  * 蒸馏思考档位全词汇表(唯一事实源):'' = 自动(模型默认档 → high),
12
12
  * 其余为各适配器通用档位词汇(deepseek 认 'off',OpenAI 系是 'none')。
@@ -18,6 +18,10 @@ export interface MemoryConfig {
18
18
  dataDir: string;
19
19
  /** 新会话的默认记忆档位:auto(双族自动)| chat(个人)| work(工作)。 */
20
20
  family: ExtractMode;
21
+ /** §E 存储作用域(可见范围):`global`(默认,跨工作区可见)| `workspace`(按工作区隔离 work 族)。
22
+ * 与 `family` **正交**——前者问"这是什么内容",后者问"它该在多大范围内可见"(ADR-0008 条 1)。
23
+ * 默认 `global` 保证既有部署零漂移:检索侧"是否带工作区标识"即开关,不带即不过滤。 */
24
+ scope: ScopeMode;
21
25
  capture: {
22
26
  enabled: boolean;
23
27
  /** 助手消息是否剥离代码块(减少嵌入噪声)。 */
@@ -56,6 +60,20 @@ export interface MemoryConfig {
56
60
  * 同时为真才执行;图谱是 L1 的可重建投影,关闭不影响记忆主链路。 */
57
61
  enabled: boolean;
58
62
  };
63
+ /** §C 矛盾冻结:去重判定"两边都像是对的、机器判不了"时不自动裁决,
64
+ * 把冲突对停放到待人工裁决区(conflict_pending)。**默认关**——
65
+ * 冻结消耗人的注意力,不可默认全开。 */
66
+ conflictFreeze: {
67
+ /** 总开关。关闭时去重 prompt 与改动前**逐字一致**,冲突分支结构性不可达。 */
68
+ enabled: boolean;
69
+ /** 待裁决队列上限(条)。未裁决数达上限时**不再停放**,直接按 LLM 的
70
+ * winner/loser 自动了结(仍写 conflict_pending,`resolution='auto'`)。
71
+ * 语义是「不收新的」而非「偷偷删旧的」——有界性由此结构性成立。 */
72
+ maxPending: number;
73
+ /** 超时降级(天):停放超过该天数的待裁决对在下一轮蒸馏开头被自动了结。
74
+ * **0 = 不做超时降级**(显式关闭,而非"立刻全部超时")。 */
75
+ timeoutDays: number;
76
+ };
59
77
  recall: {
60
78
  enabled: boolean;
61
79
  /** 每步自动召回注入的 L1 条数。 */
@@ -160,6 +178,7 @@ export interface MemoryConfig {
160
178
  export declare const memorySchema: Schema<Schemastery.ObjectS<{
161
179
  dataDir: Schema<string, string>;
162
180
  family: Schema<"chat" | "work" | "auto", "chat" | "work" | "auto">;
181
+ scope: Schema<string, string>;
163
182
  capture: Schema<Schemastery.ObjectS<{
164
183
  enabled: Schema<boolean, boolean>;
165
184
  stripCodeBlocks: Schema<boolean, boolean>;
@@ -205,6 +224,15 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
205
224
  }>, Schemastery.ObjectT<{
206
225
  enabled: Schema<boolean, boolean>;
207
226
  }>>;
227
+ conflictFreeze: Schema<Schemastery.ObjectS<{
228
+ enabled: Schema<boolean, boolean>;
229
+ maxPending: Schema<number, number>;
230
+ timeoutDays: Schema<number, number>;
231
+ }>, Schemastery.ObjectT<{
232
+ enabled: Schema<boolean, boolean>;
233
+ maxPending: Schema<number, number>;
234
+ timeoutDays: Schema<number, number>;
235
+ }>>;
208
236
  recall: Schema<Schemastery.ObjectS<{
209
237
  enabled: Schema<boolean, boolean>;
210
238
  maxResults: Schema<number, number>;
@@ -421,6 +449,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
421
449
  }>, Schemastery.ObjectT<{
422
450
  dataDir: Schema<string, string>;
423
451
  family: Schema<"chat" | "work" | "auto", "chat" | "work" | "auto">;
452
+ scope: Schema<string, string>;
424
453
  capture: Schema<Schemastery.ObjectS<{
425
454
  enabled: Schema<boolean, boolean>;
426
455
  stripCodeBlocks: Schema<boolean, boolean>;
@@ -466,6 +495,15 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
466
495
  }>, Schemastery.ObjectT<{
467
496
  enabled: Schema<boolean, boolean>;
468
497
  }>>;
498
+ conflictFreeze: Schema<Schemastery.ObjectS<{
499
+ enabled: Schema<boolean, boolean>;
500
+ maxPending: Schema<number, number>;
501
+ timeoutDays: Schema<number, number>;
502
+ }>, Schemastery.ObjectT<{
503
+ enabled: Schema<boolean, boolean>;
504
+ maxPending: Schema<number, number>;
505
+ timeoutDays: Schema<number, number>;
506
+ }>>;
469
507
  recall: Schema<Schemastery.ObjectS<{
470
508
  enabled: Schema<boolean, boolean>;
471
509
  maxResults: Schema<number, number>;
package/dist/config.js CHANGED
@@ -16,6 +16,12 @@ export const EFFORT_CHOICES = ['', 'off', 'none', 'minimal', 'low', 'medium', 'h
16
16
  export const memorySchema = Schema.object({
17
17
  dataDir: Schema.string().default(''),
18
18
  family: Schema.union(['auto', 'chat', 'work']).default('auto'),
19
+ // §E 可见范围:默认 global(既有部署不传该键 = 行为与改动前逐字一致)。
20
+ // **实测**(schemastery):`Schema.union` 对非法值**抛错**——`$.x expected "a" | "b" but got "bogus"`;
21
+ // `Schema.string()` 则原样透传。ADR-0008 条 4 要求「解析失败不阻断启动」
22
+ // (历史上 `nullable` 崩溃整棵插件树的教训在案),故此处用 string + 消费侧 `normScope` 归一。
23
+ // ⚠️ 既有 `family` 仍是 union,存在同款风险(传 'bogus' 会抛)——已登记 findings §17,不在本波修。
24
+ scope: Schema.string().default('global'),
19
25
  capture: Schema.object({
20
26
  enabled: Schema.boolean().default(true),
21
27
  stripCodeBlocks: Schema.boolean().default(true),
@@ -42,6 +48,16 @@ export const memorySchema = Schema.object({
42
48
  graph: Schema.object({
43
49
  enabled: Schema.boolean().default(false),
44
50
  }),
51
+ // §C 矛盾冻结:默认关(新功能默认关)。开启后去重决策词表多出 conflict 动作,
52
+ // 冲突对停放待人工裁决,不再由 LLM 直接 update/merge 覆盖。
53
+ conflictFreeze: Schema.object({
54
+ enabled: Schema.boolean().default(false),
55
+ // 上限给"人会看"留出余量:100 条待裁决 ≈ 连续 5~20 轮蒸馏全在冲突,
56
+ // 远超正常使用强度;真达到说明该调 prompt 而不是加容量。
57
+ maxPending: Schema.number().min(0).max(10_000).default(100),
58
+ // 30 天:足够跨过假期与项目间歇,又不至于让互相矛盾的两条记忆长期并列召回。
59
+ timeoutDays: Schema.number().min(0).max(3650).default(30),
60
+ }),
45
61
  recall: Schema.object({
46
62
  enabled: Schema.boolean().default(true),
47
63
  maxResults: Schema.number().min(1).max(20).default(5),
@@ -0,0 +1,38 @@
1
+ /**
2
+ * §C 矛盾冻结的**裁决出口**(task_25)。
3
+ *
4
+ * 冻结把裁决权交还给人,那么必须有一个"人能把结论说回去"的出口:
5
+ * 否则待裁决队列是只进不出的黑洞,安全阀(task_24)的自动了结会成为唯一出路——
6
+ * 那等于把 opt-in 的冻结悄悄退回成"超时后机器自己判"。
7
+ *
8
+ * 工具层与 RPC 层共用本模块,保证两条出口的**语义与返回形状完全一致**
9
+ * (与 `memory_receipts` 一样:同一份 `ReceiptsView` 供工具与端点共用)。
10
+ */
11
+ import type { L1Store } from './store/l1.js';
12
+ /** 裁决结果的对外形状(snake_case,工具与端点共用)。 */
13
+ export interface ConflictResolutionView {
14
+ pair_id: string;
15
+ outcome: string;
16
+ /** 裁决时刻(ISO)。空串 = 未生效。 */
17
+ resolved_at: string;
18
+ /** 因裁决从检索中退场的记录 id(无则空串)。 */
19
+ removed_record_id: string;
20
+ notice?: string;
21
+ }
22
+ export interface ConflictResolveDeps {
23
+ l1: Pick<L1Store, 'listConflictPending' | 'resolveConflictPending' | 'deleteBatch' | 'syncGraphDisputed'>;
24
+ /** `conflictFreeze.enabled`。未开启时队列恒空,直接给出提示而非静默无操作。 */
25
+ conflictFreezeEnabled: boolean;
26
+ }
27
+ /**
28
+ * 裁决一条待裁决对。
29
+ *
30
+ * 顺序刻意如此:
31
+ * ① **先打 `resolved_at` 再退场 loser**。反过来的话,退场成功但打标失败会留下
32
+ * "记录已消失、队列里那条仍在待裁决"的状态——人再点一次才发现无据可依。
33
+ * 打标用 `WHERE resolved_at = ''`,天然防重复裁决:第二次调用拿到 0 行即中止。
34
+ * ② 退场后才**重算**图谱 `disputed`(派生字段必须由当前事实重算,见 `syncDisputed`)。
35
+ */
36
+ export declare function resolveConflictPair(deps: ConflictResolveDeps, pairId: string, outcome: string): Promise<ConflictResolutionView>;
37
+ /** 裁决结果的人类可读渲染(工具路径用)。schema 产出的是可选字段,故按部分取值渲染。 */
38
+ export declare function renderConflictResolution(v: Partial<ConflictResolutionView>): string;
@@ -0,0 +1,65 @@
1
+ const OUTCOMES = ['winner', 'loser', 'both'];
2
+ function view(partial) {
3
+ return { pair_id: '', outcome: '', resolved_at: '', removed_record_id: '', ...partial };
4
+ }
5
+ /**
6
+ * 裁决一条待裁决对。
7
+ *
8
+ * 顺序刻意如此:
9
+ * ① **先打 `resolved_at` 再退场 loser**。反过来的话,退场成功但打标失败会留下
10
+ * "记录已消失、队列里那条仍在待裁决"的状态——人再点一次才发现无据可依。
11
+ * 打标用 `WHERE resolved_at = ''`,天然防重复裁决:第二次调用拿到 0 行即中止。
12
+ * ② 退场后才**重算**图谱 `disputed`(派生字段必须由当前事实重算,见 `syncDisputed`)。
13
+ */
14
+ export async function resolveConflictPair(deps, pairId, outcome) {
15
+ const clean = outcome.trim();
16
+ if (!OUTCOMES.includes(clean)) {
17
+ return view({
18
+ pair_id: pairId,
19
+ outcome: clean,
20
+ notice: `outcome 必须是 ${OUTCOMES.join(' / ')} 之一:winner(LLM 建议的胜方为真)、loser(败方为真)、both(两者其实是各自独立的事实,都保留)。`,
21
+ });
22
+ }
23
+ if (!deps.conflictFreezeEnabled) {
24
+ return view({
25
+ pair_id: pairId,
26
+ outcome: clean,
27
+ notice: '矛盾冻结未开启(conflictFreeze.enabled=false):没有待裁决对,无从裁决。',
28
+ });
29
+ }
30
+ const pair = deps.l1.listConflictPending().find((p) => p.pairId === pairId);
31
+ if (!pair) {
32
+ return view({
33
+ pair_id: pairId,
34
+ outcome: clean,
35
+ notice: '找不到该待裁决对:pair_id 有误,或它已被裁决(resolved_at 非空的不再接受二次裁决)。',
36
+ });
37
+ }
38
+ const resolvedAt = new Date().toISOString();
39
+ const removedId = clean === 'winner' ? pair.loserId : clean === 'loser' ? pair.winnerId : '';
40
+ if (deps.l1.resolveConflictPending(pairId, clean, resolvedAt) === 0) {
41
+ // 并发/重复调用:这一对在本次读取与本次写入之间被裁决了
42
+ return view({ pair_id: pairId, outcome: clean, notice: '该对已被裁决,本次未生效(裁决不可覆盖)。' });
43
+ }
44
+ if (removedId)
45
+ await deps.l1.deleteBatch([removedId]);
46
+ // 图谱 disputed 重算:此刻仍未裁决的对才是争议集,已了结的节点自动复原 active
47
+ const ids = new Set();
48
+ for (const p of deps.l1.listConflictPending()) {
49
+ ids.add(p.winnerId);
50
+ ids.add(p.loserId);
51
+ }
52
+ deps.l1.syncGraphDisputed([...ids]);
53
+ return view({ pair_id: pairId, outcome: clean, resolved_at: resolvedAt, removed_record_id: removedId });
54
+ }
55
+ /** 裁决结果的人类可读渲染(工具路径用)。schema 产出的是可选字段,故按部分取值渲染。 */
56
+ export function renderConflictResolution(v) {
57
+ if (v.notice)
58
+ return v.notice;
59
+ const outcome = v.outcome ?? '';
60
+ const label = outcome === 'winner' ? '判定 LLM 建议的胜方为真' : outcome === 'loser' ? '判定败方为真' : '两者都保留(判为各自独立的事实)';
61
+ const removed = v.removed_record_id
62
+ ? `\n退场记录:${v.removed_record_id}(已从检索中移除,事实源保留)`
63
+ : '\n未移除任何记录。';
64
+ return `已裁决待裁决对 ${v.pair_id ?? ''}\n结论:${outcome}(${label})\n裁决时刻:${v.resolved_at ?? ''}${removed}`;
65
+ }
@@ -9,6 +9,21 @@ export declare const GRAPH_FIELD_WEIGHTS: {
9
9
  readonly relations: 3;
10
10
  readonly type: 2;
11
11
  };
12
+ /**
13
+ * 图谱命中 → L1 记录 id 序列(§D 第 3 路 RRF 的输入;纯函数,无 I/O)。
14
+ *
15
+ * 图谱是**可重建派生投影**,节点/fact 的 `sourceRecordIds` 是它回链 L1 的唯一凭证
16
+ * (types.ts:6-7)。此处把"节点排名"翻译成"L1 记录排名":
17
+ *
18
+ * - **保序**:按命中的 score 降序依次展开来源,先出现的记录 = 更高排名;
19
+ * - **去重**:同一 L1 记录可能被多个节点/fact 引用,只占首次出现的位置
20
+ * (重复占位会凭空抬高该记录在 RRF 里的权重);
21
+ * - **节点自身来源优先于其 fact 来源**:节点的 sourceRecordIds 是它的身份来源,
22
+ * 而单个 fact 的来源可能引向与该查询无关的旁支记录;
23
+ * - **只取 active fact**:superseded 是历史状态,不该把旧记录拉进当前检索结果;
24
+ * - **不查库**:返回的 id 可能已被删除(墓碑边界),由调用方按"取不到的跳过"处理。
25
+ */
26
+ export declare function graphHitRecordIds(hits: readonly GraphNodeSearchResult[]): string[];
12
27
  /**
13
28
  * 图谱节点检索:返回按 score 降序的命中(最多 limit 条;limit 钳制 1~20)。
14
29
  * 候选节点 = active | disputed(superseded/archived 不进检索)。
@@ -2,7 +2,25 @@
2
2
  * 图谱节点检索(纯函数,无 I/O):字段加权重叠计分 + 可解释命中输出。
3
3
  *
4
4
  * 与 L1 检索(FTS5/向量)不同,图谱节点量级是"可重建投影"的百~千级,在此
5
- * 直接内存打分即可,不建倒排索引(>2k 节点再考虑预建——见规划可延后清单)。
5
+ * 直接内存打分即可,不建倒排索引(>2k 节点再考虑预建)。
6
+ *
7
+ * ── 图谱演进阶梯(阈值触发,非按需猜测)──────────────────────────────
8
+ * 图谱是 L0 → L1 → 图谱 链上的可重建派生投影(不是事实源),故存储引擎**可随时替换,
9
+ * 且是重建而非迁移**——这是当前不引入图数据库不会锁死将来的核心依据。
10
+ * 本阶梯把"何时才该升级"固化成可判定的阈值,避免提前引入重型方案:
11
+ *
12
+ * Tier 能力 存储 触发阈值 成本
13
+ * T0 邻接表 + 字段加权打分(即本文件) SQLite nodes 10²~10³(现状) 已有
14
+ * T1 TS 内存 BFS/DFS:k-hop/最短路径/连通分量 **不变** nodes ≤ ~10k 纯 TS,零 schema 变更
15
+ * edges ≤ ~100k(=T1 上限)
16
+ * T2 递归 CTE(不物化全图) **不变** 图装不进内存 复杂 SQL + 手工防环
17
+ * T3 闭包表/物化路径(传递闭包预计算) **不变** 高频多跳重复查询 写放大,边变更需维护
18
+ * T4 嵌入式图引擎(Kuzu 优先,非 Neo4j) 换引擎 T1-T3 全部不足 原生依赖,破坏自包含
19
+ *
20
+ * **2k 节点** = 预建倒排索引的检查点(上方既有判断);**10k / 100k** = T1 内存遍历的上限。
21
+ *
22
+ * **注意**:10k 节点 / 100k 边是**按内存占用推算的估算值**(10 万边 × ~100 字节 ≈ 10 MB),
23
+ * **非实测**,待真实规模数据校准。多跳遍历归 TS 纯函数层,**不使用 `WITH RECURSIVE`**。
6
24
  *
7
25
  * 设计要点:
8
26
  * - 分词与 L0/L1 共用 util/text.ts 的 tokenize(jieba 词 ∪ 拉丁词 ∪ CJK 二元组),
@@ -42,6 +60,41 @@ const FIELD_ORDER = ['name', 'aliases', 'tags', 'currentState', 'facts', 'relati
42
60
  function factValueText(value) {
43
61
  return Array.isArray(value) ? value.join(' ') : value;
44
62
  }
63
+ /**
64
+ * 图谱命中 → L1 记录 id 序列(§D 第 3 路 RRF 的输入;纯函数,无 I/O)。
65
+ *
66
+ * 图谱是**可重建派生投影**,节点/fact 的 `sourceRecordIds` 是它回链 L1 的唯一凭证
67
+ * (types.ts:6-7)。此处把"节点排名"翻译成"L1 记录排名":
68
+ *
69
+ * - **保序**:按命中的 score 降序依次展开来源,先出现的记录 = 更高排名;
70
+ * - **去重**:同一 L1 记录可能被多个节点/fact 引用,只占首次出现的位置
71
+ * (重复占位会凭空抬高该记录在 RRF 里的权重);
72
+ * - **节点自身来源优先于其 fact 来源**:节点的 sourceRecordIds 是它的身份来源,
73
+ * 而单个 fact 的来源可能引向与该查询无关的旁支记录;
74
+ * - **只取 active fact**:superseded 是历史状态,不该把旧记录拉进当前检索结果;
75
+ * - **不查库**:返回的 id 可能已被删除(墓碑边界),由调用方按"取不到的跳过"处理。
76
+ */
77
+ export function graphHitRecordIds(hits) {
78
+ const seen = new Set();
79
+ const out = [];
80
+ const take = (id) => {
81
+ if (seen.has(id))
82
+ return;
83
+ seen.add(id);
84
+ out.push(id);
85
+ };
86
+ for (const h of hits) {
87
+ for (const id of h.node.sourceRecordIds)
88
+ take(id);
89
+ for (const f of h.node.facts) {
90
+ if (f.status !== 'active')
91
+ continue;
92
+ for (const id of f.sourceRecordIds ?? [])
93
+ take(id);
94
+ }
95
+ }
96
+ return out;
97
+ }
45
98
  /**
46
99
  * 图谱节点检索:返回按 score 降序的命中(最多 limit 条;limit 钳制 1~20)。
47
100
  * 候选节点 = active | disputed(superseded/archived 不进检索)。
@@ -1,6 +1,7 @@
1
1
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
2
2
  import { RecallDedupeStore } from '../store/recall-dedupe.js';
3
3
  import { OccupancyStore } from '../store/occupancy.js';
4
+ import { scopeFilterOf } from '../workspace.js';
4
5
  import { applyRecallBudget, raceRecallTimeout, RECALL_EMBED_CAP_MS } from '../util/recall-budget.js';
5
6
  import { clearProfileShare, emptyOccupancyLedger, estimateInjectedMessageTokens, estimateStableSectionTokens, recordProfileShare, recordRecallInjection, resetForCompaction, } from '../util/context-occupancy.js';
6
7
  import { errDetail } from '../util/filelog.js';
@@ -164,6 +165,9 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
164
165
  const hits = await raceRecallTimeout(stores.l1.search(query, cfg.recall.maxResults, {
165
166
  scoreThreshold: cfg.recall.scoreThreshold,
166
167
  family: mode === 'auto' ? undefined : mode,
168
+ // §E 可见范围:自动召回是"最没人在看"的一条路径——它静默地往模型上下文里
169
+ // 注入记忆。若这里漏了隔离,跨工作区泄漏会发生在每一次对话里而不留痕迹。
170
+ workspaceId: scopeFilterOf(cfg.scope, { agent: payload.agent }),
167
171
  // 嵌入内层钳制:给 FTS 降级留出总预算内的时间(远程限 HTTP fetch;本地经 worker 代理 race 放弃)
168
172
  embeddingTimeoutMs: RECALL_EMBED_CAP_MS,
169
173
  }), cfg.recall.timeoutMs);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import { type MemoryConfig } from './config.js';
3
3
  export declare const name = "dsh-memory-plugin";
4
- /** 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。 */
4
+ /**
5
+ * 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。
6
+ * 0.1.5 起 connection.rpc.handle() 把 /rpc 路由登记到 webServer 时按**调用方
7
+ * fiber** 校验 inject 授权(0.1.1/0.1.2 由 connection 服务自己持有 webServer,
8
+ * 调用方无需声明)——缺失即 `cannot get property "webServer" without inject`
9
+ * 致命失败。故声明 connection + webServer;两者在 0.1.1~0.1.5 均存在,
10
+ * 额外授权在旧版无害(声明式注入只会等这两个服务就绪,不改装配顺序)。
11
+ */
5
12
  export declare const inject: string[];
6
13
  /**
7
14
  * 插件配置 schema。导出名必须是 `Config`——cordis 运行时只读 plugin.Config
@@ -11,6 +18,7 @@ export declare const inject: string[];
11
18
  export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
12
19
  dataDir: import("@deepseek-ai/schemastery").default<string, string>;
13
20
  family: import("@deepseek-ai/schemastery").default<"chat" | "work" | "auto", "chat" | "work" | "auto">;
21
+ scope: import("@deepseek-ai/schemastery").default<string, string>;
14
22
  capture: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
15
23
  enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
16
24
  stripCodeBlocks: import("@deepseek-ai/schemastery").default<boolean, boolean>;
@@ -56,6 +64,15 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
56
64
  }>, Schemastery.ObjectT<{
57
65
  enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
58
66
  }>>;
67
+ conflictFreeze: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
68
+ enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
69
+ maxPending: import("@deepseek-ai/schemastery").default<number, number>;
70
+ timeoutDays: import("@deepseek-ai/schemastery").default<number, number>;
71
+ }>, Schemastery.ObjectT<{
72
+ enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
73
+ maxPending: import("@deepseek-ai/schemastery").default<number, number>;
74
+ timeoutDays: import("@deepseek-ai/schemastery").default<number, number>;
75
+ }>>;
59
76
  recall: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
60
77
  enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
61
78
  maxResults: import("@deepseek-ai/schemastery").default<number, number>;
@@ -272,6 +289,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
272
289
  }>, Schemastery.ObjectT<{
273
290
  dataDir: import("@deepseek-ai/schemastery").default<string, string>;
274
291
  family: import("@deepseek-ai/schemastery").default<"chat" | "work" | "auto", "chat" | "work" | "auto">;
292
+ scope: import("@deepseek-ai/schemastery").default<string, string>;
275
293
  capture: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
276
294
  enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
277
295
  stripCodeBlocks: import("@deepseek-ai/schemastery").default<boolean, boolean>;
@@ -317,6 +335,15 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
317
335
  }>, Schemastery.ObjectT<{
318
336
  enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
319
337
  }>>;
338
+ conflictFreeze: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
339
+ enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
340
+ maxPending: import("@deepseek-ai/schemastery").default<number, number>;
341
+ timeoutDays: import("@deepseek-ai/schemastery").default<number, number>;
342
+ }>, Schemastery.ObjectT<{
343
+ enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
344
+ maxPending: import("@deepseek-ai/schemastery").default<number, number>;
345
+ timeoutDays: import("@deepseek-ai/schemastery").default<number, number>;
346
+ }>>;
320
347
  recall: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
321
348
  enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
322
349
  maxResults: import("@deepseek-ai/schemastery").default<number, number>;
package/dist/index.js CHANGED
@@ -37,8 +37,15 @@ import { effectiveCfg } from './pipeline/runner.js';
37
37
  import { pendingPathFor } from './store/pending.js';
38
38
  import { initTokenCost, resetTokenCost } from './token-cost.js';
39
39
  export const name = 'dsh-memory-plugin';
40
- /** 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。 */
41
- export const inject = ['llm', 'tools', 'systemPrompt'];
40
+ /**
41
+ * 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。
42
+ * 0.1.5 起 connection.rpc.handle() 把 /rpc 路由登记到 webServer 时按**调用方
43
+ * fiber** 校验 inject 授权(0.1.1/0.1.2 由 connection 服务自己持有 webServer,
44
+ * 调用方无需声明)——缺失即 `cannot get property "webServer" without inject`
45
+ * 致命失败。故声明 connection + webServer;两者在 0.1.1~0.1.5 均存在,
46
+ * 额外授权在旧版无害(声明式注入只会等这两个服务就绪,不改装配顺序)。
47
+ */
48
+ export const inject = ['llm', 'tools', 'systemPrompt', 'connection', 'webServer'];
42
49
  /**
43
50
  * 插件配置 schema。导出名必须是 `Config`——cordis 运行时只读 plugin.Config
44
51
  * (Standard Schema 接口)做校验与默认值填充;导出 `schema` 会被静默忽略,
@@ -128,7 +135,9 @@ export async function apply(ctx, config) {
128
135
  }
129
136
  const stores = {
130
137
  l0: new L0Store(dataDir, db, embed, logger),
131
- l1: new L1Store(dataDir, db, embed, config.recall.strategy, logger, config.recall.decayHalfLifeDays),
138
+ l1: new L1Store(dataDir, db, embed, config.recall.strategy, logger, config.recall.decayHalfLifeDays,
139
+ // §D 第 3 路:图谱回链。图谱不可用时 searchNodes 自带 no-op,不影响双路。
140
+ (query, limit, family) => db.graphStore.searchNodes(query, limit, family ? [family] : undefined)),
132
141
  // L2/L3 分族隔离:各自目录与文件(scenes/chat|work、persona-chat|work.md)
133
142
  scenes: {
134
143
  chat: new SceneStore(dataDir, 'chat', logger),
@@ -16,4 +16,10 @@ export type FamilyStates = Record<MemoryFamily, MemoryState>;
16
16
  * 每条消息按 content 长度 + 64 字符脚手架开销(id/时间戳行)计。
17
17
  */
18
18
  export declare function chunkByCharBudget(messages: ConversationMessage[], budgetChars: number): ConversationMessage[][];
19
- export declare function runExtraction(ctx: Context, cfg: MemoryConfig, store: L1Store, states: FamilyStates, pending: ConversationMessage[], background: ConversationMessage[], logger: MemoryLogger, mode: ExtractMode): Promise<ExtractionResult>;
19
+ export declare function runExtraction(ctx: Context, cfg: MemoryConfig, store: L1Store, states: FamilyStates, pending: ConversationMessage[], background: ConversationMessage[], logger: MemoryLogger, mode: ExtractMode,
20
+ /**
21
+ * §E 当前工作区标识(由调用方经 `sessionWorkspaceIdOf` 解析;拿不到传 undefined)。
22
+ * 传 undefined 时行为与改动前**逐字一致**——`cfg.scope='global'` 的既有部署
23
+ * 永远走这条分支,这是零漂移的构造性保证。
24
+ */
25
+ workspaceId?: string): Promise<ExtractionResult>;