memorix 0.7.11 → 0.8.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.md +19 -0
- package/dist/cli/index.js +381 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +721 -314
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.8.0] — 2026-02-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Session Lifecycle Management** — 3 new MCP tools for cross-session context continuity:
|
|
9
|
+
- `memorix_session_start` — Start a coding session, auto-inject context from previous sessions (summaries + key observations). Previous active sessions are auto-closed.
|
|
10
|
+
- `memorix_session_end` — End a session with structured summary (Goal/Discoveries/Accomplished/Files format). Summary is injected into the next session.
|
|
11
|
+
- `memorix_session_context` — Manually retrieve session history and context (useful after compaction recovery).
|
|
12
|
+
- **Topic Key Upsert** — `memorix_store` now accepts an optional `topicKey` parameter. When an observation with the same `topicKey + projectId` already exists, it is **updated in-place** instead of creating a duplicate. `revisionCount` increments on each upsert. Prevents data bloat for evolving decisions, architecture docs, etc.
|
|
13
|
+
- **`memorix_suggest_topic_key` tool** — Suggests stable topic keys from type + title using family heuristics (`architecture/*`, `bug/*`, `decision/*`, `config/*`, `discovery/*`, `pattern/*`). Supports CJK characters.
|
|
14
|
+
- **Session persistence** — `sessions.json` with atomic writes and file locking for cross-process safety.
|
|
15
|
+
- **Observation fields** — `topicKey`, `revisionCount`, `updatedAt`, `sessionId` added to `Observation` interface.
|
|
16
|
+
- **30 new tests** — 16 session lifecycle tests + 14 topic key upsert tests (468 total).
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
- **`storeObservation` API** — Now returns `{ observation, upserted }` instead of just `Observation`, enabling callers to distinguish new vs updated observations.
|
|
20
|
+
|
|
21
|
+
### Inspired by
|
|
22
|
+
- [Engram](https://github.com/alanbuscaglia/engram) — Session lifecycle design, topic_key upsert pattern, structured session summaries.
|
|
23
|
+
|
|
5
24
|
## [0.7.11] — 2026-02-24
|
|
6
25
|
|
|
7
26
|
### Added
|
package/dist/cli/index.js
CHANGED
|
@@ -111,10 +111,12 @@ __export(persistence_exports, {
|
|
|
111
111
|
loadGraphJsonl: () => loadGraphJsonl,
|
|
112
112
|
loadIdCounter: () => loadIdCounter,
|
|
113
113
|
loadObservationsJson: () => loadObservationsJson,
|
|
114
|
+
loadSessionsJson: () => loadSessionsJson,
|
|
114
115
|
migrateGlobalData: () => migrateGlobalData,
|
|
115
116
|
saveGraphJsonl: () => saveGraphJsonl,
|
|
116
117
|
saveIdCounter: () => saveIdCounter,
|
|
117
|
-
saveObservationsJson: () => saveObservationsJson
|
|
118
|
+
saveObservationsJson: () => saveObservationsJson,
|
|
119
|
+
saveSessionsJson: () => saveSessionsJson
|
|
118
120
|
});
|
|
119
121
|
import { promises as fs2 } from "fs";
|
|
120
122
|
import path3 from "path";
|
|
@@ -333,6 +335,22 @@ async function loadIdCounter(projectDir2) {
|
|
|
333
335
|
return 1;
|
|
334
336
|
}
|
|
335
337
|
}
|
|
338
|
+
async function saveSessionsJson(projectDir2, sessions) {
|
|
339
|
+
const filePath = path3.join(projectDir2, "sessions.json");
|
|
340
|
+
await atomicWriteFile(filePath, JSON.stringify(sessions, null, 2));
|
|
341
|
+
}
|
|
342
|
+
async function loadSessionsJson(projectDir2) {
|
|
343
|
+
const filePath = path3.join(projectDir2, "sessions.json");
|
|
344
|
+
try {
|
|
345
|
+
const data = await fs2.readFile(filePath, "utf-8");
|
|
346
|
+
return JSON.parse(data);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
336
354
|
var DEFAULT_DATA_DIR;
|
|
337
355
|
var init_persistence = __esm({
|
|
338
356
|
"src/store/persistence.ts"() {
|
|
@@ -495,7 +513,7 @@ var init_graph = __esm({
|
|
|
495
513
|
});
|
|
496
514
|
|
|
497
515
|
// src/types.ts
|
|
498
|
-
var OBSERVATION_ICONS;
|
|
516
|
+
var OBSERVATION_ICONS, TOPIC_KEY_FAMILIES;
|
|
499
517
|
var init_types = __esm({
|
|
500
518
|
"src/types.ts"() {
|
|
501
519
|
"use strict";
|
|
@@ -511,6 +529,14 @@ var init_types = __esm({
|
|
|
511
529
|
"decision": "\u{1F7E4}",
|
|
512
530
|
"trade-off": "\u2696\uFE0F"
|
|
513
531
|
};
|
|
532
|
+
TOPIC_KEY_FAMILIES = {
|
|
533
|
+
"architecture": ["architecture", "design", "adr", "structure", "pattern"],
|
|
534
|
+
"bug": ["bugfix", "fix", "error", "regression", "crash", "problem-solution"],
|
|
535
|
+
"decision": ["decision", "trade-off", "choice", "strategy"],
|
|
536
|
+
"config": ["config", "setup", "env", "environment", "deployment"],
|
|
537
|
+
"discovery": ["discovery", "learning", "insight", "gotcha"],
|
|
538
|
+
"pattern": ["pattern", "convention", "standard", "best-practice"]
|
|
539
|
+
};
|
|
514
540
|
}
|
|
515
541
|
});
|
|
516
542
|
|
|
@@ -1090,7 +1116,8 @@ __export(observations_exports, {
|
|
|
1090
1116
|
getProjectObservations: () => getProjectObservations,
|
|
1091
1117
|
initObservations: () => initObservations,
|
|
1092
1118
|
reindexObservations: () => reindexObservations,
|
|
1093
|
-
storeObservation: () => storeObservation
|
|
1119
|
+
storeObservation: () => storeObservation,
|
|
1120
|
+
suggestTopicKey: () => suggestTopicKey
|
|
1094
1121
|
});
|
|
1095
1122
|
async function initObservations(dir) {
|
|
1096
1123
|
projectDir = dir;
|
|
@@ -1099,8 +1126,16 @@ async function initObservations(dir) {
|
|
|
1099
1126
|
nextId = await loadIdCounter(dir);
|
|
1100
1127
|
}
|
|
1101
1128
|
async function storeObservation(input) {
|
|
1102
|
-
const id = nextId++;
|
|
1103
1129
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1130
|
+
if (input.topicKey) {
|
|
1131
|
+
const existing = observations.find(
|
|
1132
|
+
(o) => o.topicKey === input.topicKey && o.projectId === input.projectId
|
|
1133
|
+
);
|
|
1134
|
+
if (existing) {
|
|
1135
|
+
return { observation: await upsertObservation(existing, input, now), upserted: true };
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const id = nextId++;
|
|
1104
1139
|
const contentForExtraction = [input.title, input.narrative, ...input.facts ?? []].join(" ");
|
|
1105
1140
|
const extracted = extractEntities(contentForExtraction);
|
|
1106
1141
|
const enrichedConcepts = enrichConcepts(input.concepts ?? [], extracted);
|
|
@@ -1131,7 +1166,10 @@ async function storeObservation(input) {
|
|
|
1131
1166
|
tokens,
|
|
1132
1167
|
createdAt: now,
|
|
1133
1168
|
projectId: input.projectId,
|
|
1134
|
-
hasCausalLanguage: extracted.hasCausalLanguage
|
|
1169
|
+
hasCausalLanguage: extracted.hasCausalLanguage,
|
|
1170
|
+
topicKey: input.topicKey,
|
|
1171
|
+
revisionCount: 1,
|
|
1172
|
+
sessionId: input.sessionId
|
|
1135
1173
|
};
|
|
1136
1174
|
observations.push(observation);
|
|
1137
1175
|
const searchableText = [input.title, input.narrative, ...input.facts ?? []].join(" ");
|
|
@@ -1169,7 +1207,70 @@ async function storeObservation(input) {
|
|
|
1169
1207
|
await saveIdCounter(projectDir, nextId);
|
|
1170
1208
|
});
|
|
1171
1209
|
}
|
|
1172
|
-
return observation;
|
|
1210
|
+
return { observation, upserted: false };
|
|
1211
|
+
}
|
|
1212
|
+
async function upsertObservation(existing, input, now) {
|
|
1213
|
+
const contentForExtraction = [input.title, input.narrative, ...input.facts ?? []].join(" ");
|
|
1214
|
+
const extracted = extractEntities(contentForExtraction);
|
|
1215
|
+
const enrichedConcepts = enrichConcepts(input.concepts ?? [], extracted);
|
|
1216
|
+
const userFiles = new Set((input.filesModified ?? []).map((f) => f.toLowerCase()));
|
|
1217
|
+
const enrichedFiles = [...input.filesModified ?? []];
|
|
1218
|
+
for (const f of extracted.files) {
|
|
1219
|
+
if (!userFiles.has(f.toLowerCase())) enrichedFiles.push(f);
|
|
1220
|
+
}
|
|
1221
|
+
const fullText = [input.title, input.narrative, ...input.facts ?? [], ...enrichedFiles, ...enrichedConcepts].join(" ");
|
|
1222
|
+
const tokens = countTextTokens(fullText);
|
|
1223
|
+
existing.entityName = input.entityName;
|
|
1224
|
+
existing.type = input.type;
|
|
1225
|
+
existing.title = input.title;
|
|
1226
|
+
existing.narrative = input.narrative;
|
|
1227
|
+
existing.facts = input.facts ?? [];
|
|
1228
|
+
existing.filesModified = enrichedFiles;
|
|
1229
|
+
existing.concepts = enrichedConcepts;
|
|
1230
|
+
existing.tokens = tokens;
|
|
1231
|
+
existing.updatedAt = now;
|
|
1232
|
+
existing.hasCausalLanguage = extracted.hasCausalLanguage;
|
|
1233
|
+
existing.revisionCount = (existing.revisionCount ?? 1) + 1;
|
|
1234
|
+
if (input.sessionId) existing.sessionId = input.sessionId;
|
|
1235
|
+
const searchableText = [input.title, input.narrative, ...input.facts ?? []].join(" ");
|
|
1236
|
+
const embedding = await generateEmbedding(searchableText);
|
|
1237
|
+
const doc = {
|
|
1238
|
+
id: `obs-${existing.id}`,
|
|
1239
|
+
observationId: existing.id,
|
|
1240
|
+
entityName: existing.entityName,
|
|
1241
|
+
type: existing.type,
|
|
1242
|
+
title: existing.title,
|
|
1243
|
+
narrative: existing.narrative,
|
|
1244
|
+
facts: existing.facts.join("\n"),
|
|
1245
|
+
filesModified: enrichedFiles.join("\n"),
|
|
1246
|
+
concepts: enrichedConcepts.map((c) => c.replace(/-/g, " ")).join(", "),
|
|
1247
|
+
tokens,
|
|
1248
|
+
createdAt: existing.createdAt,
|
|
1249
|
+
projectId: existing.projectId,
|
|
1250
|
+
accessCount: 0,
|
|
1251
|
+
lastAccessedAt: "",
|
|
1252
|
+
...embedding ? { embedding } : {}
|
|
1253
|
+
};
|
|
1254
|
+
try {
|
|
1255
|
+
const { removeObservation: removeObservation2 } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
|
|
1256
|
+
await removeObservation2(`obs-${existing.id}`);
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
await insertObservation(doc);
|
|
1260
|
+
if (projectDir) {
|
|
1261
|
+
await withFileLock(projectDir, async () => {
|
|
1262
|
+
const diskObs = await loadObservationsJson(projectDir);
|
|
1263
|
+
const idx = diskObs.findIndex((o) => o.id === existing.id);
|
|
1264
|
+
if (idx >= 0) {
|
|
1265
|
+
diskObs[idx] = existing;
|
|
1266
|
+
} else {
|
|
1267
|
+
diskObs.push(existing);
|
|
1268
|
+
}
|
|
1269
|
+
observations = diskObs;
|
|
1270
|
+
await saveObservationsJson(projectDir, observations);
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
return existing;
|
|
1173
1274
|
}
|
|
1174
1275
|
function getObservation(id) {
|
|
1175
1276
|
return observations.find((o) => o.id === id);
|
|
@@ -1180,6 +1281,19 @@ function getProjectObservations(projectId) {
|
|
|
1180
1281
|
function getObservationCount2() {
|
|
1181
1282
|
return observations.length;
|
|
1182
1283
|
}
|
|
1284
|
+
function suggestTopicKey(type, title) {
|
|
1285
|
+
let family = "general";
|
|
1286
|
+
const typeLower = type.toLowerCase();
|
|
1287
|
+
for (const [fam, keywords] of Object.entries(TOPIC_KEY_FAMILIES)) {
|
|
1288
|
+
if (keywords.some((k) => typeLower.includes(k))) {
|
|
1289
|
+
family = fam;
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 60);
|
|
1294
|
+
if (!slug) return "";
|
|
1295
|
+
return `${family}/${slug}`;
|
|
1296
|
+
}
|
|
1183
1297
|
async function reindexObservations() {
|
|
1184
1298
|
let count2 = 0;
|
|
1185
1299
|
for (const obs of observations) {
|
|
@@ -1222,6 +1336,7 @@ var init_observations = __esm({
|
|
|
1222
1336
|
"src/memory/observations.ts"() {
|
|
1223
1337
|
"use strict";
|
|
1224
1338
|
init_esm_shims();
|
|
1339
|
+
init_types();
|
|
1225
1340
|
init_orama_store();
|
|
1226
1341
|
init_persistence();
|
|
1227
1342
|
init_file_lock();
|
|
@@ -4377,6 +4492,138 @@ var init_engine3 = __esm({
|
|
|
4377
4492
|
}
|
|
4378
4493
|
});
|
|
4379
4494
|
|
|
4495
|
+
// src/memory/session.ts
|
|
4496
|
+
var session_exports = {};
|
|
4497
|
+
__export(session_exports, {
|
|
4498
|
+
endSession: () => endSession,
|
|
4499
|
+
getActiveSession: () => getActiveSession,
|
|
4500
|
+
getSessionContext: () => getSessionContext,
|
|
4501
|
+
listSessions: () => listSessions,
|
|
4502
|
+
startSession: () => startSession
|
|
4503
|
+
});
|
|
4504
|
+
function generateSessionId() {
|
|
4505
|
+
const ts = Date.now().toString(36);
|
|
4506
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
4507
|
+
return `sess-${ts}-${rand}`;
|
|
4508
|
+
}
|
|
4509
|
+
async function startSession(projectDir2, projectId, opts) {
|
|
4510
|
+
const sessionId = opts?.sessionId || generateSessionId();
|
|
4511
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4512
|
+
const session = {
|
|
4513
|
+
id: sessionId,
|
|
4514
|
+
projectId,
|
|
4515
|
+
startedAt: now,
|
|
4516
|
+
status: "active",
|
|
4517
|
+
agent: opts?.agent
|
|
4518
|
+
};
|
|
4519
|
+
const previousContext = await getSessionContext(projectDir2, projectId);
|
|
4520
|
+
await withFileLock(projectDir2, async () => {
|
|
4521
|
+
const sessions = await loadSessionsJson(projectDir2);
|
|
4522
|
+
for (const s of sessions) {
|
|
4523
|
+
if (s.projectId === projectId && s.status === "active") {
|
|
4524
|
+
s.status = "completed";
|
|
4525
|
+
s.endedAt = now;
|
|
4526
|
+
if (!s.summary) {
|
|
4527
|
+
s.summary = "(session ended implicitly by new session start)";
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
sessions.push(session);
|
|
4532
|
+
await saveSessionsJson(projectDir2, sessions);
|
|
4533
|
+
});
|
|
4534
|
+
return { session, previousContext };
|
|
4535
|
+
}
|
|
4536
|
+
async function endSession(projectDir2, sessionId, summary) {
|
|
4537
|
+
let endedSession = null;
|
|
4538
|
+
await withFileLock(projectDir2, async () => {
|
|
4539
|
+
const sessions = await loadSessionsJson(projectDir2);
|
|
4540
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
4541
|
+
if (!session) return;
|
|
4542
|
+
session.status = "completed";
|
|
4543
|
+
session.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4544
|
+
if (summary) {
|
|
4545
|
+
session.summary = summary;
|
|
4546
|
+
}
|
|
4547
|
+
endedSession = session;
|
|
4548
|
+
await saveSessionsJson(projectDir2, sessions);
|
|
4549
|
+
});
|
|
4550
|
+
return endedSession;
|
|
4551
|
+
}
|
|
4552
|
+
async function getSessionContext(projectDir2, projectId, limit = 3) {
|
|
4553
|
+
const sessions = await loadSessionsJson(projectDir2);
|
|
4554
|
+
const allObs = await loadObservationsJson(projectDir2);
|
|
4555
|
+
const projectSessions = sessions.filter((s) => s.projectId === projectId && s.status === "completed").sort((a, b) => new Date(b.endedAt || b.startedAt).getTime() - new Date(a.endedAt || a.startedAt).getTime()).slice(0, limit);
|
|
4556
|
+
if (projectSessions.length === 0 && allObs.length === 0) {
|
|
4557
|
+
return "";
|
|
4558
|
+
}
|
|
4559
|
+
const lines = [];
|
|
4560
|
+
if (projectSessions.length > 0) {
|
|
4561
|
+
const last = projectSessions[0];
|
|
4562
|
+
lines.push(`## Previous Session`);
|
|
4563
|
+
if (last.agent) {
|
|
4564
|
+
lines.push(`Agent: ${last.agent}`);
|
|
4565
|
+
}
|
|
4566
|
+
lines.push(`Ended: ${last.endedAt || last.startedAt}`);
|
|
4567
|
+
if (last.summary && last.summary !== "(session ended implicitly by new session start)") {
|
|
4568
|
+
lines.push("");
|
|
4569
|
+
lines.push(last.summary);
|
|
4570
|
+
}
|
|
4571
|
+
lines.push("");
|
|
4572
|
+
}
|
|
4573
|
+
const PRIORITY_TYPES = /* @__PURE__ */ new Set(["gotcha", "decision", "problem-solution", "trade-off", "discovery"]);
|
|
4574
|
+
const TYPE_EMOJI = {
|
|
4575
|
+
"gotcha": "\u{1F534}",
|
|
4576
|
+
"decision": "\u{1F7E4}",
|
|
4577
|
+
"problem-solution": "\u{1F7E1}",
|
|
4578
|
+
"trade-off": "\u2696\uFE0F",
|
|
4579
|
+
"discovery": "\u{1F7E3}",
|
|
4580
|
+
"how-it-works": "\u{1F535}",
|
|
4581
|
+
"what-changed": "\u{1F7E2}",
|
|
4582
|
+
"why-it-exists": "\u{1F7E0}",
|
|
4583
|
+
"session-request": "\u{1F3AF}"
|
|
4584
|
+
};
|
|
4585
|
+
const priorityObs = allObs.filter((o) => o.projectId === projectId && PRIORITY_TYPES.has(o.type)).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, 5);
|
|
4586
|
+
if (priorityObs.length > 0) {
|
|
4587
|
+
lines.push(`## Key Memories`);
|
|
4588
|
+
for (const obs of priorityObs) {
|
|
4589
|
+
const emoji = TYPE_EMOJI[obs.type] ?? "\u{1F4CC}";
|
|
4590
|
+
const fact = obs.facts?.[0] ? ` \u2014 ${obs.facts[0]}` : "";
|
|
4591
|
+
lines.push(`${emoji} ${obs.title}${fact}`);
|
|
4592
|
+
}
|
|
4593
|
+
lines.push("");
|
|
4594
|
+
}
|
|
4595
|
+
if (projectSessions.length > 1) {
|
|
4596
|
+
lines.push(`## Session History (last ${projectSessions.length})`);
|
|
4597
|
+
for (const s of projectSessions) {
|
|
4598
|
+
const date = (s.endedAt || s.startedAt).slice(0, 10);
|
|
4599
|
+
const agent = s.agent ? ` [${s.agent}]` : "";
|
|
4600
|
+
const summary = s.summary ? ` \u2014 ${s.summary.split("\n")[0].replace(/^#+\s*/, "").slice(0, 80)}` : "";
|
|
4601
|
+
lines.push(`- ${date}${agent}${summary}`);
|
|
4602
|
+
}
|
|
4603
|
+
lines.push("");
|
|
4604
|
+
}
|
|
4605
|
+
return lines.join("\n");
|
|
4606
|
+
}
|
|
4607
|
+
async function listSessions(projectDir2, projectId) {
|
|
4608
|
+
const sessions = await loadSessionsJson(projectDir2);
|
|
4609
|
+
if (projectId) {
|
|
4610
|
+
return sessions.filter((s) => s.projectId === projectId);
|
|
4611
|
+
}
|
|
4612
|
+
return sessions;
|
|
4613
|
+
}
|
|
4614
|
+
async function getActiveSession(projectDir2, projectId) {
|
|
4615
|
+
const sessions = await loadSessionsJson(projectDir2);
|
|
4616
|
+
return sessions.find((s) => s.projectId === projectId && s.status === "active") || null;
|
|
4617
|
+
}
|
|
4618
|
+
var init_session = __esm({
|
|
4619
|
+
"src/memory/session.ts"() {
|
|
4620
|
+
"use strict";
|
|
4621
|
+
init_esm_shims();
|
|
4622
|
+
init_persistence();
|
|
4623
|
+
init_file_lock();
|
|
4624
|
+
}
|
|
4625
|
+
});
|
|
4626
|
+
|
|
4380
4627
|
// src/dashboard/server.ts
|
|
4381
4628
|
var server_exports = {};
|
|
4382
4629
|
__export(server_exports, {
|
|
@@ -4838,14 +5085,17 @@ async function createMemorixServer(cwd, existingServer) {
|
|
|
4838
5085
|
narrative: z.string().describe("Full description of the observation"),
|
|
4839
5086
|
facts: z.array(z.string()).optional().describe('Structured facts (e.g., "Default timeout: 60s")'),
|
|
4840
5087
|
filesModified: z.array(z.string()).optional().describe("Files involved"),
|
|
4841
|
-
concepts: z.array(z.string()).optional().describe("Related concepts/keywords")
|
|
5088
|
+
concepts: z.array(z.string()).optional().describe("Related concepts/keywords"),
|
|
5089
|
+
topicKey: z.string().optional().describe(
|
|
5090
|
+
'Optional topic identifier for upserts (e.g., "architecture/auth-model"). If an observation with the same topicKey already exists in this project, it will be UPDATED instead of creating a new one. Use memorix_suggest_topic_key to generate a stable key. Good for evolving decisions, architecture docs, etc.'
|
|
5091
|
+
)
|
|
4842
5092
|
}
|
|
4843
5093
|
},
|
|
4844
|
-
async ({ entityName, type, title, narrative, facts, filesModified, concepts }) => {
|
|
5094
|
+
async ({ entityName, type, title, narrative, facts, filesModified, concepts, topicKey }) => {
|
|
4845
5095
|
await graphManager.createEntities([
|
|
4846
5096
|
{ name: entityName, entityType: "auto", observations: [] }
|
|
4847
5097
|
]);
|
|
4848
|
-
const obs = await storeObservation({
|
|
5098
|
+
const { observation: obs, upserted } = await storeObservation({
|
|
4849
5099
|
entityName,
|
|
4850
5100
|
type,
|
|
4851
5101
|
title,
|
|
@@ -4853,7 +5103,8 @@ async function createMemorixServer(cwd, existingServer) {
|
|
|
4853
5103
|
facts,
|
|
4854
5104
|
filesModified,
|
|
4855
5105
|
concepts,
|
|
4856
|
-
projectId: project.id
|
|
5106
|
+
projectId: project.id,
|
|
5107
|
+
topicKey
|
|
4857
5108
|
});
|
|
4858
5109
|
await graphManager.addObservations([
|
|
4859
5110
|
{ entityName, contents: [`[#${obs.id}] ${title}`] }
|
|
@@ -4867,19 +5118,47 @@ async function createMemorixServer(cwd, existingServer) {
|
|
|
4867
5118
|
if (autoConcepts.length > 0) enrichmentParts.push(`+${autoConcepts.length} concepts enriched`);
|
|
4868
5119
|
if (autoRelCount > 0) enrichmentParts.push(`+${autoRelCount} relations auto-created`);
|
|
4869
5120
|
if (obs.hasCausalLanguage) enrichmentParts.push("causal language detected");
|
|
5121
|
+
if (upserted) enrichmentParts.push(`topic upserted (rev ${obs.revisionCount ?? 1})`);
|
|
4870
5122
|
const enrichment = enrichmentParts.length > 0 ? `
|
|
4871
5123
|
Auto-enriched: ${enrichmentParts.join(", ")}` : "";
|
|
5124
|
+
const action = upserted ? "\u{1F504} Updated" : "\u2705 Stored";
|
|
4872
5125
|
return {
|
|
4873
5126
|
content: [
|
|
4874
5127
|
{
|
|
4875
5128
|
type: "text",
|
|
4876
|
-
text:
|
|
4877
|
-
Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
|
|
5129
|
+
text: `${action} observation #${obs.id} "${title}" (~${obs.tokens} tokens)
|
|
5130
|
+
Entity: ${entityName} | Type: ${type} | Project: ${project.id}${obs.topicKey ? ` | Topic: ${obs.topicKey}` : ""}${enrichment}`
|
|
4878
5131
|
}
|
|
4879
5132
|
]
|
|
4880
5133
|
};
|
|
4881
5134
|
}
|
|
4882
5135
|
);
|
|
5136
|
+
server.registerTool(
|
|
5137
|
+
"memorix_suggest_topic_key",
|
|
5138
|
+
{
|
|
5139
|
+
title: "Suggest Topic Key",
|
|
5140
|
+
description: 'Suggest a stable topic_key for memory upserts. Use this before memorix_store when you want evolving topics (like architecture decisions, config docs) to update a single observation over time instead of creating duplicates. Returns a key like "architecture/auth-model" or "bug/timeout-in-api-gateway".',
|
|
5141
|
+
inputSchema: {
|
|
5142
|
+
type: z.string().describe("Observation type (e.g., decision, architecture, bugfix, discovery)"),
|
|
5143
|
+
title: z.string().describe("Observation title \u2014 used to generate the stable key")
|
|
5144
|
+
}
|
|
5145
|
+
},
|
|
5146
|
+
async ({ type: obsType, title }) => {
|
|
5147
|
+
const { suggestTopicKey: suggestTopicKey2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
|
|
5148
|
+
const key = suggestTopicKey2(obsType, title);
|
|
5149
|
+
if (!key) {
|
|
5150
|
+
return {
|
|
5151
|
+
content: [{ type: "text", text: "Could not suggest topic_key from the given input. Provide a more descriptive title." }],
|
|
5152
|
+
isError: true
|
|
5153
|
+
};
|
|
5154
|
+
}
|
|
5155
|
+
return {
|
|
5156
|
+
content: [{ type: "text", text: `Suggested topic_key: \`${key}\`
|
|
5157
|
+
|
|
5158
|
+
Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert behavior.` }]
|
|
5159
|
+
};
|
|
5160
|
+
}
|
|
5161
|
+
);
|
|
4883
5162
|
server.registerTool(
|
|
4884
5163
|
"memorix_search",
|
|
4885
5164
|
{
|
|
@@ -5472,6 +5751,96 @@ ${skill.content}` }]
|
|
|
5472
5751
|
};
|
|
5473
5752
|
}
|
|
5474
5753
|
);
|
|
5754
|
+
server.registerTool(
|
|
5755
|
+
"memorix_session_start",
|
|
5756
|
+
{
|
|
5757
|
+
title: "Start Session",
|
|
5758
|
+
description: "Start a new coding session. Returns context from previous sessions so you can resume work seamlessly. Call this at the beginning of a session to track activity and get injected context. Any previous active session for this project will be auto-closed.",
|
|
5759
|
+
inputSchema: {
|
|
5760
|
+
sessionId: z.string().optional().describe("Custom session ID (auto-generated if omitted)"),
|
|
5761
|
+
agent: z.string().optional().describe('Agent/IDE name (e.g., "cursor", "windsurf", "claude-code")')
|
|
5762
|
+
}
|
|
5763
|
+
},
|
|
5764
|
+
async ({ sessionId, agent }) => {
|
|
5765
|
+
const { startSession: startSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
|
|
5766
|
+
const result = await startSession2(projectDir2, project.id, { sessionId, agent });
|
|
5767
|
+
const lines = [
|
|
5768
|
+
`\u2705 Session started: ${result.session.id}`,
|
|
5769
|
+
`Project: ${project.name} (${project.id})`,
|
|
5770
|
+
result.session.agent ? `Agent: ${result.session.agent}` : "",
|
|
5771
|
+
""
|
|
5772
|
+
];
|
|
5773
|
+
if (result.previousContext) {
|
|
5774
|
+
lines.push("---", "\u{1F4CB} **Context from previous sessions:**", "", result.previousContext);
|
|
5775
|
+
} else {
|
|
5776
|
+
lines.push("No previous session context found. This appears to be a fresh project.");
|
|
5777
|
+
}
|
|
5778
|
+
return {
|
|
5779
|
+
content: [{ type: "text", text: lines.filter(Boolean).join("\n") }]
|
|
5780
|
+
};
|
|
5781
|
+
}
|
|
5782
|
+
);
|
|
5783
|
+
server.registerTool(
|
|
5784
|
+
"memorix_session_end",
|
|
5785
|
+
{
|
|
5786
|
+
title: "End Session",
|
|
5787
|
+
description: "End a coding session with a structured summary. This summary will be injected into the next session so the next agent can resume work seamlessly.\n\nRecommended summary format:\n## Goal\n[What we were working on]\n\n## Discoveries\n- [Technical findings, gotchas, learnings]\n\n## Accomplished\n- \u2705 [Completed tasks]\n- \u{1F532} [Pending for next session]\n\n## Relevant Files\n- path/to/file \u2014 [what changed]",
|
|
5788
|
+
inputSchema: {
|
|
5789
|
+
sessionId: z.string().describe("Session ID to close (from memorix_session_start)"),
|
|
5790
|
+
summary: z.string().optional().describe("Structured session summary (Goal/Discoveries/Accomplished/Files format)")
|
|
5791
|
+
}
|
|
5792
|
+
},
|
|
5793
|
+
async ({ sessionId, summary }) => {
|
|
5794
|
+
const { endSession: endSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
|
|
5795
|
+
const session = await endSession2(projectDir2, sessionId, summary);
|
|
5796
|
+
if (!session) {
|
|
5797
|
+
return {
|
|
5798
|
+
content: [{ type: "text", text: `Session "${sessionId}" not found.` }],
|
|
5799
|
+
isError: true
|
|
5800
|
+
};
|
|
5801
|
+
}
|
|
5802
|
+
return {
|
|
5803
|
+
content: [{
|
|
5804
|
+
type: "text",
|
|
5805
|
+
text: `\u2705 Session "${sessionId}" completed.
|
|
5806
|
+
Duration: ${session.startedAt} \u2192 ${session.endedAt}
|
|
5807
|
+
${summary ? "Summary saved for next session context injection." : "No summary provided \u2014 consider adding one for better cross-session context."}`
|
|
5808
|
+
}]
|
|
5809
|
+
};
|
|
5810
|
+
}
|
|
5811
|
+
);
|
|
5812
|
+
server.registerTool(
|
|
5813
|
+
"memorix_session_context",
|
|
5814
|
+
{
|
|
5815
|
+
title: "Session Context",
|
|
5816
|
+
description: "Get context from previous coding sessions. Use this after compaction to recover lost context, or to manually review session history. Returns previous session summaries and key observations.",
|
|
5817
|
+
inputSchema: {
|
|
5818
|
+
limit: z.number().optional().describe("Number of recent sessions to include (default: 3)")
|
|
5819
|
+
}
|
|
5820
|
+
},
|
|
5821
|
+
async ({ limit }) => {
|
|
5822
|
+
const { getSessionContext: getSessionContext2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_session(), session_exports));
|
|
5823
|
+
const context = await getSessionContext2(projectDir2, project.id, limit ?? 3);
|
|
5824
|
+
const sessions = await listSessions2(projectDir2, project.id);
|
|
5825
|
+
const activeSessions = sessions.filter((s) => s.status === "active");
|
|
5826
|
+
const completedSessions = sessions.filter((s) => s.status === "completed");
|
|
5827
|
+
const header = [
|
|
5828
|
+
`## Session Stats`,
|
|
5829
|
+
`- Active: ${activeSessions.length}`,
|
|
5830
|
+
`- Completed: ${completedSessions.length}`,
|
|
5831
|
+
`- Total: ${sessions.length}`,
|
|
5832
|
+
""
|
|
5833
|
+
];
|
|
5834
|
+
if (!context) {
|
|
5835
|
+
return {
|
|
5836
|
+
content: [{ type: "text", text: header.join("\n") + "\nNo previous session context available." }]
|
|
5837
|
+
};
|
|
5838
|
+
}
|
|
5839
|
+
return {
|
|
5840
|
+
content: [{ type: "text", text: header.join("\n") + context }]
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5843
|
+
);
|
|
5475
5844
|
let dashboardRunning = false;
|
|
5476
5845
|
server.registerTool(
|
|
5477
5846
|
"memorix_dashboard",
|