@gethmy/mcp 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +80 -23
- package/dist/index.js +80 -23
- package/dist/lib/active-learning.js +939 -787
- package/dist/lib/api-client.js +2527 -638
- package/dist/lib/auto-session.js +177 -196
- package/dist/lib/cli.js +34954 -128
- package/dist/lib/config.js +235 -201
- package/dist/lib/consolidation.js +374 -289
- package/dist/lib/context-assembly.js +1265 -838
- package/dist/lib/graph-expansion.js +139 -155
- package/dist/lib/http.js +1917 -130
- package/dist/lib/index.js +29525 -5
- package/dist/lib/lifecycle-maintenance.js +663 -79
- package/dist/lib/memory-cleanup.js +1316 -381
- package/dist/lib/onboard.js +2588 -32
- package/dist/lib/prompt-builder.js +438 -445
- package/dist/lib/remote.js +31733 -143
- package/dist/lib/server.js +29389 -3216
- package/dist/lib/skills.js +315 -132
- package/dist/lib/tui/agents.js +128 -107
- package/dist/lib/tui/docs.js +1590 -687
- package/dist/lib/tui/setup.js +5698 -804
- package/dist/lib/tui/theme.js +183 -86
- package/dist/lib/tui/writer.js +1149 -176
- package/package.json +2 -2
- package/src/api-client.ts +37 -1
- package/src/memory-cleanup.ts +92 -52
- package/src/server.ts +16 -1
- package/dist/lib/__tests__/active-learning.test.js +0 -386
- package/dist/lib/__tests__/agent-performance-profiles.test.js +0 -325
- package/dist/lib/__tests__/auto-session.test.js +0 -661
- package/dist/lib/__tests__/context-assembly.test.js +0 -362
- package/dist/lib/__tests__/graph-expansion.test.js +0 -150
- package/dist/lib/__tests__/integration-memory-crud.test.js +0 -797
- package/dist/lib/__tests__/integration-memory-system.test.js +0 -281
- package/dist/lib/__tests__/lifecycle-maintenance.test.js +0 -207
- package/dist/lib/__tests__/pattern-detection.test.js +0 -295
- package/dist/lib/__tests__/prompt-builder.test.js +0 -418
|
@@ -1,822 +1,974 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
set: (newValue) => all[name] = () => newValue
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
29
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
30
|
+
|
|
31
|
+
// src/config.ts
|
|
32
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
33
|
+
import { homedir } from "node:os";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
var DEFAULT_API_URL = "https://app.gethmy.com/api";
|
|
36
|
+
var LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
37
|
+
function getConfigDir() {
|
|
38
|
+
return join(homedir(), ".harmony-mcp");
|
|
39
|
+
}
|
|
40
|
+
function getConfigPath() {
|
|
41
|
+
return join(getConfigDir(), "config.json");
|
|
42
|
+
}
|
|
43
|
+
function getLocalConfigPath(cwd) {
|
|
44
|
+
return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
45
|
+
}
|
|
46
|
+
function loadConfig() {
|
|
47
|
+
const configPath = getConfigPath();
|
|
48
|
+
if (!existsSync(configPath)) {
|
|
49
|
+
return {
|
|
50
|
+
apiKey: null,
|
|
51
|
+
apiUrl: DEFAULT_API_URL,
|
|
52
|
+
activeWorkspaceId: null,
|
|
53
|
+
activeProjectId: null,
|
|
54
|
+
userEmail: null,
|
|
55
|
+
memoryDir: null
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const data = readFileSync(configPath, "utf-8");
|
|
60
|
+
const config = JSON.parse(data);
|
|
61
|
+
return {
|
|
62
|
+
apiKey: config.apiKey || null,
|
|
63
|
+
apiUrl: config.apiUrl || DEFAULT_API_URL,
|
|
64
|
+
activeWorkspaceId: config.activeWorkspaceId || null,
|
|
65
|
+
activeProjectId: config.activeProjectId || null,
|
|
66
|
+
userEmail: config.userEmail || null,
|
|
67
|
+
memoryDir: config.memoryDir || null
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
return {
|
|
71
|
+
apiKey: null,
|
|
72
|
+
apiUrl: DEFAULT_API_URL,
|
|
73
|
+
activeWorkspaceId: null,
|
|
74
|
+
activeProjectId: null,
|
|
75
|
+
userEmail: null,
|
|
76
|
+
memoryDir: null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function saveConfig(config) {
|
|
81
|
+
const configDir = getConfigDir();
|
|
82
|
+
const configPath = getConfigPath();
|
|
83
|
+
if (!existsSync(configDir)) {
|
|
84
|
+
mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
85
|
+
}
|
|
86
|
+
const existingConfig = loadConfig();
|
|
87
|
+
const newConfig = { ...existingConfig, ...config };
|
|
88
|
+
writeFileSync(configPath, JSON.stringify(newConfig, null, 2), {
|
|
89
|
+
mode: 384
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function loadLocalConfig(cwd) {
|
|
93
|
+
const localConfigPath = getLocalConfigPath(cwd);
|
|
94
|
+
if (!existsSync(localConfigPath)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const data = readFileSync(localConfigPath, "utf-8");
|
|
99
|
+
const config = JSON.parse(data);
|
|
100
|
+
return {
|
|
101
|
+
workspaceId: config.workspaceId || null,
|
|
102
|
+
projectId: config.projectId || null
|
|
103
|
+
};
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function saveLocalConfig(config, cwd) {
|
|
109
|
+
const localConfigPath = getLocalConfigPath(cwd);
|
|
110
|
+
const existingConfig = loadLocalConfig(cwd) || {
|
|
111
|
+
workspaceId: null,
|
|
112
|
+
projectId: null
|
|
113
|
+
};
|
|
114
|
+
const newConfig = { ...existingConfig, ...config };
|
|
115
|
+
const cleanConfig = {};
|
|
116
|
+
if (newConfig.workspaceId)
|
|
117
|
+
cleanConfig.workspaceId = newConfig.workspaceId;
|
|
118
|
+
if (newConfig.projectId)
|
|
119
|
+
cleanConfig.projectId = newConfig.projectId;
|
|
120
|
+
writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
|
|
121
|
+
}
|
|
122
|
+
function hasLocalConfig(cwd) {
|
|
123
|
+
return existsSync(getLocalConfigPath(cwd));
|
|
124
|
+
}
|
|
125
|
+
function getApiKey() {
|
|
126
|
+
const config = loadConfig();
|
|
127
|
+
if (!config.apiKey) {
|
|
128
|
+
throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to set your API key.
|
|
129
|
+
` + "You can generate an API key at https://gethmy.com → Settings → API Keys.");
|
|
130
|
+
}
|
|
131
|
+
return config.apiKey;
|
|
132
|
+
}
|
|
133
|
+
function getApiUrl() {
|
|
134
|
+
const config = loadConfig();
|
|
135
|
+
return config.apiUrl;
|
|
136
|
+
}
|
|
137
|
+
function getUserEmail() {
|
|
138
|
+
const config = loadConfig();
|
|
139
|
+
return config.userEmail;
|
|
140
|
+
}
|
|
141
|
+
function setUserEmail(email) {
|
|
142
|
+
saveConfig({ userEmail: email });
|
|
143
|
+
}
|
|
144
|
+
function setActiveWorkspace(workspaceId, options) {
|
|
145
|
+
if (options?.local) {
|
|
146
|
+
saveLocalConfig({ workspaceId }, options.cwd);
|
|
147
|
+
} else {
|
|
148
|
+
saveConfig({ activeWorkspaceId: workspaceId });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function setActiveProject(projectId, options) {
|
|
152
|
+
if (options?.local) {
|
|
153
|
+
saveLocalConfig({ projectId }, options.cwd);
|
|
154
|
+
} else {
|
|
155
|
+
saveConfig({ activeProjectId: projectId });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function getActiveWorkspaceId(cwd) {
|
|
159
|
+
const localConfig = loadLocalConfig(cwd);
|
|
160
|
+
if (localConfig?.workspaceId) {
|
|
161
|
+
return localConfig.workspaceId;
|
|
162
|
+
}
|
|
163
|
+
return loadConfig().activeWorkspaceId;
|
|
164
|
+
}
|
|
165
|
+
function getActiveProjectId(cwd) {
|
|
166
|
+
const localConfig = loadLocalConfig(cwd);
|
|
167
|
+
if (localConfig?.projectId) {
|
|
168
|
+
return localConfig.projectId;
|
|
169
|
+
}
|
|
170
|
+
return loadConfig().activeProjectId;
|
|
171
|
+
}
|
|
172
|
+
function isConfigured() {
|
|
173
|
+
const config = loadConfig();
|
|
174
|
+
return !!config.apiKey;
|
|
175
|
+
}
|
|
176
|
+
function areSkillsInstalled(cwd) {
|
|
177
|
+
const home = homedir();
|
|
178
|
+
const workingDir = cwd || process.cwd();
|
|
179
|
+
const foundPaths = [];
|
|
180
|
+
const globalSkillsDir = join(home, ".agents", "skills");
|
|
181
|
+
const globalSkillPath = join(globalSkillsDir, "hmy", "SKILL.md");
|
|
182
|
+
if (existsSync(globalSkillPath)) {
|
|
183
|
+
foundPaths.push(globalSkillPath);
|
|
184
|
+
return { installed: true, location: "global", paths: foundPaths };
|
|
185
|
+
}
|
|
186
|
+
const claudeGlobalSkill = join(home, ".claude", "skills", "hmy.md");
|
|
187
|
+
if (existsSync(claudeGlobalSkill)) {
|
|
188
|
+
foundPaths.push(claudeGlobalSkill);
|
|
189
|
+
return { installed: true, location: "global", paths: foundPaths };
|
|
190
|
+
}
|
|
191
|
+
const claudeGlobalSkillAlt = join(home, ".claude", "skills", "hmy", "SKILL.md");
|
|
192
|
+
if (existsSync(claudeGlobalSkillAlt)) {
|
|
193
|
+
foundPaths.push(claudeGlobalSkillAlt);
|
|
194
|
+
return { installed: true, location: "global", paths: foundPaths };
|
|
195
|
+
}
|
|
196
|
+
const localSkillPath = join(workingDir, ".claude", "skills", "hmy.md");
|
|
197
|
+
if (existsSync(localSkillPath)) {
|
|
198
|
+
foundPaths.push(localSkillPath);
|
|
199
|
+
return { installed: true, location: "local", paths: foundPaths };
|
|
200
|
+
}
|
|
201
|
+
const localSkillPathAlt = join(workingDir, ".claude", "skills", "hmy", "SKILL.md");
|
|
202
|
+
if (existsSync(localSkillPathAlt)) {
|
|
203
|
+
foundPaths.push(localSkillPathAlt);
|
|
204
|
+
return { installed: true, location: "local", paths: foundPaths };
|
|
205
|
+
}
|
|
206
|
+
return { installed: false, location: null, paths: [] };
|
|
207
|
+
}
|
|
208
|
+
function hasProjectContext(cwd) {
|
|
209
|
+
const localConfig = loadLocalConfig(cwd);
|
|
210
|
+
return !!(localConfig?.workspaceId || localConfig?.projectId);
|
|
211
|
+
}
|
|
212
|
+
function getMemoryDir() {
|
|
213
|
+
const config = loadConfig();
|
|
214
|
+
if (config.memoryDir)
|
|
215
|
+
return config.memoryDir;
|
|
216
|
+
return join(homedir(), ".harmony", "memory");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/graph-expansion.ts
|
|
220
|
+
async function autoExpandGraph(client, entityId, title, content, _tags, workspaceId, projectId, maxRelations = 5) {
|
|
221
|
+
try {
|
|
222
|
+
const contentSnippet = content.slice(0, 200).trim();
|
|
223
|
+
const query = [title, contentSnippet].filter(Boolean).join(" ");
|
|
224
|
+
let candidates = [];
|
|
225
|
+
const { entities } = await client.searchMemoryEntities(workspaceId, query, {
|
|
226
|
+
project_id: projectId,
|
|
227
|
+
limit: 20
|
|
228
|
+
});
|
|
229
|
+
candidates = entities.filter((e) => e.id !== entityId).slice(0, maxRelations);
|
|
230
|
+
if (candidates.length === 0) {
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
232
|
+
const retry = await client.searchMemoryEntities(workspaceId, query, {
|
|
233
|
+
project_id: projectId,
|
|
234
|
+
limit: 20
|
|
235
|
+
});
|
|
236
|
+
candidates = retry.entities.filter((e) => e.id !== entityId).slice(0, maxRelations);
|
|
44
237
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const history = sessionTaskHistory.get(ctx.cardId);
|
|
60
|
-
// Always track step history regardless of rate limit
|
|
61
|
-
if (ctx.currentTask) {
|
|
62
|
-
const previousTask = history?.lastTask || "";
|
|
63
|
-
const similarity = levenshteinSimilarity(previousTask, ctx.currentTask);
|
|
64
|
-
const existingSteps = history?.steps || [];
|
|
65
|
-
if (existingSteps.length === 0 ||
|
|
66
|
-
(similarity < 0.6 && previousTask.length > 0)) {
|
|
67
|
-
const newStep = {
|
|
68
|
-
task: ctx.currentTask,
|
|
69
|
-
progress: ctx.progressPercent ?? 0,
|
|
70
|
-
timestamp: now,
|
|
71
|
-
};
|
|
72
|
-
if (existingSteps.length === 0 && previousTask.length > 0) {
|
|
73
|
-
existingSteps.push({
|
|
74
|
-
task: previousTask,
|
|
75
|
-
progress: 0,
|
|
76
|
-
timestamp: history?.lastExtractionAt ?? now,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
existingSteps.push(newStep);
|
|
80
|
-
sessionTaskHistory.set(ctx.cardId, {
|
|
81
|
-
lastTask: ctx.currentTask,
|
|
82
|
-
lastExtractionAt: history?.lastExtractionAt ?? 0,
|
|
83
|
-
steps: existingSteps,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
238
|
+
let relationsCreated = 0;
|
|
239
|
+
for (const candidate of candidates) {
|
|
240
|
+
try {
|
|
241
|
+
await client.createMemoryRelation({
|
|
242
|
+
source_id: entityId,
|
|
243
|
+
target_id: candidate.id,
|
|
244
|
+
relation_type: "relates_to",
|
|
245
|
+
confidence: 0.6
|
|
246
|
+
});
|
|
247
|
+
relationsCreated++;
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const status = err?.status;
|
|
250
|
+
if (status !== 409) {}
|
|
251
|
+
}
|
|
86
252
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
253
|
+
return { relationsCreated };
|
|
254
|
+
} catch {
|
|
255
|
+
return { relationsCreated: 0 };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function findSimilarEntities(client, title, content, workspaceId, options) {
|
|
259
|
+
const contentSnippet = content.slice(0, 200).trim();
|
|
260
|
+
const query = [title, contentSnippet].filter(Boolean).join(" ");
|
|
261
|
+
try {
|
|
262
|
+
const { entities } = await client.searchMemoryEntities(workspaceId, query, {
|
|
263
|
+
project_id: options?.projectId,
|
|
264
|
+
limit: options?.limit ?? 20,
|
|
265
|
+
type: options?.type
|
|
266
|
+
});
|
|
267
|
+
const minScore = options?.minRrfScore ?? 0;
|
|
268
|
+
const excludeSet = new Set(options?.excludeIds || []);
|
|
269
|
+
return entities.filter((e) => {
|
|
270
|
+
if (excludeSet.has(e.id))
|
|
271
|
+
return false;
|
|
272
|
+
if (minScore > 0 && (e.rrf_score ?? 0) < minScore)
|
|
273
|
+
return false;
|
|
274
|
+
return true;
|
|
275
|
+
});
|
|
276
|
+
} catch {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
var CAUSAL_LOOKUP = [
|
|
281
|
+
{
|
|
282
|
+
sourceType: "error",
|
|
283
|
+
targetType: "solution",
|
|
284
|
+
relation: "resolved_by",
|
|
285
|
+
direction: "forward"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
sourceType: "solution",
|
|
289
|
+
targetType: "error",
|
|
290
|
+
relation: "resolved_by",
|
|
291
|
+
direction: "reverse"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
sourceType: "lesson",
|
|
295
|
+
targetType: "error",
|
|
296
|
+
relation: "learned_from",
|
|
297
|
+
direction: "forward"
|
|
298
|
+
}
|
|
299
|
+
];
|
|
300
|
+
async function linkCrossTypeNeighbors(client, entityId, entityType, title, content, workspaceId, projectId) {
|
|
301
|
+
const rules = CAUSAL_LOOKUP.filter((r) => r.sourceType === entityType);
|
|
302
|
+
if (rules.length === 0)
|
|
303
|
+
return { relationsCreated: 0 };
|
|
304
|
+
let relationsCreated = 0;
|
|
305
|
+
for (const rule of rules) {
|
|
306
|
+
try {
|
|
307
|
+
const matches = await findSimilarEntities(client, title, content, workspaceId, {
|
|
308
|
+
projectId,
|
|
309
|
+
limit: 10,
|
|
310
|
+
minRrfScore: 0.04,
|
|
311
|
+
excludeIds: [entityId],
|
|
312
|
+
type: rule.targetType
|
|
313
|
+
});
|
|
314
|
+
for (const match of matches.slice(0, 3)) {
|
|
315
|
+
const sourceId = rule.direction === "forward" ? entityId : match.id;
|
|
316
|
+
const targetId = rule.direction === "forward" ? match.id : entityId;
|
|
317
|
+
try {
|
|
318
|
+
await client.createMemoryRelation({
|
|
319
|
+
source_id: sourceId,
|
|
320
|
+
target_id: targetId,
|
|
321
|
+
relation_type: rule.relation,
|
|
322
|
+
confidence: 0.65
|
|
323
|
+
});
|
|
324
|
+
relationsCreated++;
|
|
325
|
+
} catch {}
|
|
326
|
+
}
|
|
327
|
+
} catch {}
|
|
328
|
+
}
|
|
329
|
+
return { relationsCreated };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/active-learning.ts
|
|
333
|
+
var CONTRADICTION_TYPES = new Set([
|
|
334
|
+
"decision",
|
|
335
|
+
"pattern",
|
|
336
|
+
"preference",
|
|
337
|
+
"lesson"
|
|
338
|
+
]);
|
|
339
|
+
var sessionTaskHistory = new Map;
|
|
340
|
+
var MID_SESSION_RATE_LIMIT_MS = 120000;
|
|
341
|
+
function levenshteinSimilarity(a, b) {
|
|
342
|
+
if (a === b)
|
|
343
|
+
return 1;
|
|
344
|
+
if (a.length === 0 || b.length === 0)
|
|
345
|
+
return 0;
|
|
346
|
+
const sa = a.slice(0, 200).toLowerCase();
|
|
347
|
+
const sb = b.slice(0, 200).toLowerCase();
|
|
348
|
+
const matrix = [];
|
|
349
|
+
for (let i = 0;i <= sa.length; i++) {
|
|
350
|
+
matrix[i] = [i];
|
|
351
|
+
}
|
|
352
|
+
for (let j = 0;j <= sb.length; j++) {
|
|
353
|
+
matrix[0][j] = j;
|
|
354
|
+
}
|
|
355
|
+
for (let i = 1;i <= sa.length; i++) {
|
|
356
|
+
for (let j = 1;j <= sb.length; j++) {
|
|
357
|
+
const cost = sa[i - 1] === sb[j - 1] ? 0 : 1;
|
|
358
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
93
359
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
360
|
+
}
|
|
361
|
+
const maxLen = Math.max(sa.length, sb.length);
|
|
362
|
+
return 1 - matrix[sa.length][sb.length] / maxLen;
|
|
363
|
+
}
|
|
364
|
+
async function extractMidSessionLearnings(_client, ctx) {
|
|
365
|
+
const workspaceId = getActiveWorkspaceId();
|
|
366
|
+
if (!workspaceId)
|
|
367
|
+
return { count: 0, entityIds: [] };
|
|
368
|
+
const _projectId = getActiveProjectId() || undefined;
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
const _entityIds = [];
|
|
371
|
+
const history = sessionTaskHistory.get(ctx.cardId);
|
|
372
|
+
if (ctx.currentTask) {
|
|
373
|
+
const previousTask = history?.lastTask || "";
|
|
374
|
+
const similarity = levenshteinSimilarity(previousTask, ctx.currentTask);
|
|
375
|
+
const existingSteps = history?.steps || [];
|
|
376
|
+
if (existingSteps.length === 0 || similarity < 0.6 && previousTask.length > 0) {
|
|
377
|
+
const newStep = {
|
|
378
|
+
task: ctx.currentTask,
|
|
379
|
+
progress: ctx.progressPercent ?? 0,
|
|
380
|
+
timestamp: now
|
|
381
|
+
};
|
|
382
|
+
if (existingSteps.length === 0 && previousTask.length > 0) {
|
|
383
|
+
existingSteps.push({
|
|
384
|
+
task: previousTask,
|
|
385
|
+
progress: 0,
|
|
386
|
+
timestamp: history?.lastExtractionAt ?? now
|
|
102
387
|
});
|
|
103
|
-
|
|
388
|
+
}
|
|
389
|
+
existingSteps.push(newStep);
|
|
390
|
+
sessionTaskHistory.set(ctx.cardId, {
|
|
391
|
+
lastTask: ctx.currentTask,
|
|
392
|
+
lastExtractionAt: history?.lastExtractionAt ?? 0,
|
|
393
|
+
steps: existingSteps
|
|
394
|
+
});
|
|
104
395
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const currentHistory = sessionTaskHistory.get(ctx.cardId);
|
|
110
|
-
sessionTaskHistory.set(ctx.cardId, {
|
|
111
|
-
lastTask: ctx.currentTask,
|
|
112
|
-
lastExtractionAt: currentHistory?.lastExtractionAt ?? 0,
|
|
113
|
-
steps: currentHistory?.steps || [],
|
|
114
|
-
});
|
|
396
|
+
}
|
|
397
|
+
if (history && now - history.lastExtractionAt < MID_SESSION_RATE_LIMIT_MS) {
|
|
398
|
+
if (ctx.status !== "blocked" || !ctx.blockers?.length) {
|
|
399
|
+
return { count: 0, entityIds: [] };
|
|
115
400
|
}
|
|
401
|
+
}
|
|
402
|
+
if (ctx.status === "blocked" && ctx.blockers?.length) {
|
|
403
|
+
sessionTaskHistory.set(ctx.cardId, {
|
|
404
|
+
lastTask: ctx.currentTask || "",
|
|
405
|
+
lastExtractionAt: now,
|
|
406
|
+
steps: history?.steps || []
|
|
407
|
+
});
|
|
116
408
|
return { count: 0, entityIds: [] };
|
|
409
|
+
}
|
|
410
|
+
if (ctx.currentTask) {
|
|
411
|
+
const currentHistory = sessionTaskHistory.get(ctx.cardId);
|
|
412
|
+
sessionTaskHistory.set(ctx.cardId, {
|
|
413
|
+
lastTask: ctx.currentTask,
|
|
414
|
+
lastExtractionAt: currentHistory?.lastExtractionAt ?? 0,
|
|
415
|
+
steps: currentHistory?.steps || []
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return { count: 0, entityIds: [] };
|
|
117
419
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
*/
|
|
121
|
-
export function clearMidSessionTracking(cardId) {
|
|
122
|
-
sessionTaskHistory.delete(cardId);
|
|
420
|
+
function clearMidSessionTracking(cardId) {
|
|
421
|
+
sessionTaskHistory.delete(cardId);
|
|
123
422
|
}
|
|
124
|
-
/**
|
|
125
|
-
* Enrich raw steps with progress deltas and key decision detection.
|
|
126
|
-
* A step is a "key decision" if it represents a >20% progress jump.
|
|
127
|
-
*/
|
|
128
423
|
function enrichSteps(steps) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
424
|
+
return steps.map((step, i) => {
|
|
425
|
+
const prevProgress = i > 0 ? steps[i - 1].progress : 0;
|
|
426
|
+
const prevTimestamp = i > 0 ? steps[i - 1].timestamp : step.timestamp;
|
|
427
|
+
const progressDelta = step.progress - prevProgress;
|
|
428
|
+
return {
|
|
429
|
+
task: step.task,
|
|
430
|
+
progress: step.progress,
|
|
431
|
+
progressDelta,
|
|
432
|
+
durationMs: step.timestamp - prevTimestamp,
|
|
433
|
+
isKeyDecision: progressDelta >= 20
|
|
434
|
+
};
|
|
435
|
+
});
|
|
141
436
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Build structured procedure content from session data and enriched steps.
|
|
144
|
-
*/
|
|
145
437
|
function buildProcedureContent(session, enrichedSteps, wikiLinksLine) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
durationInfo,
|
|
180
|
-
wikiLinksLine,
|
|
181
|
-
]
|
|
182
|
-
.filter((line) => line !== undefined)
|
|
183
|
-
.join("\n");
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Extract a new procedure or reinforce an existing similar one.
|
|
187
|
-
*
|
|
188
|
-
* Deduplication: searches for existing procedure entities with similar titles.
|
|
189
|
-
* If found, merges new steps and bumps confidence. Otherwise creates a new one.
|
|
190
|
-
*/
|
|
438
|
+
const triggerLabels = session.cardLabels.length > 0 ? `Labels: ${session.cardLabels.join(", ")}` : "";
|
|
439
|
+
const stepsMarkdown = enrichedSteps.map((s, i) => {
|
|
440
|
+
const marker = s.isKeyDecision ? " **[key step]**" : "";
|
|
441
|
+
const duration = s.durationMs > 0 ? ` (~${Math.round(s.durationMs / 60000)}min)` : "";
|
|
442
|
+
return `${i + 1}. ${s.task} (${s.progress}%, +${s.progressDelta}%)${marker}${duration}`;
|
|
443
|
+
}).join(`
|
|
444
|
+
`);
|
|
445
|
+
const subtaskSection = session.cardSubtasks && session.cardSubtasks.length > 0 ? [
|
|
446
|
+
"",
|
|
447
|
+
"## Subtasks Completed",
|
|
448
|
+
...session.cardSubtasks.map((s) => `- [${s.done ? "x" : " "}] ${s.title}`)
|
|
449
|
+
].join(`
|
|
450
|
+
`) : "";
|
|
451
|
+
const durationInfo = session.sessionDurationMs ? `
|
|
452
|
+
Duration: ${Math.round(session.sessionDurationMs / 60000)} minutes` : "";
|
|
453
|
+
return [
|
|
454
|
+
"## Trigger",
|
|
455
|
+
`When working on: "${session.cardTitle}"`,
|
|
456
|
+
triggerLabels,
|
|
457
|
+
"",
|
|
458
|
+
"## Steps",
|
|
459
|
+
stepsMarkdown,
|
|
460
|
+
subtaskSection,
|
|
461
|
+
"",
|
|
462
|
+
"## Outcome",
|
|
463
|
+
`Completed at ${session.progressPercent ?? "unknown"}%`,
|
|
464
|
+
session.currentTask ? `Final state: ${session.currentTask}` : "",
|
|
465
|
+
`Agent: ${session.agentName}`,
|
|
466
|
+
durationInfo,
|
|
467
|
+
wikiLinksLine
|
|
468
|
+
].filter((line) => line !== undefined).join(`
|
|
469
|
+
`);
|
|
470
|
+
}
|
|
191
471
|
async function extractOrReinforceProcedure(client, session, steps, workspaceId, projectId, wikiLinksLine = "") {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
...(shouldPromote ? { memory_tier: "reference" } : {}),
|
|
243
|
-
});
|
|
244
|
-
return { mode: "reinforced", entityId: fullEntity.id };
|
|
245
|
-
}
|
|
472
|
+
const enrichedSteps = enrichSteps(steps);
|
|
473
|
+
const newContent = buildProcedureContent(session, enrichedSteps, wikiLinksLine);
|
|
474
|
+
const tags = [
|
|
475
|
+
"auto-extracted",
|
|
476
|
+
"procedure",
|
|
477
|
+
...session.cardLabels.slice(0, 3)
|
|
478
|
+
];
|
|
479
|
+
try {
|
|
480
|
+
const similar = await findSimilarEntities(client, `Procedure: ${session.cardTitle}`, newContent, workspaceId, { projectId, limit: 5, minRrfScore: 0.03 });
|
|
481
|
+
const matchingProcedure = similar.find((e) => e.type === "procedure" && (e.rrf_score ?? 0) >= 0.05);
|
|
482
|
+
if (matchingProcedure) {
|
|
483
|
+
const { entity: rawEntity } = await client.getMemoryEntity(matchingProcedure.id);
|
|
484
|
+
const fullEntity = rawEntity;
|
|
485
|
+
const currentMeta = fullEntity.metadata || {};
|
|
486
|
+
const sourceCards = (currentMeta.source_cards || []).slice();
|
|
487
|
+
if (!sourceCards.includes(session.cardId)) {
|
|
488
|
+
sourceCards.push(session.cardId);
|
|
489
|
+
}
|
|
490
|
+
const reuseCount = (currentMeta.reuse_count || 0) + 1;
|
|
491
|
+
const currentConfidence = fullEntity.confidence ?? 0.7;
|
|
492
|
+
const newConfidence = Math.min(0.95, currentConfidence + 0.05);
|
|
493
|
+
const stepsAppendix = enrichedSteps.map((s, i) => `${i + 1}. ${s.task} (${s.progress}%)`).join(`
|
|
494
|
+
`);
|
|
495
|
+
const appendix = `
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
### Execution ${reuseCount + 1}: ${session.cardTitle}
|
|
499
|
+
${stepsAppendix}
|
|
500
|
+
Agent: ${session.agentName} | ${new Date().toISOString().split("T")[0]}`;
|
|
501
|
+
const updatedMeta = {
|
|
502
|
+
...currentMeta,
|
|
503
|
+
reuse_count: reuseCount,
|
|
504
|
+
source_cards: sourceCards,
|
|
505
|
+
last_reinforced_at: new Date().toISOString(),
|
|
506
|
+
step_count: Math.max(currentMeta.step_count || 0, steps.length)
|
|
507
|
+
};
|
|
508
|
+
const shouldPromote = reuseCount >= 2 && fullEntity.memory_tier !== "reference";
|
|
509
|
+
await client.updateMemoryEntity(fullEntity.id, {
|
|
510
|
+
content: (fullEntity.content || "") + appendix,
|
|
511
|
+
confidence: newConfidence,
|
|
512
|
+
metadata: {
|
|
513
|
+
...updatedMeta,
|
|
514
|
+
...shouldPromote ? {
|
|
515
|
+
promoted_reason: `Reinforced by ${reuseCount + 1} successful sessions`,
|
|
516
|
+
promoted_at: new Date().toISOString()
|
|
517
|
+
} : {}
|
|
518
|
+
},
|
|
519
|
+
...shouldPromote ? { memory_tier: "reference" } : {}
|
|
520
|
+
});
|
|
521
|
+
return { mode: "reinforced", entityId: fullEntity.id };
|
|
246
522
|
}
|
|
247
|
-
|
|
248
|
-
|
|
523
|
+
} catch {}
|
|
524
|
+
return {
|
|
525
|
+
mode: "created",
|
|
526
|
+
learning: {
|
|
527
|
+
title: `Procedure: ${session.cardTitle}`,
|
|
528
|
+
content: newContent,
|
|
529
|
+
type: "procedure",
|
|
530
|
+
tier: "episode",
|
|
531
|
+
confidence: 0.7,
|
|
532
|
+
tags,
|
|
533
|
+
metadata: {
|
|
534
|
+
source: "active_learning",
|
|
535
|
+
card_id: session.cardId,
|
|
536
|
+
source_cards: [session.cardId],
|
|
537
|
+
step_count: steps.length,
|
|
538
|
+
key_step_count: enrichedSteps.filter((s) => s.isKeyDecision).length,
|
|
539
|
+
reuse_count: 0
|
|
540
|
+
}
|
|
249
541
|
}
|
|
250
|
-
|
|
251
|
-
return {
|
|
252
|
-
mode: "created",
|
|
253
|
-
learning: {
|
|
254
|
-
title: `Procedure: ${session.cardTitle}`,
|
|
255
|
-
content: newContent,
|
|
256
|
-
type: "procedure",
|
|
257
|
-
tier: "episode",
|
|
258
|
-
confidence: 0.7,
|
|
259
|
-
tags,
|
|
260
|
-
metadata: {
|
|
261
|
-
source: "active_learning",
|
|
262
|
-
card_id: session.cardId,
|
|
263
|
-
source_cards: [session.cardId],
|
|
264
|
-
step_count: steps.length,
|
|
265
|
-
key_step_count: enrichedSteps.filter((s) => s.isKeyDecision).length,
|
|
266
|
-
reuse_count: 0,
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
};
|
|
542
|
+
};
|
|
270
543
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
confidence: 0.75,
|
|
285
|
-
},
|
|
544
|
+
var SESSION_CAUSAL_RULES = [
|
|
545
|
+
{
|
|
546
|
+
sourceType: "error",
|
|
547
|
+
targetType: "solution",
|
|
548
|
+
relation: "resolved_by",
|
|
549
|
+
confidence: 0.8
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
sourceType: "lesson",
|
|
553
|
+
targetType: "error",
|
|
554
|
+
relation: "learned_from",
|
|
555
|
+
confidence: 0.75
|
|
556
|
+
}
|
|
286
557
|
];
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
*/
|
|
295
|
-
export async function linkSessionEntities(client, createdPairs, _workspaceId, _projectId) {
|
|
296
|
-
let relationsCreated = 0;
|
|
297
|
-
for (const rule of SESSION_CAUSAL_RULES) {
|
|
298
|
-
const sources = createdPairs.filter((p) => p.learning.type === rule.sourceType);
|
|
299
|
-
const targets = createdPairs.filter((p) => p.learning.type === rule.targetType);
|
|
300
|
-
for (const source of sources) {
|
|
301
|
-
for (const target of targets) {
|
|
302
|
-
try {
|
|
303
|
-
await client.createMemoryRelation({
|
|
304
|
-
source_id: source.id,
|
|
305
|
-
target_id: target.id,
|
|
306
|
-
relation_type: rule.relation,
|
|
307
|
-
confidence: rule.confidence,
|
|
308
|
-
});
|
|
309
|
-
relationsCreated++;
|
|
310
|
-
}
|
|
311
|
-
catch {
|
|
312
|
-
// Skip duplicate/failed relations silently
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return { relationsCreated };
|
|
318
|
-
}
|
|
319
|
-
// --- Session-End Learning ---
|
|
320
|
-
/**
|
|
321
|
-
* Extract learnings from a completed agent session.
|
|
322
|
-
* Returns the number of memories created.
|
|
323
|
-
*/
|
|
324
|
-
export async function extractLearnings(client, session) {
|
|
325
|
-
const workspaceId = getActiveWorkspaceId();
|
|
326
|
-
if (!workspaceId) {
|
|
327
|
-
return { count: 0, entityIds: [] };
|
|
328
|
-
}
|
|
329
|
-
const projectId = getActiveProjectId() || undefined;
|
|
330
|
-
const learnings = [];
|
|
331
|
-
// Search for related entities to enrich summaries with wiki-links
|
|
332
|
-
let relatedEntityTitles = [];
|
|
333
|
-
if (workspaceId) {
|
|
558
|
+
async function linkSessionEntities(client, createdPairs, _workspaceId, _projectId) {
|
|
559
|
+
let relationsCreated = 0;
|
|
560
|
+
for (const rule of SESSION_CAUSAL_RULES) {
|
|
561
|
+
const sources = createdPairs.filter((p) => p.learning.type === rule.sourceType);
|
|
562
|
+
const targets = createdPairs.filter((p) => p.learning.type === rule.targetType);
|
|
563
|
+
for (const source of sources) {
|
|
564
|
+
for (const target of targets) {
|
|
334
565
|
try {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
/* non-fatal */
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
const wikiLinksLine = relatedEntityTitles.length > 0
|
|
350
|
-
? `\nRelated: ${relatedEntityTitles.map((t) => `[[${t}]]`).join(", ")}`
|
|
351
|
-
: "";
|
|
352
|
-
// Rule 1: Session had blockers → create error entity (only for substantial blockers)
|
|
353
|
-
// Skip trivial blocker strings — only store if the blocker text contains
|
|
354
|
-
// enough detail to be useful to a future agent (>80 chars).
|
|
355
|
-
if (session.blockers && session.blockers.length > 0) {
|
|
356
|
-
for (const blocker of session.blockers) {
|
|
357
|
-
if (blocker.length < 80)
|
|
358
|
-
continue; // Skip trivial blockers like "stuck" or "waiting on API"
|
|
359
|
-
// Dedup: check if a similar error entity already exists
|
|
360
|
-
let isDuplicate = false;
|
|
361
|
-
try {
|
|
362
|
-
const similar = await findSimilarEntities(client, blocker.slice(0, 200), blocker, workspaceId, { projectId, limit: 3, minRrfScore: 0.05 });
|
|
363
|
-
isDuplicate = similar.some((e) => e.type === "error" && (e.rrf_score ?? 0) >= 0.06);
|
|
364
|
-
}
|
|
365
|
-
catch {
|
|
366
|
-
/* non-fatal */
|
|
367
|
-
}
|
|
368
|
-
if (!isDuplicate) {
|
|
369
|
-
learnings.push({
|
|
370
|
-
title: `Blocker: ${blocker.slice(0, 100)}`,
|
|
371
|
-
content: `Encountered while working on "${session.cardTitle}":\n\n${blocker}\n\nAgent: ${session.agentName}\nSession status: ${session.status}`,
|
|
372
|
-
type: "error",
|
|
373
|
-
tier: "episode",
|
|
374
|
-
confidence: 0.6,
|
|
375
|
-
tags: [
|
|
376
|
-
"auto-extracted",
|
|
377
|
-
"blocker",
|
|
378
|
-
...session.cardLabels.slice(0, 3),
|
|
379
|
-
],
|
|
380
|
-
metadata: {
|
|
381
|
-
source: "active_learning",
|
|
382
|
-
card_id: session.cardId,
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
566
|
+
await client.createMemoryRelation({
|
|
567
|
+
source_id: source.id,
|
|
568
|
+
target_id: target.id,
|
|
569
|
+
relation_type: rule.relation,
|
|
570
|
+
confidence: rule.confidence
|
|
571
|
+
});
|
|
572
|
+
relationsCreated++;
|
|
573
|
+
} catch {}
|
|
574
|
+
}
|
|
387
575
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
576
|
+
}
|
|
577
|
+
return { relationsCreated };
|
|
578
|
+
}
|
|
579
|
+
async function extractLearnings(client, session) {
|
|
580
|
+
const workspaceId = getActiveWorkspaceId();
|
|
581
|
+
if (!workspaceId) {
|
|
582
|
+
return { count: 0, entityIds: [] };
|
|
583
|
+
}
|
|
584
|
+
const projectId = getActiveProjectId() || undefined;
|
|
585
|
+
const learnings = [];
|
|
586
|
+
let relatedEntityTitles = [];
|
|
587
|
+
if (workspaceId) {
|
|
588
|
+
try {
|
|
589
|
+
const searchQuery = [
|
|
590
|
+
session.cardTitle,
|
|
591
|
+
session.currentTask || "",
|
|
592
|
+
...session.cardLabels
|
|
593
|
+
].filter(Boolean).join(" ");
|
|
594
|
+
const similar = await findSimilarEntities(client, searchQuery, session.currentTask || session.cardTitle, workspaceId, { projectId, limit: 5, minRrfScore: 0.01 });
|
|
595
|
+
relatedEntityTitles = similar.slice(0, 3).map((e) => e.title);
|
|
596
|
+
} catch {}
|
|
597
|
+
}
|
|
598
|
+
const wikiLinksLine = relatedEntityTitles.length > 0 ? `
|
|
599
|
+
Related: ${relatedEntityTitles.map((t) => `[[${t}]]`).join(", ")}` : "";
|
|
600
|
+
if (session.blockers && session.blockers.length > 0) {
|
|
601
|
+
for (const blocker of session.blockers) {
|
|
602
|
+
if (blocker.length < 80)
|
|
603
|
+
continue;
|
|
604
|
+
let isDuplicate = false;
|
|
605
|
+
try {
|
|
606
|
+
const similar = await findSimilarEntities(client, blocker.slice(0, 200), blocker, workspaceId, { projectId, limit: 3, minRrfScore: 0.05 });
|
|
607
|
+
isDuplicate = similar.some((e) => e.type === "error" && (e.rrf_score ?? 0) >= 0.06);
|
|
608
|
+
} catch {}
|
|
609
|
+
if (!isDuplicate) {
|
|
396
610
|
learnings.push({
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
tags: [
|
|
417
|
-
"auto-extracted",
|
|
418
|
-
"session-paused",
|
|
419
|
-
...session.cardLabels.slice(0, 3),
|
|
420
|
-
],
|
|
421
|
-
metadata: {
|
|
422
|
-
source: "active_learning",
|
|
423
|
-
card_id: session.cardId,
|
|
424
|
-
},
|
|
611
|
+
title: `Blocker: ${blocker.slice(0, 100)}`,
|
|
612
|
+
content: `Encountered while working on "${session.cardTitle}":
|
|
613
|
+
|
|
614
|
+
${blocker}
|
|
615
|
+
|
|
616
|
+
Agent: ${session.agentName}
|
|
617
|
+
Session status: ${session.status}`,
|
|
618
|
+
type: "error",
|
|
619
|
+
tier: "episode",
|
|
620
|
+
confidence: 0.6,
|
|
621
|
+
tags: [
|
|
622
|
+
"auto-extracted",
|
|
623
|
+
"blocker",
|
|
624
|
+
...session.cardLabels.slice(0, 3)
|
|
625
|
+
],
|
|
626
|
+
metadata: {
|
|
627
|
+
source: "active_learning",
|
|
628
|
+
card_id: session.cardId
|
|
629
|
+
}
|
|
425
630
|
});
|
|
631
|
+
}
|
|
426
632
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
agent_identifier: session.agentIdentifier,
|
|
474
|
-
});
|
|
475
|
-
const entity = result.entity;
|
|
476
|
-
if (entity?.id) {
|
|
477
|
-
entityIds.push(entity.id);
|
|
478
|
-
createdPairs.push({ id: entity.id, learning });
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
482
|
-
// Non-fatal: individual learning extraction failure shouldn't block others
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
// Auto-expand the knowledge graph and detect contradictions for each newly created entity (fire-and-forget)
|
|
486
|
-
for (const { id, learning } of createdPairs) {
|
|
487
|
-
autoExpandGraph(client, id, learning.title, learning.content, learning.tags, workspaceId, projectId).catch(() => { });
|
|
488
|
-
detectContradictions(client, id, learning.type, learning.title, learning.content, learning.tags, workspaceId, projectId).catch(() => { });
|
|
489
|
-
// Cross-type causal linking (e.g., new error → existing solutions)
|
|
490
|
-
linkCrossTypeNeighbors(client, id, learning.type, learning.title, learning.content, workspaceId, projectId).catch(() => { });
|
|
491
|
-
}
|
|
492
|
-
// Same-session causal linking (e.g., error + solution in same session → resolved_by)
|
|
493
|
-
if (createdPairs.length >= 2) {
|
|
494
|
-
linkSessionEntities(client, createdPairs, workspaceId, projectId).catch(() => { });
|
|
633
|
+
}
|
|
634
|
+
if (session.status === "paused" && (session.blockers?.length ?? 0) > 0) {
|
|
635
|
+
const durationInfo = session.sessionDurationMs ? `
|
|
636
|
+
Duration: ${Math.round(session.sessionDurationMs / 60000)} minutes` : "";
|
|
637
|
+
learnings.push({
|
|
638
|
+
title: `Paused: ${session.cardTitle}`,
|
|
639
|
+
content: [
|
|
640
|
+
`Paused work on "${session.cardTitle}".`,
|
|
641
|
+
session.currentTask ? `Last task: ${session.currentTask}` : "",
|
|
642
|
+
session.progressPercent !== undefined ? `Progress: ${session.progressPercent}%` : "",
|
|
643
|
+
durationInfo,
|
|
644
|
+
session.blockers?.length ? `Blockers: ${session.blockers.join("; ")}` : "",
|
|
645
|
+
`
|
|
646
|
+
Agent: ${session.agentName}`,
|
|
647
|
+
wikiLinksLine
|
|
648
|
+
].filter(Boolean).join(`
|
|
649
|
+
`),
|
|
650
|
+
type: "lesson",
|
|
651
|
+
tier: "draft",
|
|
652
|
+
confidence: 0.6,
|
|
653
|
+
tags: [
|
|
654
|
+
"auto-extracted",
|
|
655
|
+
"session-paused",
|
|
656
|
+
...session.cardLabels.slice(0, 3)
|
|
657
|
+
],
|
|
658
|
+
metadata: {
|
|
659
|
+
source: "active_learning",
|
|
660
|
+
card_id: session.cardId
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
const entityIds = [];
|
|
665
|
+
const stepHistory = sessionTaskHistory.get(session.cardId);
|
|
666
|
+
const MIN_PROCEDURE_STEPS = 5;
|
|
667
|
+
const MIN_PROCEDURE_DURATION_MS = 10 * 60 * 1000;
|
|
668
|
+
const hasEnoughSteps = stepHistory && stepHistory.steps.length >= MIN_PROCEDURE_STEPS;
|
|
669
|
+
const hasMinDuration = (session.sessionDurationMs ?? 0) >= MIN_PROCEDURE_DURATION_MS;
|
|
670
|
+
const isSuccessful = session.status === "completed" && (session.progressPercent === undefined || session.progressPercent >= 85) && !session.blockers?.length;
|
|
671
|
+
if (isSuccessful && hasEnoughSteps && hasMinDuration) {
|
|
672
|
+
const procedureResult = await extractOrReinforceProcedure(client, session, stepHistory.steps, workspaceId, projectId, wikiLinksLine);
|
|
673
|
+
if (procedureResult) {
|
|
674
|
+
if (procedureResult.mode === "created") {
|
|
675
|
+
learnings.push(procedureResult.learning);
|
|
676
|
+
} else {
|
|
677
|
+
entityIds.push(procedureResult.entityId);
|
|
678
|
+
}
|
|
495
679
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
680
|
+
}
|
|
681
|
+
const createdPairs = [];
|
|
682
|
+
for (const learning of learnings) {
|
|
683
|
+
try {
|
|
684
|
+
const metadata = {
|
|
685
|
+
...learning.metadata
|
|
686
|
+
};
|
|
687
|
+
const result = await client.createMemoryEntity({
|
|
688
|
+
workspace_id: workspaceId,
|
|
689
|
+
project_id: projectId,
|
|
690
|
+
type: learning.type,
|
|
691
|
+
scope: "project",
|
|
692
|
+
memory_tier: learning.tier,
|
|
693
|
+
title: learning.title,
|
|
694
|
+
content: learning.content,
|
|
695
|
+
confidence: learning.confidence,
|
|
696
|
+
tags: learning.tags,
|
|
697
|
+
metadata,
|
|
698
|
+
agent_identifier: session.agentIdentifier
|
|
699
|
+
});
|
|
700
|
+
const entity = result.entity;
|
|
701
|
+
if (entity?.id) {
|
|
702
|
+
entityIds.push(entity.id);
|
|
703
|
+
createdPairs.push({ id: entity.id, learning });
|
|
704
|
+
}
|
|
705
|
+
} catch {}
|
|
706
|
+
}
|
|
707
|
+
for (const { id, learning } of createdPairs) {
|
|
708
|
+
autoExpandGraph(client, id, learning.title, learning.content, learning.tags, workspaceId, projectId).catch(() => {});
|
|
709
|
+
detectContradictions(client, id, learning.type, learning.title, learning.content, learning.tags, workspaceId, projectId).catch(() => {});
|
|
710
|
+
linkCrossTypeNeighbors(client, id, learning.type, learning.title, learning.content, workspaceId, projectId).catch(() => {});
|
|
711
|
+
}
|
|
712
|
+
if (createdPairs.length >= 2) {
|
|
713
|
+
linkSessionEntities(client, createdPairs, workspaceId, projectId).catch(() => {});
|
|
714
|
+
}
|
|
715
|
+
clearMidSessionTracking(session.cardId);
|
|
716
|
+
return { count: entityIds.length, entityIds };
|
|
717
|
+
}
|
|
718
|
+
var PATTERN_THRESHOLD = 3;
|
|
719
|
+
async function detectAndCreatePatterns(client, newEntityIds, session, workspaceId, projectId) {
|
|
720
|
+
const patternEntityIds = [];
|
|
721
|
+
for (const newEntityId of newEntityIds) {
|
|
722
|
+
try {
|
|
723
|
+
const { entity: rawEntity } = await client.getMemoryEntity(newEntityId);
|
|
724
|
+
const entity = rawEntity;
|
|
725
|
+
if (!entity?.type)
|
|
726
|
+
continue;
|
|
727
|
+
const similar = await findSimilarEntities(client, entity.title, entity.content, workspaceId, { projectId, limit: 30, minRrfScore: 0.01 });
|
|
728
|
+
const existing = similar.filter((c) => !newEntityIds.includes(c.id) && c.type === entity.type);
|
|
729
|
+
if (existing.length < PATTERN_THRESHOLD)
|
|
730
|
+
continue;
|
|
731
|
+
const memberTitles = [
|
|
732
|
+
entity.title,
|
|
733
|
+
...existing.slice(0, 4).map((e) => e.title)
|
|
734
|
+
];
|
|
735
|
+
const patternTitle = `Pattern: recurring ${entity.type} (${existing.length + 1} instances)`;
|
|
736
|
+
const { entities: existingPatterns } = await client.listMemoryEntities({
|
|
737
|
+
workspace_id: workspaceId,
|
|
738
|
+
project_id: projectId,
|
|
739
|
+
type: "pattern",
|
|
740
|
+
limit: 10
|
|
741
|
+
});
|
|
742
|
+
const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_type === entity.type);
|
|
743
|
+
let patternId = null;
|
|
744
|
+
if (matchingPattern) {
|
|
745
|
+
patternId = matchingPattern.id;
|
|
746
|
+
await client.updateMemoryEntity(patternId, {
|
|
747
|
+
content: `Recurring pattern: ${entity.type} entities appearing ${existing.length + 1} times.
|
|
748
|
+
|
|
749
|
+
Members:
|
|
750
|
+
${memberTitles.map((t) => `- ${t}`).join(`
|
|
751
|
+
`)}
|
|
752
|
+
|
|
753
|
+
Last updated: ${new Date().toISOString()}`,
|
|
754
|
+
metadata: {
|
|
755
|
+
pattern_count: existing.length + 1,
|
|
756
|
+
pattern_type: entity.type,
|
|
757
|
+
last_updated: new Date().toISOString()
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
} else {
|
|
761
|
+
const result = await client.createMemoryEntity({
|
|
762
|
+
workspace_id: workspaceId,
|
|
763
|
+
project_id: projectId,
|
|
764
|
+
type: "pattern",
|
|
765
|
+
scope: "project",
|
|
766
|
+
memory_tier: "reference",
|
|
767
|
+
title: patternTitle,
|
|
768
|
+
content: `Recurring pattern: ${entity.type} entities detected ${existing.length + 1} times.
|
|
769
|
+
|
|
770
|
+
Members:
|
|
771
|
+
${memberTitles.map((t) => `- ${t}`).join(`
|
|
772
|
+
`)}`,
|
|
773
|
+
confidence: 0.75,
|
|
774
|
+
tags: ["auto-extracted", "pattern", entity.type],
|
|
775
|
+
metadata: {
|
|
776
|
+
source: "pattern_detection",
|
|
777
|
+
pattern_type: entity.type,
|
|
778
|
+
pattern_count: existing.length + 1
|
|
779
|
+
},
|
|
780
|
+
agent_identifier: session.agentIdentifier
|
|
781
|
+
});
|
|
782
|
+
const created = result.entity;
|
|
783
|
+
if (created?.id) {
|
|
784
|
+
patternId = created.id;
|
|
785
|
+
patternEntityIds.push(patternId);
|
|
600
786
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
/**
|
|
607
|
-
* Detect recurring error→solution chains across sessions.
|
|
608
|
-
*
|
|
609
|
-
* When a session produces both error and solution entities, searches for
|
|
610
|
-
* similar errors from other sessions that also have `resolved_by` relations.
|
|
611
|
-
* If ≥3 such pairs exist, creates a `pattern` entity to capture the
|
|
612
|
-
* recurring causal chain.
|
|
613
|
-
*/
|
|
614
|
-
export async function detectCausalPatterns(client, createdPairs, session, workspaceId, projectId) {
|
|
615
|
-
const patternIds = [];
|
|
616
|
-
// Find error+solution pairs from this session
|
|
617
|
-
const errors = createdPairs.filter((p) => p.learning.type === "error");
|
|
618
|
-
const solutions = createdPairs.filter((p) => p.learning.type === "solution");
|
|
619
|
-
if (errors.length === 0 || solutions.length === 0)
|
|
620
|
-
return patternIds;
|
|
621
|
-
for (const errorPair of errors) {
|
|
787
|
+
}
|
|
788
|
+
if (!patternId)
|
|
789
|
+
continue;
|
|
790
|
+
const toLink = [newEntityId, ...existing.slice(0, 4).map((e) => e.id)];
|
|
791
|
+
for (const sourceId of toLink) {
|
|
622
792
|
try {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
try {
|
|
635
|
-
const { outgoing } = await client.getRelatedEntities(similar.id);
|
|
636
|
-
const resolvedByRel = outgoing.find((r) => r.relation_type === "resolved_by");
|
|
637
|
-
if (resolvedByRel) {
|
|
638
|
-
resolvedErrors.push({
|
|
639
|
-
errorId: similar.id,
|
|
640
|
-
errorTitle: similar.title,
|
|
641
|
-
solutionTitle: resolvedByRel.target_title ||
|
|
642
|
-
"unknown",
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
catch {
|
|
647
|
-
// Non-fatal: skip entities where relation lookup fails
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
// Need at least CAUSAL_PATTERN_THRESHOLD similar resolved errors (including current)
|
|
651
|
-
if (resolvedErrors.length + 1 < CAUSAL_PATTERN_THRESHOLD)
|
|
652
|
-
continue;
|
|
653
|
-
// Check if a causal pattern already exists for this domain
|
|
654
|
-
const { entities: existingPatterns } = await client.listMemoryEntities({
|
|
655
|
-
workspace_id: workspaceId,
|
|
656
|
-
project_id: projectId,
|
|
657
|
-
type: "pattern",
|
|
658
|
-
limit: 10,
|
|
659
|
-
});
|
|
660
|
-
const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_chain_type === "error_resolved_by_solution");
|
|
661
|
-
if (matchingPattern) {
|
|
662
|
-
// Update existing causal pattern
|
|
663
|
-
await client.updateMemoryEntity(matchingPattern.id, {
|
|
664
|
-
content: [
|
|
665
|
-
`Recurring error→solution chain detected (${resolvedErrors.length + 1} instances).`,
|
|
666
|
-
"",
|
|
667
|
-
"## Error→Solution Pairs",
|
|
668
|
-
`- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
|
|
669
|
-
...resolvedErrors
|
|
670
|
-
.slice(0, 5)
|
|
671
|
-
.map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`),
|
|
672
|
-
"",
|
|
673
|
-
`Last updated: ${new Date().toISOString()}`,
|
|
674
|
-
].join("\n"),
|
|
675
|
-
metadata: {
|
|
676
|
-
pattern_chain_type: "error_resolved_by_solution",
|
|
677
|
-
pattern_count: resolvedErrors.length + 1,
|
|
678
|
-
last_updated: new Date().toISOString(),
|
|
679
|
-
},
|
|
680
|
-
});
|
|
681
|
-
// Link current error+solution to the pattern
|
|
682
|
-
for (const pair of [errorPair, solutions[0]]) {
|
|
683
|
-
try {
|
|
684
|
-
await client.createMemoryRelation({
|
|
685
|
-
source_id: pair.id,
|
|
686
|
-
target_id: matchingPattern.id,
|
|
687
|
-
relation_type: "part_of",
|
|
688
|
-
confidence: 0.75,
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
catch {
|
|
692
|
-
// Skip duplicate relations
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
else {
|
|
697
|
-
// Create new causal pattern
|
|
698
|
-
const result = await client.createMemoryEntity({
|
|
699
|
-
workspace_id: workspaceId,
|
|
700
|
-
project_id: projectId,
|
|
701
|
-
type: "pattern",
|
|
702
|
-
scope: "project",
|
|
703
|
-
memory_tier: "reference",
|
|
704
|
-
title: `Pattern: recurring error→solution chain (${resolvedErrors.length + 1} instances)`,
|
|
705
|
-
content: [
|
|
706
|
-
`Recurring error→solution chain detected across ${resolvedErrors.length + 1} sessions.`,
|
|
707
|
-
"",
|
|
708
|
-
"## Error→Solution Pairs",
|
|
709
|
-
`- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
|
|
710
|
-
...resolvedErrors
|
|
711
|
-
.slice(0, 5)
|
|
712
|
-
.map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`),
|
|
713
|
-
].join("\n"),
|
|
714
|
-
confidence: 0.8,
|
|
715
|
-
tags: ["auto-extracted", "pattern", "causal-chain"],
|
|
716
|
-
metadata: {
|
|
717
|
-
source: "causal_pattern_detection",
|
|
718
|
-
pattern_chain_type: "error_resolved_by_solution",
|
|
719
|
-
pattern_count: resolvedErrors.length + 1,
|
|
720
|
-
},
|
|
721
|
-
agent_identifier: session.agentIdentifier,
|
|
722
|
-
});
|
|
723
|
-
const created = result.entity;
|
|
724
|
-
if (created?.id) {
|
|
725
|
-
patternIds.push(created.id);
|
|
726
|
-
// Link members to the pattern
|
|
727
|
-
const memberIds = [
|
|
728
|
-
errorPair.id,
|
|
729
|
-
solutions[0].id,
|
|
730
|
-
...resolvedErrors.slice(0, 4).map((r) => r.errorId),
|
|
731
|
-
];
|
|
732
|
-
for (const memberId of memberIds) {
|
|
733
|
-
try {
|
|
734
|
-
await client.createMemoryRelation({
|
|
735
|
-
source_id: memberId,
|
|
736
|
-
target_id: created.id,
|
|
737
|
-
relation_type: "part_of",
|
|
738
|
-
confidence: 0.75,
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
catch {
|
|
742
|
-
// Skip duplicate relations
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
catch {
|
|
749
|
-
// Non-fatal: causal pattern detection failure should not block anything
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
return patternIds;
|
|
793
|
+
await client.createMemoryRelation({
|
|
794
|
+
source_id: sourceId,
|
|
795
|
+
target_id: patternId,
|
|
796
|
+
relation_type: "part_of",
|
|
797
|
+
confidence: 0.75
|
|
798
|
+
});
|
|
799
|
+
} catch {}
|
|
800
|
+
}
|
|
801
|
+
} catch {}
|
|
802
|
+
}
|
|
803
|
+
return patternEntityIds;
|
|
753
804
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
*/
|
|
763
|
-
export async function detectContradictions(client, entityId, entityType, title, content, tags, workspaceId, projectId) {
|
|
764
|
-
// Only types that can participate in contradicts relations
|
|
765
|
-
if (!CONTRADICTION_TYPES.has(entityType))
|
|
766
|
-
return [];
|
|
767
|
-
// Need at least a title to search
|
|
768
|
-
if (!title.trim())
|
|
769
|
-
return [];
|
|
805
|
+
var CAUSAL_PATTERN_THRESHOLD = 3;
|
|
806
|
+
async function detectCausalPatterns(client, createdPairs, session, workspaceId, projectId) {
|
|
807
|
+
const patternIds = [];
|
|
808
|
+
const errors = createdPairs.filter((p) => p.learning.type === "error");
|
|
809
|
+
const solutions = createdPairs.filter((p) => p.learning.type === "solution");
|
|
810
|
+
if (errors.length === 0 || solutions.length === 0)
|
|
811
|
+
return patternIds;
|
|
812
|
+
for (const errorPair of errors) {
|
|
770
813
|
try {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
814
|
+
const similarErrors = await findSimilarEntities(client, errorPair.learning.title, errorPair.learning.content, workspaceId, {
|
|
815
|
+
projectId,
|
|
816
|
+
limit: 20,
|
|
817
|
+
minRrfScore: 0.03,
|
|
818
|
+
excludeIds: createdPairs.map((p) => p.id),
|
|
819
|
+
type: "error"
|
|
820
|
+
});
|
|
821
|
+
const resolvedErrors = [];
|
|
822
|
+
for (const similar of similarErrors.slice(0, 10)) {
|
|
823
|
+
try {
|
|
824
|
+
const { outgoing } = await client.getRelatedEntities(similar.id);
|
|
825
|
+
const resolvedByRel = outgoing.find((r) => r.relation_type === "resolved_by");
|
|
826
|
+
if (resolvedByRel) {
|
|
827
|
+
resolvedErrors.push({
|
|
828
|
+
errorId: similar.id,
|
|
829
|
+
errorTitle: similar.title,
|
|
830
|
+
solutionTitle: resolvedByRel.target_title || "unknown"
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
} catch {}
|
|
834
|
+
}
|
|
835
|
+
if (resolvedErrors.length + 1 < CAUSAL_PATTERN_THRESHOLD)
|
|
836
|
+
continue;
|
|
837
|
+
const { entities: existingPatterns } = await client.listMemoryEntities({
|
|
838
|
+
workspace_id: workspaceId,
|
|
839
|
+
project_id: projectId,
|
|
840
|
+
type: "pattern",
|
|
841
|
+
limit: 10
|
|
842
|
+
});
|
|
843
|
+
const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_chain_type === "error_resolved_by_solution");
|
|
844
|
+
if (matchingPattern) {
|
|
845
|
+
await client.updateMemoryEntity(matchingPattern.id, {
|
|
846
|
+
content: [
|
|
847
|
+
`Recurring error→solution chain detected (${resolvedErrors.length + 1} instances).`,
|
|
848
|
+
"",
|
|
849
|
+
"## Error→Solution Pairs",
|
|
850
|
+
`- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
|
|
851
|
+
...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`),
|
|
852
|
+
"",
|
|
853
|
+
`Last updated: ${new Date().toISOString()}`
|
|
854
|
+
].join(`
|
|
855
|
+
`),
|
|
856
|
+
metadata: {
|
|
857
|
+
pattern_chain_type: "error_resolved_by_solution",
|
|
858
|
+
pattern_count: resolvedErrors.length + 1,
|
|
859
|
+
last_updated: new Date().toISOString()
|
|
860
|
+
}
|
|
775
861
|
});
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const contradictions = [];
|
|
784
|
-
for (const candidate of candidates) {
|
|
785
|
-
// Skip entities with very high similarity — they're duplicates/supporting, not contradictions
|
|
786
|
-
if ((candidate.rrf_score ?? 0) > 0.8)
|
|
787
|
-
continue;
|
|
788
|
-
// Skip entities with very low similarity — they're unrelated
|
|
789
|
-
if ((candidate.rrf_score ?? 0) < 0.02)
|
|
790
|
-
continue;
|
|
791
|
-
// Check tag overlap: need at least 1 shared tag to indicate same domain
|
|
792
|
-
const sharedTags = tags.filter((t) => candidate.tags?.includes(t));
|
|
793
|
-
if (tags.length > 0 && sharedTags.length === 0)
|
|
794
|
-
continue;
|
|
795
|
-
contradictions.push({
|
|
796
|
-
entityId: candidate.id,
|
|
797
|
-
title: candidate.title,
|
|
798
|
-
type: candidate.type,
|
|
799
|
-
tags: candidate.tags || [],
|
|
862
|
+
for (const pair of [errorPair, solutions[0]]) {
|
|
863
|
+
try {
|
|
864
|
+
await client.createMemoryRelation({
|
|
865
|
+
source_id: pair.id,
|
|
866
|
+
target_id: matchingPattern.id,
|
|
867
|
+
relation_type: "part_of",
|
|
868
|
+
confidence: 0.75
|
|
800
869
|
});
|
|
870
|
+
} catch {}
|
|
801
871
|
}
|
|
802
|
-
|
|
803
|
-
|
|
872
|
+
} else {
|
|
873
|
+
const result = await client.createMemoryEntity({
|
|
874
|
+
workspace_id: workspaceId,
|
|
875
|
+
project_id: projectId,
|
|
876
|
+
type: "pattern",
|
|
877
|
+
scope: "project",
|
|
878
|
+
memory_tier: "reference",
|
|
879
|
+
title: `Pattern: recurring error→solution chain (${resolvedErrors.length + 1} instances)`,
|
|
880
|
+
content: [
|
|
881
|
+
`Recurring error→solution chain detected across ${resolvedErrors.length + 1} sessions.`,
|
|
882
|
+
"",
|
|
883
|
+
"## Error→Solution Pairs",
|
|
884
|
+
`- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
|
|
885
|
+
...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`)
|
|
886
|
+
].join(`
|
|
887
|
+
`),
|
|
888
|
+
confidence: 0.8,
|
|
889
|
+
tags: ["auto-extracted", "pattern", "causal-chain"],
|
|
890
|
+
metadata: {
|
|
891
|
+
source: "causal_pattern_detection",
|
|
892
|
+
pattern_chain_type: "error_resolved_by_solution",
|
|
893
|
+
pattern_count: resolvedErrors.length + 1
|
|
894
|
+
},
|
|
895
|
+
agent_identifier: session.agentIdentifier
|
|
896
|
+
});
|
|
897
|
+
const created = result.entity;
|
|
898
|
+
if (created?.id) {
|
|
899
|
+
patternIds.push(created.id);
|
|
900
|
+
const memberIds = [
|
|
901
|
+
errorPair.id,
|
|
902
|
+
solutions[0].id,
|
|
903
|
+
...resolvedErrors.slice(0, 4).map((r) => r.errorId)
|
|
904
|
+
];
|
|
905
|
+
for (const memberId of memberIds) {
|
|
804
906
|
try {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// Skip duplicate/failed relations silently (409 Conflict is expected)
|
|
814
|
-
}
|
|
907
|
+
await client.createMemoryRelation({
|
|
908
|
+
source_id: memberId,
|
|
909
|
+
target_id: created.id,
|
|
910
|
+
relation_type: "part_of",
|
|
911
|
+
confidence: 0.75
|
|
912
|
+
});
|
|
913
|
+
} catch {}
|
|
914
|
+
}
|
|
815
915
|
}
|
|
816
|
-
|
|
916
|
+
}
|
|
917
|
+
} catch {}
|
|
918
|
+
}
|
|
919
|
+
return patternIds;
|
|
920
|
+
}
|
|
921
|
+
async function detectContradictions(client, entityId, entityType, title, content, tags, workspaceId, projectId) {
|
|
922
|
+
if (!CONTRADICTION_TYPES.has(entityType))
|
|
923
|
+
return [];
|
|
924
|
+
if (!title.trim())
|
|
925
|
+
return [];
|
|
926
|
+
try {
|
|
927
|
+
const similar = await findSimilarEntities(client, title, content, workspaceId, {
|
|
928
|
+
projectId,
|
|
929
|
+
limit: 10,
|
|
930
|
+
excludeIds: [entityId]
|
|
931
|
+
});
|
|
932
|
+
const candidates = similar.filter((e) => e.type === entityType && CONTRADICTION_TYPES.has(e.type));
|
|
933
|
+
if (candidates.length === 0)
|
|
934
|
+
return [];
|
|
935
|
+
const contradictions = [];
|
|
936
|
+
for (const candidate of candidates) {
|
|
937
|
+
if ((candidate.rrf_score ?? 0) > 0.8)
|
|
938
|
+
continue;
|
|
939
|
+
if ((candidate.rrf_score ?? 0) < 0.02)
|
|
940
|
+
continue;
|
|
941
|
+
const sharedTags = tags.filter((t) => candidate.tags?.includes(t));
|
|
942
|
+
if (tags.length > 0 && sharedTags.length === 0)
|
|
943
|
+
continue;
|
|
944
|
+
contradictions.push({
|
|
945
|
+
entityId: candidate.id,
|
|
946
|
+
title: candidate.title,
|
|
947
|
+
type: candidate.type,
|
|
948
|
+
tags: candidate.tags || []
|
|
949
|
+
});
|
|
817
950
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
951
|
+
for (const c of contradictions) {
|
|
952
|
+
try {
|
|
953
|
+
await client.createMemoryRelation({
|
|
954
|
+
source_id: entityId,
|
|
955
|
+
target_id: c.entityId,
|
|
956
|
+
relation_type: "contradicts",
|
|
957
|
+
confidence: 0.5
|
|
958
|
+
});
|
|
959
|
+
} catch {}
|
|
821
960
|
}
|
|
961
|
+
return contradictions;
|
|
962
|
+
} catch {
|
|
963
|
+
return [];
|
|
964
|
+
}
|
|
822
965
|
}
|
|
966
|
+
export {
|
|
967
|
+
linkSessionEntities,
|
|
968
|
+
extractMidSessionLearnings,
|
|
969
|
+
extractLearnings,
|
|
970
|
+
detectContradictions,
|
|
971
|
+
detectCausalPatterns,
|
|
972
|
+
detectAndCreatePatterns,
|
|
973
|
+
clearMidSessionTracking
|
|
974
|
+
};
|