cogmem 3.6.5 → 3.7.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/CHANGELOG.md +24 -0
- package/MEMORY_ATLAS.md +59 -12
- package/README.md +21 -13
- package/RELEASE_CHECKLIST.md +5 -4
- package/dist/agent/AgentMemoryBackend.d.ts +24 -3
- package/dist/agent/AgentMemoryBackend.js +285 -55
- package/dist/agent/AgentRecallQueryCompiler.d.ts +1 -1
- package/dist/agent/AgentRecallQueryCompiler.js +13 -1
- package/dist/agent/MemoryUsageReceipt.js +8 -10
- package/dist/agent/SessionWorkingState.js +3 -2
- package/dist/agent/UntrustedMemorySerializer.d.ts +2 -0
- package/dist/agent/UntrustedMemorySerializer.js +14 -0
- package/dist/atlas/ActionFrameExtractor.js +2 -0
- package/dist/atlas/EpisodeTitleGenerator.d.ts +31 -0
- package/dist/atlas/EpisodeTitleGenerator.js +191 -0
- package/dist/atlas/FacetQueryPlanner.d.ts +33 -0
- package/dist/atlas/FacetQueryPlanner.js +229 -0
- package/dist/atlas/GraphCurator.d.ts +30 -0
- package/dist/atlas/GraphCurator.js +501 -0
- package/dist/atlas/MemoryAtlasIndexer.d.ts +15 -0
- package/dist/atlas/MemoryAtlasIndexer.js +55 -6
- package/dist/atlas/MemoryAtlasQueryCompiler.js +10 -7
- package/dist/atlas/MemoryAtlasService.d.ts +4 -1
- package/dist/atlas/MemoryAtlasService.js +174 -11
- package/dist/atlas/MemoryAtlasTypes.d.ts +75 -11
- package/dist/bin/memory.js +18 -5
- package/dist/factory.d.ts +18 -0
- package/dist/factory.js +41 -2
- package/dist/host/openclaw/AutoMemoryPluginInstaller.js +135 -39
- package/dist/mcp/server.js +1 -1
- package/dist/recall/RecallExplanation.d.ts +1 -1
- package/dist/store/MemoryAtlasStore.d.ts +12 -1
- package/dist/store/MemoryAtlasStore.js +353 -15
- package/dist/utils/ActionKindRegistry.d.ts +10 -0
- package/dist/utils/ActionKindRegistry.js +18 -0
- package/dist/utils/EntityCueExtractor.d.ts +13 -0
- package/dist/utils/EntityCueExtractor.js +81 -0
- package/examples/hermes-backend/AGENTS.md +3 -3
- package/examples/hermes-backend/README.md +1 -1
- package/examples/hermes-backend/SKILL.md +14 -5
- package/examples/hermes-backend/references/operations.md +12 -9
- package/examples/openclaw-backend/AGENTS.md +3 -2
- package/examples/openclaw-backend/README.md +3 -2
- package/examples/openclaw-backend/SKILL.md +31 -9
- package/examples/openclaw-backend/references/operations.md +29 -11
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { MemoryEvent } from '../types/index.js';
|
|
2
|
+
export interface EpisodeTitleInput {
|
|
3
|
+
episodeId: string;
|
|
4
|
+
summary?: string | null;
|
|
5
|
+
topicPath?: string | null;
|
|
6
|
+
episodeType?: string | null;
|
|
7
|
+
startedAt?: number | null;
|
|
8
|
+
events: MemoryEvent[];
|
|
9
|
+
}
|
|
10
|
+
export interface EpisodeTitleResult {
|
|
11
|
+
displayTitle: string;
|
|
12
|
+
oneLineSummary: string;
|
|
13
|
+
topicHints: string[];
|
|
14
|
+
issueHints: string[];
|
|
15
|
+
eventKind: string;
|
|
16
|
+
userIntent?: string;
|
|
17
|
+
localDate?: string;
|
|
18
|
+
confidence: number;
|
|
19
|
+
reviewNeeded: boolean;
|
|
20
|
+
sourceEventIds: string[];
|
|
21
|
+
generatorTrace: {
|
|
22
|
+
usedUserText: boolean;
|
|
23
|
+
usedAssistantText: boolean;
|
|
24
|
+
fallback: boolean;
|
|
25
|
+
matchedRules: string[];
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare class EpisodeTitleGenerator {
|
|
29
|
+
generate(input: EpisodeTitleInput): EpisodeTitleResult;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=EpisodeTitleGenerator.d.ts.map
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { eventTextForMemory } from '../episode/CogmemBlockStripper.js';
|
|
2
|
+
import { extractEntityCues, inferOperationalActionCue } from '../utils/EntityCueExtractor.js';
|
|
3
|
+
const TITLE_RULES = [
|
|
4
|
+
{
|
|
5
|
+
id: 'memory_context_blackbox',
|
|
6
|
+
test: (text) => /(cogmem memory context|memory context|sourcecontext|source context|原文下钻|上下文.*黑盒|记忆上下文|摘要.*原话|记忆黑盒)/i.test(text) &&
|
|
7
|
+
!/(database locked|zombie|卡死|atlas|节点|事件名称|manual recall.*before_prompt_build|手动.*recall.*自动注入)/i.test(text),
|
|
8
|
+
displayTitle: 'CogMem Memory Context 黑盒与原文下钻',
|
|
9
|
+
oneLineSummary: '用户指出注入的 CogMem Memory Context 像黑盒,需要能从摘要下钻到原始对话。',
|
|
10
|
+
topicHints: ['memory-blackbox', 'source-drilldown', 'context-injection'],
|
|
11
|
+
issueHints: ['memory-context-blackbox'],
|
|
12
|
+
eventKind: 'diagnostic',
|
|
13
|
+
userIntent: 'inspect_original_context',
|
|
14
|
+
confidence: 0.92,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'graph_runtime_blackbox',
|
|
18
|
+
test: (text) => /(memory graph|graph.*卡死|卡死|database locked|sqlite.*lock|zombie|僵尸进程|锁冲突)/i.test(text),
|
|
19
|
+
displayTitle: 'memory graph 卡死与数据库锁',
|
|
20
|
+
oneLineSummary: '用户遇到 memory graph 卡死、SQLite 锁冲突或残留进程,导致图谱与召回命令不稳定。',
|
|
21
|
+
topicHints: ['memory-blackbox', 'graph-runtime', 'sqlite-locks'],
|
|
22
|
+
issueHints: ['graph-runtime-blackbox'],
|
|
23
|
+
eventKind: 'bug',
|
|
24
|
+
userIntent: 'debug_graph_runtime',
|
|
25
|
+
confidence: 0.9,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'atlas_readability_blackbox',
|
|
29
|
+
test: (text) => /(atlas|图谱|节点|事件名称|父主题|多维|facet|canonical|可读性|label|标题)/i.test(text),
|
|
30
|
+
displayTitle: 'Atlas 节点命名与多维导航',
|
|
31
|
+
oneLineSummary: '用户要求 Atlas 节点拥有清晰事件标题,并通过时间、主题、issue、实体等 facet 导航到同一份 canonical episode。',
|
|
32
|
+
topicHints: ['memory-blackbox', 'atlas-readability', 'facet-navigation'],
|
|
33
|
+
issueHints: ['atlas-readability'],
|
|
34
|
+
eventKind: 'plan',
|
|
35
|
+
userIntent: 'improve_atlas_navigation',
|
|
36
|
+
confidence: 0.88,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'auto_injection_mismatch',
|
|
40
|
+
test: (text) => /((manual recall|手动.*recall|CLI).*(自动注入|before_prompt_build|OpenClaw)|(自动注入|before_prompt_build|OpenClaw).*(manual recall|手动.*recall|CLI)|注入.*不一致|selected.*different|选择.*不同)/i.test(text),
|
|
41
|
+
displayTitle: '自动注入与手动 recall 不一致',
|
|
42
|
+
oneLineSummary: '用户发现 OpenClaw 自动注入与手动 recall 的记忆选择不一致,需要暴露选择依据和匹配 facet。',
|
|
43
|
+
topicHints: ['context-injection', 'memory-blackbox', 'openclaw'],
|
|
44
|
+
issueHints: ['auto-injection-mismatch'],
|
|
45
|
+
eventKind: 'diagnostic',
|
|
46
|
+
userIntent: 'align_auto_injection',
|
|
47
|
+
confidence: 0.89,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
export class EpisodeTitleGenerator {
|
|
51
|
+
generate(input) {
|
|
52
|
+
const userEvents = input.events.filter((event) => event.role === 'user');
|
|
53
|
+
const assistantEvents = input.events.filter((event) => event.role === 'assistant' || event.role === 'agent');
|
|
54
|
+
const userText = normalizeText(userEvents.map(eventTextForMemory).join('\n'));
|
|
55
|
+
const assistantText = normalizeText(assistantEvents.map(eventTextForMemory).join('\n'));
|
|
56
|
+
const summaryText = normalizeText([input.summary, input.topicPath, input.episodeType].filter(Boolean).join('\n'));
|
|
57
|
+
const preferredText = [userText, summaryText, assistantText].filter(Boolean).join('\n');
|
|
58
|
+
const operationTitle = inferOperationTitle(preferredText);
|
|
59
|
+
const matchedRule = TITLE_RULES.find((rule) => rule.test(preferredText));
|
|
60
|
+
const localDate = inferLocalDate(input.events, input.startedAt);
|
|
61
|
+
const sourceEventIds = sourceIdsFor(userEvents.length > 0 ? userEvents : input.events);
|
|
62
|
+
if (matchedRule) {
|
|
63
|
+
const matchedSourceEventIds = sourceIdsForMatching(userEvents.length > 0 ? userEvents : input.events, matchedRule.test) || sourceEventIds;
|
|
64
|
+
return {
|
|
65
|
+
displayTitle: boundTitle(matchedRule.displayTitle),
|
|
66
|
+
oneLineSummary: matchedRule.oneLineSummary,
|
|
67
|
+
topicHints: matchedRule.topicHints,
|
|
68
|
+
issueHints: matchedRule.issueHints,
|
|
69
|
+
eventKind: matchedRule.eventKind,
|
|
70
|
+
userIntent: matchedRule.userIntent,
|
|
71
|
+
localDate,
|
|
72
|
+
confidence: matchedRule.confidence,
|
|
73
|
+
reviewNeeded: matchedRule.confidence < 0.65,
|
|
74
|
+
sourceEventIds: matchedSourceEventIds,
|
|
75
|
+
generatorTrace: {
|
|
76
|
+
usedUserText: userText.length > 0,
|
|
77
|
+
usedAssistantText: userText.length === 0 && assistantText.length > 0,
|
|
78
|
+
fallback: false,
|
|
79
|
+
matchedRules: [matchedRule.id],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (operationTitle) {
|
|
84
|
+
const matchedSourceEventIds = sourceIdsForMatching(userEvents.length > 0 ? userEvents : input.events, (text) => Boolean(inferOperationTitle(text))) || sourceEventIds;
|
|
85
|
+
return {
|
|
86
|
+
displayTitle: boundTitle(operationTitle.displayTitle),
|
|
87
|
+
oneLineSummary: operationTitle.oneLineSummary,
|
|
88
|
+
topicHints: operationTitle.topicHints,
|
|
89
|
+
issueHints: [],
|
|
90
|
+
eventKind: 'operation',
|
|
91
|
+
userIntent: 'action_history',
|
|
92
|
+
localDate,
|
|
93
|
+
confidence: 0.82,
|
|
94
|
+
reviewNeeded: false,
|
|
95
|
+
sourceEventIds: matchedSourceEventIds,
|
|
96
|
+
generatorTrace: {
|
|
97
|
+
usedUserText: userText.length > 0,
|
|
98
|
+
usedAssistantText: userText.length === 0 && assistantText.length > 0,
|
|
99
|
+
fallback: false,
|
|
100
|
+
matchedRules: ['generic_entity_operation'],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const fallbackTitle = fallbackDisplayTitle({ userText, summaryText, topicPath: input.topicPath, episodeType: input.episodeType });
|
|
105
|
+
return {
|
|
106
|
+
displayTitle: fallbackTitle,
|
|
107
|
+
oneLineSummary: fallbackSummary({ userText, summaryText, topicPath: input.topicPath }),
|
|
108
|
+
topicHints: inferTopicHints(preferredText),
|
|
109
|
+
issueHints: [],
|
|
110
|
+
eventKind: normalizeKind(input.episodeType) ?? 'explanation',
|
|
111
|
+
localDate,
|
|
112
|
+
confidence: 0.55,
|
|
113
|
+
reviewNeeded: true,
|
|
114
|
+
sourceEventIds,
|
|
115
|
+
generatorTrace: {
|
|
116
|
+
usedUserText: userText.length > 0,
|
|
117
|
+
usedAssistantText: userText.length === 0 && assistantText.length > 0,
|
|
118
|
+
fallback: true,
|
|
119
|
+
matchedRules: [],
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function normalizeText(text) {
|
|
125
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
126
|
+
}
|
|
127
|
+
function inferOperationTitle(text) {
|
|
128
|
+
const action = inferOperationalActionCue(text);
|
|
129
|
+
const entity = extractEntityCues(text, 1)[0];
|
|
130
|
+
if (!action || !entity)
|
|
131
|
+
return undefined;
|
|
132
|
+
return {
|
|
133
|
+
displayTitle: `${entity.label} ${action.label}`,
|
|
134
|
+
oneLineSummary: `用户要求对 ${entity.label} 执行${action.verb}相关操作。`,
|
|
135
|
+
topicHints: [entity.id],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function sourceIdsFor(events) {
|
|
139
|
+
return events.map((event) => event.eventId).filter(Boolean).slice(0, 20);
|
|
140
|
+
}
|
|
141
|
+
function sourceIdsForMatching(events, test) {
|
|
142
|
+
const matched = events.filter((event) => test(eventTextForMemory(event)));
|
|
143
|
+
return matched.length ? sourceIdsFor(matched) : undefined;
|
|
144
|
+
}
|
|
145
|
+
function inferLocalDate(events, startedAt) {
|
|
146
|
+
for (const event of events) {
|
|
147
|
+
const candidate = event.localDate;
|
|
148
|
+
if (candidate && /^\d{4}-\d{2}-\d{2}$/.test(candidate))
|
|
149
|
+
return candidate;
|
|
150
|
+
}
|
|
151
|
+
const timestamp = events.find((event) => Number.isFinite(event.occurredAt))?.occurredAt ?? startedAt;
|
|
152
|
+
if (!timestamp || !Number.isFinite(timestamp))
|
|
153
|
+
return undefined;
|
|
154
|
+
return new Date(timestamp).toISOString().slice(0, 10);
|
|
155
|
+
}
|
|
156
|
+
function boundTitle(title) {
|
|
157
|
+
const clean = normalizeText(title).replace(/[0-9a-f]{8}-[0-9a-f-]{12,}/gi, '').trim();
|
|
158
|
+
if (/[^\x00-\x7F]/.test(clean))
|
|
159
|
+
return clean.length > 32 ? `${clean.slice(0, 31)}…` : clean;
|
|
160
|
+
const words = clean.split(/\s+/);
|
|
161
|
+
return words.length > 12 ? words.slice(0, 12).join(' ') : clean;
|
|
162
|
+
}
|
|
163
|
+
function fallbackDisplayTitle(input) {
|
|
164
|
+
const source = input.userText || input.summaryText || input.topicPath || input.episodeType || 'Untitled episode';
|
|
165
|
+
const firstSentence = source.split(/[。.!?\n]/).find(Boolean) ?? source;
|
|
166
|
+
const title = boundTitle(firstSentence);
|
|
167
|
+
if (!title || /^episode[:\s-]*[0-9a-f-]+$/i.test(title))
|
|
168
|
+
return '未命名记忆事件';
|
|
169
|
+
return title;
|
|
170
|
+
}
|
|
171
|
+
function fallbackSummary(input) {
|
|
172
|
+
const source = input.summaryText || input.userText || input.topicPath || '该 episode 缺少可读摘要,需要人工复核。';
|
|
173
|
+
const sentence = source.split(/[。.!?\n]/).find(Boolean) ?? source;
|
|
174
|
+
return normalizeText(sentence).slice(0, 140);
|
|
175
|
+
}
|
|
176
|
+
function inferTopicHints(text) {
|
|
177
|
+
const hints = new Set();
|
|
178
|
+
if (/cogmem|记忆|memory/i.test(text))
|
|
179
|
+
hints.add('memory');
|
|
180
|
+
if (/openclaw/i.test(text))
|
|
181
|
+
hints.add('openclaw');
|
|
182
|
+
for (const entity of extractEntityCues(text, 4))
|
|
183
|
+
hints.add(entity.id);
|
|
184
|
+
return Array.from(hints);
|
|
185
|
+
}
|
|
186
|
+
function normalizeKind(kind) {
|
|
187
|
+
if (!kind)
|
|
188
|
+
return undefined;
|
|
189
|
+
const normalized = kind.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
190
|
+
return normalized || undefined;
|
|
191
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type FacetType = 'time' | 'topic' | 'issue' | 'entity' | 'session' | 'thread' | 'memoryKind' | 'actionKind';
|
|
2
|
+
export type FacetOperator = 'intersection' | 'union';
|
|
3
|
+
export interface PlannedFacet {
|
|
4
|
+
type: FacetType;
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
nodeId?: string;
|
|
8
|
+
relation?: string;
|
|
9
|
+
granularity?: 'year' | 'month' | 'day';
|
|
10
|
+
from?: number;
|
|
11
|
+
to?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface FacetQueryPlan {
|
|
14
|
+
intent: string;
|
|
15
|
+
operator: FacetOperator;
|
|
16
|
+
facets: PlannedFacet[];
|
|
17
|
+
temporalIntent?: 'timeline';
|
|
18
|
+
groupBy?: 'topic' | 'time' | 'issue';
|
|
19
|
+
exactness: 'strict' | 'relaxed';
|
|
20
|
+
requiresIntersection: boolean;
|
|
21
|
+
keywords: string[];
|
|
22
|
+
query: string;
|
|
23
|
+
}
|
|
24
|
+
export interface FacetQueryPlannerOptions {
|
|
25
|
+
projectId: string;
|
|
26
|
+
now?: number;
|
|
27
|
+
localDateNow?: string;
|
|
28
|
+
timeZone?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare class FacetQueryPlanner {
|
|
31
|
+
plan(query: string, options: FacetQueryPlannerOptions): FacetQueryPlan;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=FacetQueryPlanner.d.ts.map
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { extractEntityCues } from '../utils/EntityCueExtractor.js';
|
|
2
|
+
import { ACTION_KIND_RULES } from '../utils/ActionKindRegistry.js';
|
|
3
|
+
const ISSUE_RULES = [
|
|
4
|
+
{
|
|
5
|
+
value: 'memory-context-blackbox',
|
|
6
|
+
label: 'Memory Context 黑盒',
|
|
7
|
+
test: (text) => /(memory context|sourcecontext|source context|上下文.*黑盒|记忆上下文|原文下钻|摘要.*原话)/i.test(text),
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
value: 'graph-runtime-blackbox',
|
|
11
|
+
label: 'Graph Runtime 黑盒',
|
|
12
|
+
test: (text) => /(memory graph|graph.*卡死|database locked|sqlite.*lock|zombie|僵尸进程|锁冲突|卡死)/i.test(text),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
value: 'atlas-readability',
|
|
16
|
+
label: 'Atlas 可读性黑盒',
|
|
17
|
+
test: (text) => /(atlas|图谱|节点|事件名称|父主题|多维|facet|canonical|可读性|标题)/i.test(text),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
value: 'auto-injection-mismatch',
|
|
21
|
+
label: '自动注入不一致',
|
|
22
|
+
test: (text) => /(自动注入|before_prompt_build|manual recall|手动.*recall|注入.*不一致|COGMEM_RECALL_CONTEXT)/i.test(text),
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
const TOPIC_RULES = [
|
|
26
|
+
{
|
|
27
|
+
value: 'memory-blackbox',
|
|
28
|
+
label: '记忆黑盒',
|
|
29
|
+
test: (text) => /(记忆黑盒|memory.*blackbox|memory context|sourcecontext|graph.*卡死|atlas|自动注入|上下文.*黑盒)/i.test(text),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
value: 'source-drilldown',
|
|
33
|
+
label: '原文下钻',
|
|
34
|
+
test: (text) => /(sourcecontext|source context|原文下钻|摘要.*原文)/i.test(text),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
value: 'context-injection',
|
|
38
|
+
label: '上下文注入',
|
|
39
|
+
test: (text) => /(自动注入|context injection|before_prompt_build|COGMEM_RECALL_CONTEXT|注入)/i.test(text),
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
export class FacetQueryPlanner {
|
|
43
|
+
plan(query, options) {
|
|
44
|
+
const normalized = query.trim();
|
|
45
|
+
const facets = [];
|
|
46
|
+
const actionHistory = isActionHistoryQuery(normalized);
|
|
47
|
+
const timeFacet = parseTimeFacet(normalized, options);
|
|
48
|
+
if (timeFacet)
|
|
49
|
+
facets.push(withNodeId(timeFacet, options.projectId));
|
|
50
|
+
const timeline = /(后来|继续|timeline|演化|发展|之后)/i.test(normalized);
|
|
51
|
+
const matchedIssueValues = new Set();
|
|
52
|
+
for (const rule of TOPIC_RULES) {
|
|
53
|
+
if (rule.test(normalized)) {
|
|
54
|
+
facets.push(withNodeId({ type: 'topic', value: `PROJECT/${options.projectId}/${rule.value}`, label: rule.label, relation: 'ABOUT_TOPIC' }, options.projectId));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const rule of ISSUE_RULES) {
|
|
58
|
+
if (rule.test(normalized)) {
|
|
59
|
+
facets.push(withNodeId({ type: 'issue', value: rule.value, label: rule.label, relation: 'PART_OF_ISSUE' }, options.projectId));
|
|
60
|
+
matchedIssueValues.add(rule.value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!timeline && isBroadMemoryBlackbox(normalized) && matchedIssueValues.size === 0) {
|
|
64
|
+
facets.push(withNodeId({ type: 'issue', value: 'memory-context-blackbox', label: 'Memory Context 黑盒', relation: 'PART_OF_ISSUE' }, options.projectId));
|
|
65
|
+
}
|
|
66
|
+
for (const entity of extractEntityCues(normalized)) {
|
|
67
|
+
if (!actionHistory && isConceptFacetEntity(entity, facets))
|
|
68
|
+
continue;
|
|
69
|
+
facets.push({ type: 'entity', value: `facet:${entity.id}`, label: entity.label, nodeId: `entity:${options.projectId}:facet:${entity.id}`, relation: 'INVOLVES_ENTITY' });
|
|
70
|
+
if (actionHistory) {
|
|
71
|
+
facets.push(withNodeId({ type: 'topic', value: `PROJECT/${options.projectId}/${entity.id}`, label: entity.label, relation: 'ABOUT_TOPIC' }, options.projectId));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const facet of parseKindFacets(normalized, options.projectId))
|
|
75
|
+
facets.push(facet);
|
|
76
|
+
if (actionHistory && (!facets.some((facet) => facet.type === 'actionKind') || !hasSpecificActionCue(normalized))) {
|
|
77
|
+
for (const value of ['started', 'installed', 'configured', 'restarted', 'stopped', 'operated', 'implemented', 'debugged']) {
|
|
78
|
+
facets.push(withNodeId({ type: 'actionKind', value, label: value, relation: 'HAS_ACTION_KIND' }, options.projectId));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
intent: actionHistory ? 'action_history' : /聊过|记得|还记得|讨论|原话|那次/i.test(normalized) ? 'historical_discussion' : 'graph_search',
|
|
83
|
+
operator: 'intersection',
|
|
84
|
+
facets: dedupeFacets(facets),
|
|
85
|
+
temporalIntent: timeline ? 'timeline' : undefined,
|
|
86
|
+
groupBy: timeline ? 'time' : timeFacet && facets.length === 1 ? 'topic' : facets.some((facet) => facet.type === 'issue') ? 'issue' : undefined,
|
|
87
|
+
exactness: 'strict',
|
|
88
|
+
requiresIntersection: facets.length > 1,
|
|
89
|
+
keywords: extractKeywords(normalized),
|
|
90
|
+
query: normalized,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function isActionHistoryQuery(query) {
|
|
95
|
+
return /(让你.{0,20}(对|给|把)?.{0,20}(做过|做了|启动|安装|配置|修改|重启|停止|操作|处理|执行)|对.{0,30}(做过什么|做了什么|哪些操作)|what did (i ask you to do|you do) to|operations? on)/i.test(query);
|
|
96
|
+
}
|
|
97
|
+
function hasSpecificActionCue(query) {
|
|
98
|
+
return /(启动|start|started|launch|launched|boot|安装|install|installed|setup|配置|config|configured|设置|修改配置|重启|restart|停止|stop|修复|实现|implemented|review|审查|debug|排查|卡死|locked|zombie)/i.test(query);
|
|
99
|
+
}
|
|
100
|
+
function isBroadMemoryBlackbox(query) {
|
|
101
|
+
return /(记忆黑盒|memory.*blackbox)/i.test(query) &&
|
|
102
|
+
!/(memory graph|graph|database locked|sqlite|zombie|僵尸|卡死|atlas|图谱|节点|事件名称|自动注入|before_prompt_build|manual recall|手动.*recall)/i.test(query);
|
|
103
|
+
}
|
|
104
|
+
function isConceptFacetEntity(entity, facets) {
|
|
105
|
+
const label = entity.label.toLocaleLowerCase();
|
|
106
|
+
return facets.some((facet) => {
|
|
107
|
+
if (facet.type !== 'topic' && facet.type !== 'issue')
|
|
108
|
+
return false;
|
|
109
|
+
const facetLabel = facet.label.toLocaleLowerCase();
|
|
110
|
+
const facetSlug = facet.value.split('/').pop()?.toLocaleLowerCase();
|
|
111
|
+
return label === facetLabel || label.includes(facetLabel) || Boolean(facetSlug && entity.id === facetSlug);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function parseKindFacets(query, projectId) {
|
|
115
|
+
const facets = [];
|
|
116
|
+
const memoryKindRules = [
|
|
117
|
+
['bug', /(bug|问题|卡死|failure|故障|错误|报错)/i],
|
|
118
|
+
['decision', /(decision|决定|方案|结论)/i],
|
|
119
|
+
['plan', /(计划|下一步|策略|plan)/i],
|
|
120
|
+
];
|
|
121
|
+
for (const [value, test] of memoryKindRules) {
|
|
122
|
+
if (test.test(query))
|
|
123
|
+
facets.push(withNodeId({ type: 'memoryKind', value, label: value, relation: 'HAS_MEMORY_KIND' }, projectId));
|
|
124
|
+
}
|
|
125
|
+
for (const rule of ACTION_KIND_RULES) {
|
|
126
|
+
if (rule.pattern.test(query))
|
|
127
|
+
facets.push(withNodeId({ type: 'actionKind', value: rule.kind, label: rule.kind, relation: 'HAS_ACTION_KIND' }, projectId));
|
|
128
|
+
}
|
|
129
|
+
return facets;
|
|
130
|
+
}
|
|
131
|
+
function parseTimeFacet(query, options) {
|
|
132
|
+
const currentYear = localYear(options);
|
|
133
|
+
const isoDay = query.match(/(20\d{2})[-年\/.](\d{1,2})[-月\/.](\d{1,2})日?/);
|
|
134
|
+
if (isoDay)
|
|
135
|
+
return dayFacet(Number(isoDay[1]), Number(isoDay[2]), Number(isoDay[3]));
|
|
136
|
+
const cnDay = query.match(/(?:(20\d{2})年)?(\d{1,2})月(\d{1,2})(?:号|日)?/);
|
|
137
|
+
if (cnDay)
|
|
138
|
+
return dayFacet(cnDay[1] ? Number(cnDay[1]) : currentYear, Number(cnDay[2]), Number(cnDay[3]));
|
|
139
|
+
const isoMonth = query.match(/(20\d{2})[-年\/.](\d{1,2})月?/);
|
|
140
|
+
if (isoMonth)
|
|
141
|
+
return monthFacet(Number(isoMonth[1]), Number(isoMonth[2]));
|
|
142
|
+
const year = query.match(/\b(20\d{2})\b|((20\d{2})年)/);
|
|
143
|
+
if (year)
|
|
144
|
+
return yearFacet(Number(year[1] ?? year[3]));
|
|
145
|
+
if (/去年/.test(query))
|
|
146
|
+
return yearFacet(currentYear - 1);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
function localYear(options) {
|
|
150
|
+
const explicit = options.localDateNow?.match(/^(20\d{2})-\d{2}-\d{2}$/u)?.[1];
|
|
151
|
+
if (explicit)
|
|
152
|
+
return Number(explicit);
|
|
153
|
+
const now = options.now ?? Date.now();
|
|
154
|
+
try {
|
|
155
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
156
|
+
timeZone: options.timeZone || 'Asia/Tokyo',
|
|
157
|
+
year: 'numeric',
|
|
158
|
+
month: '2-digit',
|
|
159
|
+
day: '2-digit',
|
|
160
|
+
}).formatToParts(new Date(now));
|
|
161
|
+
const year = parts.find((part) => part.type === 'year')?.value;
|
|
162
|
+
if (year)
|
|
163
|
+
return Number(year);
|
|
164
|
+
}
|
|
165
|
+
catch { /* fall back below */ }
|
|
166
|
+
return new Date(now).getUTCFullYear();
|
|
167
|
+
}
|
|
168
|
+
function dayFacet(year, month, day) {
|
|
169
|
+
const label = `${year}-${pad(month)}-${pad(day)}`;
|
|
170
|
+
return {
|
|
171
|
+
type: 'time',
|
|
172
|
+
value: label,
|
|
173
|
+
label,
|
|
174
|
+
relation: 'OCCURRED_ON',
|
|
175
|
+
granularity: 'day',
|
|
176
|
+
from: Date.UTC(year, month - 1, day),
|
|
177
|
+
to: Date.UTC(year, month - 1, day + 1),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function monthFacet(year, month) {
|
|
181
|
+
const label = `${year}-${pad(month)}`;
|
|
182
|
+
return {
|
|
183
|
+
type: 'time',
|
|
184
|
+
value: label,
|
|
185
|
+
label,
|
|
186
|
+
relation: 'OCCURRED_IN',
|
|
187
|
+
granularity: 'month',
|
|
188
|
+
from: Date.UTC(year, month - 1, 1),
|
|
189
|
+
to: Date.UTC(year, month, 1),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function yearFacet(year) {
|
|
193
|
+
const label = String(year);
|
|
194
|
+
return {
|
|
195
|
+
type: 'time',
|
|
196
|
+
value: label,
|
|
197
|
+
label,
|
|
198
|
+
relation: 'OCCURRED_IN',
|
|
199
|
+
granularity: 'year',
|
|
200
|
+
from: Date.UTC(year, 0, 1),
|
|
201
|
+
to: Date.UTC(year + 1, 0, 1),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function withNodeId(facet, projectId) {
|
|
205
|
+
return {
|
|
206
|
+
...facet,
|
|
207
|
+
nodeId: `${facet.type}:${projectId}:${facet.value}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function dedupeFacets(facets) {
|
|
211
|
+
const seen = new Set();
|
|
212
|
+
return facets.filter((facet) => {
|
|
213
|
+
const key = `${facet.type}:${facet.value}`;
|
|
214
|
+
if (seen.has(key))
|
|
215
|
+
return false;
|
|
216
|
+
seen.add(key);
|
|
217
|
+
return true;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function extractKeywords(query) {
|
|
221
|
+
return query
|
|
222
|
+
.split(/[^\p{L}\p{N}_-]+/u)
|
|
223
|
+
.map((token) => token.trim())
|
|
224
|
+
.filter((token) => token.length >= 2)
|
|
225
|
+
.slice(0, 8);
|
|
226
|
+
}
|
|
227
|
+
function pad(value) {
|
|
228
|
+
return String(value).padStart(2, '0');
|
|
229
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type Database from 'bun:sqlite';
|
|
2
|
+
import type { EventStore } from '../store/EventStore.js';
|
|
3
|
+
import { MemoryAtlasStore } from '../store/MemoryAtlasStore.js';
|
|
4
|
+
export interface GraphCuratorResult {
|
|
5
|
+
episodeCount: number;
|
|
6
|
+
facetNodeCount: number;
|
|
7
|
+
facetEdgeCount: number;
|
|
8
|
+
reviewNeeded: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class GraphCurator {
|
|
11
|
+
private readonly db;
|
|
12
|
+
private readonly eventStore;
|
|
13
|
+
private readonly atlasStore;
|
|
14
|
+
private readonly titleGenerator;
|
|
15
|
+
constructor(db: Database, eventStore: EventStore, atlasStore: MemoryAtlasStore);
|
|
16
|
+
rebuild(projectId: string, now?: number): GraphCuratorResult;
|
|
17
|
+
rebuildEpisodes(projectId: string, episodeIds: string[], now?: number): GraphCuratorResult;
|
|
18
|
+
private projectEpisode;
|
|
19
|
+
private episodeEventIds;
|
|
20
|
+
private facetTargetsFor;
|
|
21
|
+
private upsertFacetNode;
|
|
22
|
+
private upsertRawEventNode;
|
|
23
|
+
private projectEpisodeRelations;
|
|
24
|
+
private upsertEpisodeRelation;
|
|
25
|
+
private entityHintsFor;
|
|
26
|
+
private deleteFacetEdges;
|
|
27
|
+
private deleteFacetEdgesForEpisodes;
|
|
28
|
+
private upsertEdge;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=GraphCurator.d.ts.map
|