dsh-prime-memory 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +401 -0
- package/CHANGELOG.ja.md +401 -0
- package/CHANGELOG.ko.md +401 -0
- package/CHANGELOG.md +100 -0
- package/dist/client.js +1951 -1188
- package/dist/config.d.ts +176 -1240
- package/dist/config.js +113 -2
- package/dist/conflict-service.d.ts +12 -0
- package/dist/conflict-service.js +24 -0
- package/dist/contract.d.ts +327 -0
- package/dist/domain-gate.d.ts +19 -0
- package/dist/domain-gate.js +45 -0
- package/dist/graph/constraints.d.ts +4 -2
- package/dist/graph/constraints.js +4 -2
- package/dist/hooks/obsolete-notice.d.ts +26 -0
- package/dist/hooks/obsolete-notice.js +64 -0
- package/dist/hooks/recall.d.ts +41 -0
- package/dist/hooks/recall.js +127 -28
- package/dist/hooks/skill-recall.d.ts +25 -0
- package/dist/hooks/skill-recall.js +72 -0
- package/dist/index.d.ts +1 -1029
- package/dist/index.js +55 -2
- package/dist/memory-types.d.ts +69 -0
- package/dist/memory-types.js +165 -0
- package/dist/memport/format.d.ts +75 -0
- package/dist/memport/format.js +345 -0
- package/dist/memport/prefilter.d.ts +62 -0
- package/dist/memport/prefilter.js +132 -0
- package/dist/memport/scan.d.ts +13 -0
- package/dist/memport/scan.js +100 -0
- package/dist/metadata-validators.d.ts +22 -0
- package/dist/metadata-validators.js +45 -0
- package/dist/pipeline/l1.d.ts +44 -2
- package/dist/pipeline/l1.js +177 -9
- package/dist/pipeline/md-sync.d.ts +65 -0
- package/dist/pipeline/md-sync.js +456 -0
- package/dist/pipeline/ruminate.d.ts +46 -1
- package/dist/pipeline/ruminate.js +150 -1
- package/dist/pipeline/runner.d.ts +10 -0
- package/dist/pipeline/runner.js +7 -1
- package/dist/pipeline/slot-migrate.d.ts +45 -0
- package/dist/pipeline/slot-migrate.js +45 -0
- package/dist/pipeline/workspace-relink.d.ts +60 -0
- package/dist/pipeline/workspace-relink.js +73 -0
- package/dist/projection/recall.d.ts +112 -0
- package/dist/projection/recall.js +183 -0
- package/dist/projection/slots.d.ts +5 -0
- package/dist/prompts/examples.d.ts +73 -0
- package/dist/prompts/examples.js +178 -0
- package/dist/prompts/graph-projection.js +3 -0
- package/dist/prompts/l1-dedup.d.ts +9 -3
- package/dist/prompts/l1-dedup.js +34 -13
- package/dist/prompts/l1-extraction.d.ts +9 -4
- package/dist/prompts/l1-extraction.js +76 -8
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +15 -1
- package/dist/skills/crystallize.d.ts +41 -0
- package/dist/skills/crystallize.js +75 -0
- package/dist/stats.d.ts +30 -1
- package/dist/stats.js +504 -13
- package/dist/store/embedding-source.d.ts +8 -0
- package/dist/store/embedding-source.js +39 -2
- package/dist/store/file-versions.d.ts +4 -0
- package/dist/store/file-versions.js +4 -0
- package/dist/store/graph-store.d.ts +14 -0
- package/dist/store/graph-store.js +35 -0
- package/dist/store/l1-snapshot.d.ts +26 -1
- package/dist/store/l1-snapshot.js +53 -8
- package/dist/store/l1.d.ts +77 -3
- package/dist/store/l1.js +128 -3
- package/dist/store/memory-md-archive.d.ts +84 -0
- package/dist/store/memory-md-archive.js +155 -0
- package/dist/store/memory-md-disable.d.ts +23 -0
- package/dist/store/memory-md-disable.js +106 -0
- package/dist/store/memory-md-export.d.ts +50 -0
- package/dist/store/memory-md-export.js +200 -0
- package/dist/store/memory-md-ledger.d.ts +94 -0
- package/dist/store/memory-md-ledger.js +243 -0
- package/dist/store/memory-md-orphan.d.ts +27 -0
- package/dist/store/memory-md-orphan.js +153 -0
- package/dist/store/memory-md-parse.d.ts +35 -0
- package/dist/store/memory-md-parse.js +85 -0
- package/dist/store/memory-md-skeleton.d.ts +18 -0
- package/dist/store/memory-md-skeleton.js +61 -0
- package/dist/store/memory-md-types.d.ts +174 -0
- package/dist/store/memory-md-types.js +10 -0
- package/dist/store/memory-md.d.ts +108 -0
- package/dist/store/memory-md.js +219 -0
- package/dist/store/memory-sources.d.ts +59 -0
- package/dist/store/memory-sources.js +214 -0
- package/dist/store/obsolete-notices.d.ts +39 -0
- package/dist/store/obsolete-notices.js +153 -0
- package/dist/store/recall-feedback.d.ts +75 -0
- package/dist/store/recall-feedback.js +213 -0
- package/dist/store/recall-usage.d.ts +11 -0
- package/dist/store/recall-usage.js +29 -0
- package/dist/store/session-recalled.d.ts +60 -0
- package/dist/store/session-recalled.js +310 -0
- package/dist/store/skill-store.d.ts +99 -0
- package/dist/store/skill-store.js +330 -0
- package/dist/store/sqlite.d.ts +71 -0
- package/dist/store/sqlite.js +259 -15
- package/dist/store/supersede.d.ts +1 -1
- package/dist/store/supersede.js +1 -1
- package/dist/tools/index.d.ts +4 -13
- package/dist/tools/index.js +80 -12
- package/dist/tools/skills.d.ts +13 -0
- package/dist/tools/skills.js +130 -0
- package/dist/types.d.ts +30 -7
- package/dist/types.js +10 -2
- package/dist/util/maturity.d.ts +44 -0
- package/dist/util/maturity.js +43 -0
- package/dist/util/md-name.d.ts +129 -0
- package/dist/util/md-name.js +366 -0
- package/dist/util/md-normalize.d.ts +23 -0
- package/dist/util/md-normalize.js +131 -0
- package/dist/wing-backfill.js +7 -4
- package/dist/workspace-route.d.ts +22 -0
- package/dist/workspace-route.js +66 -0
- package/dsh.plugin.json +1 -1
- package/package.json +6 -5
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* 历史上 Wing 侧零校验,非法值先入库、再由下游剥离,形成无谓的写入-剥离循环。
|
|
11
11
|
*/
|
|
12
12
|
import { WING_CATALOG, WING_FALLBACK } from './types.js';
|
|
13
|
+
import { isCognitiveHall, COG_HALL_METADATA_KEY } from './cognitive-hall.js';
|
|
13
14
|
/** Wing 词表(8 域 + general 兜底):与 relabel 机械校验、提示词候选集同源。 */
|
|
14
15
|
const WING_ID_SET = new Set([
|
|
15
16
|
...WING_CATALOG.map((w) => w.id),
|
|
@@ -66,3 +67,47 @@ export function normTags(v, max = 3) {
|
|
|
66
67
|
return out;
|
|
67
68
|
}
|
|
68
69
|
export { isCognitiveHall, COGNITIVE_HALLS } from './cognitive-hall.js';
|
|
70
|
+
/** usageScenario 单条最大长度(“何时该浮现”的短语,非 slug,故不校 isTag 字符集)。 */
|
|
71
|
+
const USAGE_SCENARIO_MAX_CHARS = 60;
|
|
72
|
+
/**
|
|
73
|
+
* usageScenario 归一 + 去重 + 上限。**所有写回点必须过这里**。
|
|
74
|
+
*
|
|
75
|
+
* 与 normTags 的区别:usageScenario 是自然语言短语(如“修改 profile 配置时”),
|
|
76
|
+
* 不是 slug,所以不做字符集校验,只做 trim/去重/长度钳制/上限。
|
|
77
|
+
* 非数组或空→ `[]`(缺字段中性,不抛——防单条抽取失败波及整切片重试)。
|
|
78
|
+
*/
|
|
79
|
+
export function normUsageScenario(v, max = 3) {
|
|
80
|
+
if (!Array.isArray(v) || max <= 0)
|
|
81
|
+
return [];
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const raw of v) {
|
|
84
|
+
if (typeof raw !== 'string')
|
|
85
|
+
continue;
|
|
86
|
+
const t = raw.trim();
|
|
87
|
+
if (!t || out.includes(t))
|
|
88
|
+
continue;
|
|
89
|
+
out.push(t.slice(0, USAGE_SCENARIO_MAX_CHARS));
|
|
90
|
+
if (out.length >= max)
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 读 **Wing(生活域)**。⚠️ 磁盘键是 `metadata.hall`——历史别名,存的其实是 **wing 值**。
|
|
97
|
+
* 只认词表内的合法 wing(8 角 + general),否则 undefined。
|
|
98
|
+
*
|
|
99
|
+
* 用它取代裸读 `metadata.hall`:从入口上把“wing 轴”与“认知 hall 轴”分开,
|
|
100
|
+
* 防止把认知 hall 值(facts/events/…)误当 wing 读——两轴不串。
|
|
101
|
+
*/
|
|
102
|
+
export function readWing(metadata) {
|
|
103
|
+
const v = metadata?.hall;
|
|
104
|
+
return typeof v === 'string' && isWingId(v) ? v : undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 读 **认知 hall(大类)**。磁盘键 `metadata.cogHall`,与 wing 的磁盘键 `hall` 正交。
|
|
108
|
+
* 只认五个枚举值,否则 undefined(wing 值不当认知 hall 读)。
|
|
109
|
+
*/
|
|
110
|
+
export function readCognitiveHall(metadata) {
|
|
111
|
+
const v = metadata?.[COG_HALL_METADATA_KEY];
|
|
112
|
+
return isCognitiveHall(v) ? v : undefined;
|
|
113
|
+
}
|
package/dist/pipeline/l1.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import type { MemoryConfig } from '../config.js';
|
|
3
3
|
import type { L1Store } from '../store/l1.js';
|
|
4
4
|
import type { MemoryState } from '../store/state.js';
|
|
5
|
-
import type { ConversationAnchor, ConversationMessage, ExtractMode, MemoryFamily, MemoryLogger, MemoryRecord } from '../types.js';
|
|
5
|
+
import type { ConversationAnchor, ConversationMessage, ExtractedMemory, ExtractMode, MemoryFamily, MemoryLogger, MemoryRecord, MemoryScope } from '../types.js';
|
|
6
6
|
export interface ExtractionResult {
|
|
7
7
|
stored: number;
|
|
8
8
|
skipped: boolean;
|
|
@@ -15,6 +15,42 @@ export interface ExtractionResult {
|
|
|
15
15
|
}
|
|
16
16
|
/** 分族 checkpoint 桶(活引用,改动由调用方 save 落盘)。 */
|
|
17
17
|
export type FamilyStates = Record<MemoryFamily, MemoryState>;
|
|
18
|
+
/** 抽取产出 + 管线补上的五个字段(与 Step 1 的 `extracted` 元素同形)。 */
|
|
19
|
+
export type PendingMemory = ExtractedMemory & {
|
|
20
|
+
record_id: string;
|
|
21
|
+
scene_name: string;
|
|
22
|
+
family: MemoryFamily;
|
|
23
|
+
/** §E 归属:write 侧已按 `resolveRecordScope` 算好,`toStoreRecord` 原样带出 */
|
|
24
|
+
scope: MemoryScope;
|
|
25
|
+
workspaceId: string;
|
|
26
|
+
/** 治理归属(ADR-0015 T1.9/T1.10):进管线时一次算定(I-21),写时随 store 行落盘。 */
|
|
27
|
+
repoKeyName: string;
|
|
28
|
+
repoKeyOwner: string;
|
|
29
|
+
applicability: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 合并时的检索轴并集(P0-c)。
|
|
33
|
+
*
|
|
34
|
+
* 此前合并产物只带新记忆的 metadata,targets 的 tags/hall/cogHall/使用场景全丢——
|
|
35
|
+
* 一次合并就把这条记忆从它原来的 Room/Wing 里摘出去,而面板上看不出任何异常(F2)。
|
|
36
|
+
* 这里只并**检索轴**:派生评价(maturity/usage/rating)与内容层字段不在其列。
|
|
37
|
+
*
|
|
38
|
+
* 单值轴(hall/cogHall)两侧互斥时**保留新记忆侧并留痕**,不拼成 `"a,b"`——那会
|
|
39
|
+
* 造出一个不存在的域(与 `isWingId` 的值域约束相冲)。
|
|
40
|
+
*
|
|
41
|
+
* @param self - 新记忆自己的 metadata(可为 undefined)。
|
|
42
|
+
* @param targets - 被合并掉的旧记录们的 metadata。
|
|
43
|
+
* @returns 并集后的 metadata 与若干条“丢了什么”的留痕文案。
|
|
44
|
+
*/
|
|
45
|
+
export declare function mergeAxes(self: Record<string, unknown> | undefined, targets: ReadonlyArray<Record<string, unknown> | undefined>): {
|
|
46
|
+
metadata: Record<string, unknown>;
|
|
47
|
+
notes: string[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* 导出仅供用例:`title → name` 这一跳是本计划的风隙(模型给的名字能不能落成合法身份名),
|
|
51
|
+
* 而驱动整条 LLM 管线会把断言埋进抽取/去重两段的噪声里。与 `mergeAxes` 同一先例。
|
|
52
|
+
*/
|
|
53
|
+
export declare function toStoreRecord(m: PendingMemory, now: number, ts: number, anchorMap?: ReadonlyMap<string, ConversationAnchor>): MemoryRecord;
|
|
18
54
|
/**
|
|
19
55
|
* 按字符预算把消息切成多块(保持顺序,单条超预算独占一块,由 callLLM 兜底截断)。
|
|
20
56
|
* 每条消息按 content 长度 + 64 字符脚手架开销(id/时间戳行)计。
|
|
@@ -32,4 +68,10 @@ workspaceId?: string,
|
|
|
32
68
|
* **缺省时行为与改动前逐字一致**——传入的 `pending` 消息若不带锚点(老数据、
|
|
33
69
|
* 未启用捕获侧打戳),`resolveSourceAnchors` 一律返回 undefined,不写 metadata 键。
|
|
34
70
|
*/
|
|
35
|
-
anchorMap?: ReadonlyMap<string, ConversationAnchor
|
|
71
|
+
anchorMap?: ReadonlyMap<string, ConversationAnchor>,
|
|
72
|
+
/**
|
|
73
|
+
* 合并后的**派生数据搬运钩子**(P0-d):旧 id → 新 id。使用史与人工标注存在
|
|
74
|
+
* 另两个 sidecar 里,而 merge 每次新铸 id——不搬就等于每次合并把这条记忆的
|
|
75
|
+
* `used`/人工打分清零,它会被当成“没用过”而沉底。缺省不搬(行为与改动前一致)。
|
|
76
|
+
*/
|
|
77
|
+
mergeRekey?: (oldId: string, newId: string) => void): Promise<ExtractionResult>;
|
package/dist/pipeline/l1.js
CHANGED
|
@@ -12,10 +12,13 @@ import { callLLM, parseJsonLogged, resolveLayerTokens } from '../llm.js';
|
|
|
12
12
|
import { buildReceipts, newRunId, persistReceiptsSafely } from '../store/receipts.js';
|
|
13
13
|
import { buildConflictPair, conflictRejectId, DEFER_MAX, normalizeClaimKey, normalizeConflictType, occupiesConflictQuota, pendingHardTotal, validateConflictPair, } from '../store/conflicts.js';
|
|
14
14
|
import { formatExtractionPrompt, getExtractMemoriesSystemPrompt } from '../prompts/l1-extraction.js';
|
|
15
|
+
import { normTags, normUsageScenario, readCognitiveHall, readWing } from '../metadata-validators.js';
|
|
16
|
+
import { acceptTitle } from '../util/md-name.js';
|
|
15
17
|
import { formatBatchConflictPrompt, getConflictDetectionSystemPrompt } from '../prompts/l1-dedup.js';
|
|
16
18
|
import { resolveSourceAnchors, withSourceAnchors } from './anchors.js';
|
|
17
19
|
import { familyForType, normPersistence, normScope, resolveRecordFamily, resolveRecordScope, WING_FALLBACK } from '../types.js';
|
|
18
20
|
import { normApplicability, resolveRepoScope } from '../repo-scope.js';
|
|
21
|
+
import { routeWorkspace } from '../workspace-route.js';
|
|
19
22
|
/** 解析 ISO/epoch 时间证据,非法或非正值一律 undefined——不猜测。 */
|
|
20
23
|
function parseTimeEvidence(raw) {
|
|
21
24
|
const t = typeof raw === 'string' ? Date.parse(raw) : typeof raw === 'number' ? raw : Number.NaN;
|
|
@@ -58,7 +61,97 @@ function mergeTemporal(self, targets) {
|
|
|
58
61
|
* 第 4 个参数是 R7 的锚点映射(`L0 消息 id → 会话坐标`)。**传 undefined 时
|
|
59
62
|
* 行为与改动前逐字一致**——老调用方与没有锚点的会话走这条分支。
|
|
60
63
|
*/
|
|
61
|
-
|
|
64
|
+
/**
|
|
65
|
+
* 把抽取侧的 `metadata.usageScenario` 归一到命名空间保留键 `dsh_usage_scenario`
|
|
66
|
+
*(与 dsh_source_anchors 同范式)。**无该键时原样返回**——功能关时 LLM 不产出该键,
|
|
67
|
+
* 记录 metadata 与改动前逐字一致(零漂移)。只随新蒸馏写,不回填存量。
|
|
68
|
+
*/
|
|
69
|
+
function withUsageScenario(meta) {
|
|
70
|
+
if (!meta || !('usageScenario' in meta))
|
|
71
|
+
return meta;
|
|
72
|
+
const out = { ...meta };
|
|
73
|
+
out.dsh_usage_scenario = normUsageScenario(out.usageScenario);
|
|
74
|
+
delete out.usageScenario;
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/** 把“可能是数组的 metadata 值”读成字符串数组(不是数组就当空,不猜)。 */
|
|
78
|
+
function asStrings(v) {
|
|
79
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === 'string' && x.length > 0) : [];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 合并时的检索轴并集(P0-c)。
|
|
83
|
+
*
|
|
84
|
+
* 此前合并产物只带新记忆的 metadata,targets 的 tags/hall/cogHall/使用场景全丢——
|
|
85
|
+
* 一次合并就把这条记忆从它原来的 Room/Wing 里摘出去,而面板上看不出任何异常(F2)。
|
|
86
|
+
* 这里只并**检索轴**:派生评价(maturity/usage/rating)与内容层字段不在其列。
|
|
87
|
+
*
|
|
88
|
+
* 单值轴(hall/cogHall)两侧互斥时**保留新记忆侧并留痕**,不拼成 `"a,b"`——那会
|
|
89
|
+
* 造出一个不存在的域(与 `isWingId` 的值域约束相冲)。
|
|
90
|
+
*
|
|
91
|
+
* @param self - 新记忆自己的 metadata(可为 undefined)。
|
|
92
|
+
* @param targets - 被合并掉的旧记录们的 metadata。
|
|
93
|
+
* @returns 并集后的 metadata 与若干条“丢了什么”的留痕文案。
|
|
94
|
+
*/
|
|
95
|
+
export function mergeAxes(self, targets) {
|
|
96
|
+
const out = { ...(self ?? {}) };
|
|
97
|
+
const notes = [];
|
|
98
|
+
const list = targets.filter((m) => m !== undefined);
|
|
99
|
+
// tags 是 Room 的构成单元(自生长分类):并集去重后**仍过 normTags**,
|
|
100
|
+
// 长度与字符纪律不因合并而放宽。
|
|
101
|
+
const tags = normTags([...asStrings(out.tags), ...list.flatMap((m) => asStrings(m.tags))], 6);
|
|
102
|
+
if (tags.length > 0)
|
|
103
|
+
out.tags = tags;
|
|
104
|
+
// 使用场景:同样并集,但维持≤ 3 的既有上限(多了会灌 prompt)。
|
|
105
|
+
const sc = normUsageScenario([...asStrings(out.dsh_usage_scenario), ...list.flatMap((m) => asStrings(m.dsh_usage_scenario))]);
|
|
106
|
+
if (sc.length > 0)
|
|
107
|
+
out.dsh_usage_scenario = sc;
|
|
108
|
+
// Room 待评审状态要跟着走,否则合并把“待评审”洗成“未标注”、下轮反刍重新入队。
|
|
109
|
+
const cands = normTags([...asStrings(out.roomCandidates), ...list.flatMap((m) => asStrings(m.roomCandidates))], 6);
|
|
110
|
+
if (cands.length > 0)
|
|
111
|
+
out.roomCandidates = cands;
|
|
112
|
+
if (out.roomReview === undefined) {
|
|
113
|
+
const review = list.find((m) => typeof m.roomReview === 'string' && m.roomReview.length > 0)?.roomReview;
|
|
114
|
+
if (review !== undefined)
|
|
115
|
+
out.roomReview = review;
|
|
116
|
+
}
|
|
117
|
+
// 单值轴:新记忆已打标则不抢;没打标且目标侧只一个结论才继承,互斥则留痕不猜。
|
|
118
|
+
const wingMine = readWing(out);
|
|
119
|
+
if (wingMine === undefined) {
|
|
120
|
+
const theirs = [...new Set(list.map((m) => readWing(m)).filter((v) => v !== undefined))];
|
|
121
|
+
if (theirs.length === 1)
|
|
122
|
+
out.hall = theirs[0];
|
|
123
|
+
else if (theirs.length > 1)
|
|
124
|
+
notes.push(`hall 目标侧互斥(${theirs.join('|')}),不猜`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const theirs = new Set(list.map((m) => readWing(m)).filter((v) => v !== undefined));
|
|
128
|
+
theirs.delete(wingMine);
|
|
129
|
+
if (theirs.size > 0)
|
|
130
|
+
notes.push(`hall 保留新记忆侧 ${wingMine},目标侧 ${[...theirs].join('|')} 被舍`);
|
|
131
|
+
}
|
|
132
|
+
const cogMine = readCognitiveHall(out);
|
|
133
|
+
if (cogMine === undefined) {
|
|
134
|
+
// 认知 hall 的值是固定枚举(不是 string),故用 Set 收集而不是 `v is string` 谓词。
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
for (const m of list) {
|
|
137
|
+
const v = readCognitiveHall(m);
|
|
138
|
+
if (v !== undefined)
|
|
139
|
+
seen.add(v);
|
|
140
|
+
}
|
|
141
|
+
if (seen.size === 1)
|
|
142
|
+
out.cogHall = [...seen][0];
|
|
143
|
+
}
|
|
144
|
+
return { metadata: out, notes };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 导出仅供用例:`title → name` 这一跳是本计划的风隙(模型给的名字能不能落成合法身份名),
|
|
148
|
+
* 而驱动整条 LLM 管线会把断言埋进抽取/去重两段的噪声里。与 `mergeAxes` 同一先例。
|
|
149
|
+
*/
|
|
150
|
+
export function toStoreRecord(m, now, ts, anchorMap) {
|
|
151
|
+
// 名称蒸馏(Phase 3):模型当场给的 title 只是**名字候选**,必须过同一个形状闸;
|
|
152
|
+
// 过不了就**不写键**,交给 `L1Store.assignMissingNames`——判据只留一份,
|
|
153
|
+
// 不在这里养第二套“看起来差不多”的名字清洗。
|
|
154
|
+
const titled = m.title === undefined ? undefined : acceptTitle(m.title);
|
|
62
155
|
return {
|
|
63
156
|
id: m.record_id,
|
|
64
157
|
content: m.content,
|
|
@@ -72,7 +165,7 @@ function toStoreRecord(m, now, ts, anchorMap) {
|
|
|
72
165
|
source_message_ids: m.source_message_ids ?? [],
|
|
73
166
|
// R7:锚点与 source_message_ids 同源解析;解析不到就**不写键**(不是空数组),
|
|
74
167
|
// 使无锚点记录与改动前的 metadata 逐字一致。
|
|
75
|
-
metadata: withSourceAnchors(m.metadata, anchorMap === undefined ? undefined : resolveSourceAnchors(m.source_message_ids, anchorMap)),
|
|
168
|
+
metadata: withSourceAnchors(withUsageScenario(m.metadata), anchorMap === undefined ? undefined : resolveSourceAnchors(m.source_message_ids, anchorMap)),
|
|
76
169
|
family: m.family,
|
|
77
170
|
scope: m.scope,
|
|
78
171
|
workspaceId: m.workspaceId,
|
|
@@ -82,6 +175,10 @@ function toStoreRecord(m, now, ts, anchorMap) {
|
|
|
82
175
|
...(m.repoKeyName ? { repoKeyName: m.repoKeyName } : {}),
|
|
83
176
|
...(m.repoKeyOwner ? { repoKeyOwner: m.repoKeyOwner } : {}),
|
|
84
177
|
...(m.applicability ? { applicability: m.applicability } : {}),
|
|
178
|
+
...(titled === undefined ? {} : { name: titled }),
|
|
179
|
+
// 文件身份键不在这里分:**起名只有一个真源**(`L1Store.assignMissingNames`)。
|
|
180
|
+
// 以前这里自己分一份、库里再分一份,两套判据迟早漂移,而漂移的后果是同一个基底
|
|
181
|
+
// 洗出两个名字(文件跳名)或两个记录抢一个名字(整条写不进去)。
|
|
85
182
|
...temporalOf(m.metadata),
|
|
86
183
|
};
|
|
87
184
|
}
|
|
@@ -124,7 +221,13 @@ workspaceId,
|
|
|
124
221
|
* **缺省时行为与改动前逐字一致**——传入的 `pending` 消息若不带锚点(老数据、
|
|
125
222
|
* 未启用捕获侧打戳),`resolveSourceAnchors` 一律返回 undefined,不写 metadata 键。
|
|
126
223
|
*/
|
|
127
|
-
anchorMap
|
|
224
|
+
anchorMap,
|
|
225
|
+
/**
|
|
226
|
+
* 合并后的**派生数据搬运钩子**(P0-d):旧 id → 新 id。使用史与人工标注存在
|
|
227
|
+
* 另两个 sidecar 里,而 merge 每次新铸 id——不搬就等于每次合并把这条记忆的
|
|
228
|
+
* `used`/人工打分清零,它会被当成“没用过”而沉底。缺省不搬(行为与改动前一致)。
|
|
229
|
+
*/
|
|
230
|
+
mergeRekey) {
|
|
128
231
|
if (!cfg.extract.enabled)
|
|
129
232
|
return { stored: 0, skipped: true, sceneName: chainHead(states, mode), newRecords: [] };
|
|
130
233
|
// 触发阈值(渐进爬坡 + 按会话切片计数)由 runner 判定(trigger.ts);
|
|
@@ -195,7 +298,14 @@ anchorMap) {
|
|
|
195
298
|
halls,
|
|
196
299
|
});
|
|
197
300
|
const raw = await callLLM(ctx, cfg, {
|
|
198
|
-
system: getExtractMemoriesSystemPrompt(mode
|
|
301
|
+
system: getExtractMemoriesSystemPrompt(mode, {
|
|
302
|
+
usageScenario: cfg.usageScenario?.enabled === true,
|
|
303
|
+
narrative: cfg.narrativeTemplates?.enabled === true,
|
|
304
|
+
// 名称蒸馏(Phase 3):示例段只在总开关开着时付(它与 title 是两层成本,
|
|
305
|
+
// 分开开关才能 A/B)。关着时这份 opts 产出的提示与改动前逐字相同。
|
|
306
|
+
title: cfg.naming?.enabled === true,
|
|
307
|
+
fewShot: cfg.naming?.enabled === true && cfg.naming.fewShot === true,
|
|
308
|
+
}),
|
|
199
309
|
user: userPrompt,
|
|
200
310
|
maxTokens: resolveLayerTokens(cfg, 'extract'),
|
|
201
311
|
layer: 'l1-extract',
|
|
@@ -213,14 +323,29 @@ anchorMap) {
|
|
|
213
323
|
if (!m || typeof m.content !== 'string' || !m.content.trim())
|
|
214
324
|
continue;
|
|
215
325
|
const family = resolveRecordFamily(forcedFamily, m.family, m.type ?? '');
|
|
326
|
+
// 内容路由(`cfg.workspaceSource`,默认开):按内容谈到哪个项目定归属。
|
|
327
|
+
// 不走「来源会话反查」:session_id 恒为 default、锚点仅 7.3% 能定位到会话树,
|
|
328
|
+
// 而内容自己说它属于哪个项目——不受这些缺失影响。见 `src/workspace-route.ts`。
|
|
329
|
+
const routed = cfg.workspaceSource?.enabled === true
|
|
330
|
+
? routeWorkspace(`${m.content}\n${scene.scene_name}`, cfg.workspaceSource.routes)
|
|
331
|
+
: undefined;
|
|
216
332
|
extracted.push({
|
|
217
333
|
...m,
|
|
334
|
+
// 开关关着时**不把 title 带进管线**:旧记录形状逐字不变(不新增键、不参与任何判据)。
|
|
335
|
+
...(cfg.naming?.enabled === true && m.title !== undefined && m.title !== null
|
|
336
|
+
? { title: m.title }
|
|
337
|
+
: {}),
|
|
218
338
|
record_id: newId('mem'),
|
|
219
339
|
scene_name: scene.scene_name,
|
|
220
340
|
family,
|
|
221
341
|
// §E 归属在**进管线时**一次算定,下游(store / conflict / update / merge 各分支)
|
|
222
342
|
// 一律复用它——四个分支各算一次是漏判的温床。
|
|
223
343
|
...resolveRecordScope(scopeMode, family, workspaceId),
|
|
344
|
+
// 路由值只覆盖 `workspaceId`、**不动 `scope`**:`resolveRecordScope` 规则③ 在
|
|
345
|
+
// cfg.scope='global' 时故意把归属清成 ''(零漂移保证),这里在其后补一个事实列。
|
|
346
|
+
// 可见性判据 `isScopeVisible` 先看 scope,global 即全可见 → 今天一行行为不变;
|
|
347
|
+
// 将来启用 workspace 围栏时这列已经是现成的正确依据。
|
|
348
|
+
...(routed === undefined ? {} : { workspaceId: routed }),
|
|
224
349
|
// 治理归属(ADR-0015 T1.9/T1.10):repoKey 一次算定;applicability 由抽取
|
|
225
350
|
// prompt 显式产出、经归一收窄——**绝不从 family 推导**(P0-7 四象限),
|
|
226
351
|
// 未声明/非法 = ''(不围栏)。
|
|
@@ -246,7 +371,7 @@ anchorMap) {
|
|
|
246
371
|
conflictFreeze: cfg.conflictFreeze.enabled,
|
|
247
372
|
});
|
|
248
373
|
const dedupRaw = await callLLM(ctx, cfg, {
|
|
249
|
-
system: getConflictDetectionSystemPrompt(mode, { conflictFreeze: cfg.conflictFreeze.enabled }),
|
|
374
|
+
system: getConflictDetectionSystemPrompt(mode, { conflictFreeze: cfg.conflictFreeze.enabled, title: cfg.naming?.enabled === true }),
|
|
250
375
|
user: dedupPrompt,
|
|
251
376
|
maxTokens: resolveLayerTokens(cfg, 'dedup'),
|
|
252
377
|
layer: 'l1-dedup',
|
|
@@ -446,11 +571,39 @@ anchorMap) {
|
|
|
446
571
|
.map((t) => Date.parse(t))
|
|
447
572
|
.filter((t) => !Number.isNaN(t))
|
|
448
573
|
.concat([now]);
|
|
574
|
+
const targetRecords = targets
|
|
575
|
+
.map((id) => byId.get(id))
|
|
576
|
+
.filter((r) => r !== undefined);
|
|
577
|
+
const mergedContent = decision.merged_content && decision.merged_content.trim()
|
|
578
|
+
? decision.merged_content
|
|
579
|
+
: m.content;
|
|
580
|
+
// 合并产物的身份与检索轴必须从 targets 继承(P0-c)。此前这里只带新记忆的
|
|
581
|
+
// metadata:`ext_key` 被写成空 ⇒ 幂等保护随合并消失(而它是“文件↔库”的唯一锚);
|
|
582
|
+
// `name` 同理 ⇒ 文件身份随合并漂移;tags/hall/使用场景全丢 ⇒ 一次合并就把这条
|
|
583
|
+
// 记忆从它原来的 Room/Wing 摘出去,且全程无日志(F2)。
|
|
584
|
+
const inheritedName = targetRecords.find((r) => typeof r.name === 'string' && r.name.length > 0)?.name;
|
|
585
|
+
const inheritedExtKey = targetRecords.find((r) => typeof r.extKey === 'string' && r.extKey.length > 0)?.extKey;
|
|
586
|
+
const axes = mergeAxes(m.metadata, targetRecords.map((r) => r.metadata));
|
|
587
|
+
// 名字优先继承(保持文件身份连续);没得继承就留空,由写库前的起名器按**合并后的正文**分。
|
|
588
|
+
// 继承来的名字如果已被库里另一条占着(典型:本轮正要退场的那条旧记录),
|
|
589
|
+
// `assignMissingNames` 会把它重命到一个带后缀的变体——不能直接写,写了就是撞唯一索引、
|
|
590
|
+
// 整条合并产物丢进不了检索库。
|
|
591
|
+
const mergedName = inheritedName;
|
|
592
|
+
// 丢东西必须说话:静默合并是这一整期要结的账。
|
|
593
|
+
for (const note of axes.notes) {
|
|
594
|
+
logger.info(`[memory] merge 轴冲突(${m.record_id} ← ${targets.join(',')}): ${note}`);
|
|
595
|
+
}
|
|
596
|
+
logger.info(`[memory] merge 传播身份(${m.record_id} ← ${targets.length} 条): `
|
|
597
|
+
+ `name=${mergedName ?? '(本轮没继承到,由起名器分配)'} extKey=${inheritedExtKey ?? '(无)'} `
|
|
598
|
+
+ `tags=${Array.isArray(axes.metadata.tags) ? axes.metadata.tags.length : 0}`);
|
|
599
|
+
// 名字优先级:继承旧身份(保持文件连续)> 模型当场给的 merged_title > 交给起名器。
|
|
600
|
+
// 顺序反了会让每次合并都把新语义挂在旧名字上,而旧名字描述的正是被合并掉的那批内容。
|
|
601
|
+
const mergedTitle = mergedName === undefined || mergedName.length === 0
|
|
602
|
+
? acceptTitle(decision.merged_title)
|
|
603
|
+
: mergedName;
|
|
449
604
|
added.push({
|
|
450
605
|
id: m.record_id,
|
|
451
|
-
content:
|
|
452
|
-
? decision.merged_content
|
|
453
|
-
: m.content,
|
|
606
|
+
content: mergedContent,
|
|
454
607
|
type: decision.merged_type || m.type,
|
|
455
608
|
priority: Number(decision.merged_priority) || Number(m.priority) || 60,
|
|
456
609
|
scene_name: m.scene_name,
|
|
@@ -462,8 +615,19 @@ anchorMap) {
|
|
|
462
615
|
// R7:合并/更新产出的记录同样带锚点——否则"合并一次就丢坐标",
|
|
463
616
|
// 而合并恰恰是长会话里最常发生的动作。无映射时传空表 →
|
|
464
617
|
// resolveSourceAnchors 返回 undefined → 不写 metadata 键(零漂移)。
|
|
465
|
-
metadata: withSourceAnchors(
|
|
618
|
+
metadata: withSourceAnchors(axes.metadata, resolveSourceAnchors(m.source_message_ids, anchorMap ?? new Map())),
|
|
466
619
|
family: m.family,
|
|
620
|
+
// 身份两键:从 targets 继承(没得继承且开关开着才现分)。
|
|
621
|
+
...(inheritedExtKey === undefined ? {} : { extKey: inheritedExtKey }),
|
|
622
|
+
...(mergedTitle === undefined || mergedTitle.length === 0 ? {} : { name: mergedTitle }),
|
|
623
|
+
// 归属五字段必须带上:这里此前**一个都没写**,等于每次合并都把 scope/
|
|
624
|
+
// workspaceId/repoKey/applicability 打回默认——cfg.scope='global' 时看不出来,
|
|
625
|
+
// 一旦启用围栏,一条记忆归过哪个项目就在合并后静默丢了。
|
|
626
|
+
scope: m.scope,
|
|
627
|
+
workspaceId: m.workspaceId,
|
|
628
|
+
repoKeyName: m.repoKeyName,
|
|
629
|
+
repoKeyOwner: m.repoKeyOwner,
|
|
630
|
+
applicability: m.applicability,
|
|
467
631
|
// 合并的有效期取并集:起 = 两侧最早;止 = 任一侧未闭合则仍未闭合(undefined)。
|
|
468
632
|
...mergeTemporal(temporalOf(m.metadata), targets.map((id) => byId.get(id)).filter((r) => r !== undefined)),
|
|
469
633
|
});
|
|
@@ -482,6 +646,10 @@ anchorMap) {
|
|
|
482
646
|
}
|
|
483
647
|
for (const [newId, ids] of byNewRecord) {
|
|
484
648
|
store.retire(ids, { at: retiredAt, reason: 'superseded', by: newId });
|
|
649
|
+
// 旧行退场后它的派生数据不能留在死 id 上:按“每个被合并者 → 新行”搬一次。
|
|
650
|
+
if (mergeRekey !== undefined)
|
|
651
|
+
for (const oldId of ids)
|
|
652
|
+
mergeRekey(oldId, newId);
|
|
485
653
|
}
|
|
486
654
|
}
|
|
487
655
|
// §C 自动裁决的执行面:LLM 的 loser 从检索面退场(winner 存活),同为**软删**。
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2 反向同步:md 投射文件 → 索引(文件是人编辑的那一侧)。
|
|
3
|
+
*
|
|
4
|
+
* 与导出侧(`memory-md-export.ts`)共用同一份落点计算(`targetFor`)、同一个库侧指纹
|
|
5
|
+
* 算法(`recordDigest`)和同一本账本,所以两侧看到的世界一致;写文件这条路**直接复用**
|
|
6
|
+
* `exportRecords`(传单条记录),不另写一套落盘逻辑——两套写盘迟早漂移。
|
|
7
|
+
*
|
|
8
|
+
* 判定一律走**三方**:账本里存着上次投射时的库侧指纹与文件侧指纹,"谁变了"才是可判定的。
|
|
9
|
+
* 四象限(spec P2 裁决表):
|
|
10
|
+
* - 只有文件变 → 回灌入库,然后把文件重写成规范渲染(正文仍是人写的那段,frontmatter 归一)。
|
|
11
|
+
* 这一步同时消掉写回环:库和文件都回到账本记着的那个状态,下一轮判 `same`。
|
|
12
|
+
* - 只有库变 → 重写文件。
|
|
13
|
+
* - **两边都变 → 什么都不做**,只在报告里挂账。人不裁决机器不许动;两侧保持原样、账本也不动,
|
|
14
|
+
* 所以下次对账还会报同样的 `both`(挂账是持久的,不会悄悄过期)。
|
|
15
|
+
* - 都没变 → 跳过,不刷 mtime。
|
|
16
|
+
*
|
|
17
|
+
* 另有一条独立通道:`.memory.md.disable` 后缀 = 人在文件侧手工判过时 → 退场对应记录
|
|
18
|
+
* (写 valid_to,不进任何检索通路)。这是 `recall-feedback -1` 的文件侧对偶。
|
|
19
|
+
*
|
|
20
|
+
* 两条边界:
|
|
21
|
+
* - 没有账本、也认不出记录的文件 = 人新建的,**不入库**。导入一条新记忆必须过写入内核
|
|
22
|
+
* 的判据面(归一/长度/预算/去重),在这里手搓一条等于绕过那些不变量。只计数。
|
|
23
|
+
* - 遍历范围 = 从记录算出的落点根 ∪ 账本里记过的根。所以"没有任何记录指向的全新 workspace"
|
|
24
|
+
* 不在本轮范围内——它还没有投射根,等它的第一条记忆落进去才可见。
|
|
25
|
+
*
|
|
26
|
+
* 另有一条遍历防护(P2 缺口 task_3.7):投射根下的"链接"在 win32 上不止 symlink 一种形状,
|
|
27
|
+
* junction(目录联接,reparse point)也算,而 `Dirent.isSymbolicLink()` 对它的判定随 Node 版本
|
|
28
|
+
* 与文件系统漂移。所以进任何子目录前再加一道 `realpath` 判据:解析出的真实路径必须仍在该投射根
|
|
29
|
+
* 内,界外的一律不进并计入 `skippedLinks`。⚠️ 这个 realpath **只服务划界**——不参与账本键,也不
|
|
30
|
+
* 参与落点计算(那边刻意不做 realpath,理由见 `store/memory-md.ts::targetFor` 的注释)。两处不一致
|
|
31
|
+
* 是有意的,不是 bug。
|
|
32
|
+
*/
|
|
33
|
+
import type { MdLedgerStore } from '../store/memory-md-ledger.js';
|
|
34
|
+
import type { MdSyncReport } from '../store/memory-md-types.js';
|
|
35
|
+
import type { L1Store } from '../store/l1.js';
|
|
36
|
+
import type { MemoryLogger } from '../types.js';
|
|
37
|
+
export interface MdSyncDeps {
|
|
38
|
+
store: L1Store;
|
|
39
|
+
ledger: MdLedgerStore;
|
|
40
|
+
/** `cfg.memExport.root`(空 = 默认单根);与导出侧同一个值。 */
|
|
41
|
+
cfgRoot: string | undefined;
|
|
42
|
+
dataDir: string;
|
|
43
|
+
maxFiles: number;
|
|
44
|
+
timeBudgetMs: number;
|
|
45
|
+
logger?: MemoryLogger;
|
|
46
|
+
/** 注入时钟(测试用)。 */
|
|
47
|
+
now?: () => number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 越界判据:`resolved` 必须就是根,或者是根的子目录。
|
|
51
|
+
*
|
|
52
|
+
* 前缀比较带 `sep`:少了那个分隔符,兄弟目录 `<root>2` 会被判成界内(经典越界陷阱)。
|
|
53
|
+
* 两侧都出自 `realpath`,同一卷上大小写与 8.3 短名都已归一,所以这里做**严格字符串**比较;
|
|
54
|
+
* win32 上再折叠大小写只会放松判据(把真越界说成合法),不做。
|
|
55
|
+
* 导出只为把判据本身钉成用例:造不出链接的机器上,判据仍要有测试可跑。
|
|
56
|
+
*/
|
|
57
|
+
export declare function isWithinRoot(rootReal: string, resolved: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* 反向同步一轮。
|
|
60
|
+
*
|
|
61
|
+
* @param deps - 见 `MdSyncDeps`。
|
|
62
|
+
* @returns 同步报告(含挂账样本、剩余量、以及遍历防护拦下的条目数 `skippedLinks`)。
|
|
63
|
+
* 任何单条失败只计数并留痕,绝不抛挂整轮。
|
|
64
|
+
*/
|
|
65
|
+
export declare function syncFromFiles(deps: MdSyncDeps): Promise<MdSyncReport>;
|