synapse-causal-memory 0.2.2 → 0.2.3

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/index.js CHANGED
@@ -9,6 +9,8 @@ var CAUSAL_MEMORY_CONFIG = {
9
9
  LOCAL_DB_PATH: resolve(import.meta.dir, "causal-memory.json"),
10
10
  DEFAULT_HOST: process.env.SYNAPSE_MEMORY_HOST || "127.0.0.1",
11
11
  DEFAULT_PORT: parseInt(process.env.SYNAPSE_MEMORY_PORT || "4242", 10),
12
+ REMOTE_URL: process.env.SYNAPSE_MEMORY_REMOTE_URL || "",
13
+ REMOTE_TIMEOUT_MS: parseInt(process.env.SYNAPSE_MEMORY_REMOTE_TIMEOUT || "3000", 10),
12
14
  MCP_PROTOCOL_VERSION: process.env.SYNAPSE_MCP_VERSION || "2024-11-05",
13
15
  MCP_SERVER_NAME: "synapse-causal-memory",
14
16
  MAX_CONTEXT_PREVIEW_LINES: parseInt(process.env.SYNAPSE_MEMORY_MAX_PREVIEW || "5", 10)
@@ -164,6 +166,42 @@ class CausalMemoryStore {
164
166
  }
165
167
  }
166
168
  }
169
+ static getRuleDomainPriority(rule) {
170
+ const titleLower = rule.title.toLowerCase();
171
+ const dom = rule.domain;
172
+ if (titleLower.includes("biophys") && dom === "User_Preferences_And_Rules") {
173
+ return 0;
174
+ }
175
+ if (dom === "Knowledge_Base") {
176
+ return 1;
177
+ }
178
+ return 2;
179
+ }
180
+ static deduplicateRulesByTitle(rules) {
181
+ const byTitle = new Map;
182
+ for (const rule of rules) {
183
+ if (!rule.title)
184
+ continue;
185
+ const key = rule.title.toLowerCase();
186
+ const existing = byTitle.get(key);
187
+ if (!existing) {
188
+ byTitle.set(key, rule);
189
+ } else {
190
+ const existingPrio = CausalMemoryStore.getRuleDomainPriority(existing);
191
+ const incomingPrio = CausalMemoryStore.getRuleDomainPriority(rule);
192
+ if (incomingPrio > existingPrio) {
193
+ byTitle.set(key, rule);
194
+ } else if (incomingPrio === existingPrio) {
195
+ const existingDate = existing.lastVerified || "";
196
+ const incomingDate = rule.lastVerified || "";
197
+ if (incomingDate >= existingDate) {
198
+ byTitle.set(key, rule);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ return Array.from(byTitle.values());
204
+ }
167
205
  loadDatabase() {
168
206
  try {
169
207
  if (existsSync(this.dbPath)) {
@@ -171,7 +209,8 @@ class CausalMemoryStore {
171
209
  this.lastMtimeMs = stat.mtimeMs;
172
210
  const raw = readFileSync(this.dbPath, "utf-8");
173
211
  const parsed = JSON.parse(raw);
174
- const rules = Array.isArray(parsed.rules) ? parsed.rules : [];
212
+ const rawRules = Array.isArray(parsed.rules) ? parsed.rules : [];
213
+ const rules = CausalMemoryStore.deduplicateRulesByTitle(rawRules);
175
214
  const relations = Array.isArray(parsed.relations) ? parsed.relations : [];
176
215
  return {
177
216
  version: parsed.version || CAUSAL_MEMORY_CONFIG.SCHEMA_VERSION,
@@ -620,6 +659,34 @@ class CausalMemoryStore {
620
659
  }
621
660
  return created;
622
661
  }
662
+ deleteRule(ruleId, persistToDisk = true) {
663
+ const targetId = ruleId.toLowerCase();
664
+ const removedTitles = new Set;
665
+ const filteredRules = this.database.rules.filter((r) => {
666
+ if (r.id.toLowerCase() === targetId) {
667
+ removedTitles.add(r.title.toLowerCase());
668
+ return false;
669
+ }
670
+ return true;
671
+ });
672
+ if (removedTitles.size === 0) {
673
+ return false;
674
+ }
675
+ const filteredRelations = this.database.relations.filter((rel) => !removedTitles.has(rel.from.toLowerCase()) && !removedTitles.has(rel.to.toLowerCase()) && rel.from.toLowerCase() !== targetId && rel.to.toLowerCase() !== targetId);
676
+ this.database = {
677
+ ...this.database,
678
+ updatedAt: new Date().toISOString(),
679
+ totalRules: filteredRules.length,
680
+ totalRelations: filteredRelations.length,
681
+ rules: filteredRules,
682
+ relations: filteredRelations
683
+ };
684
+ this.rebuildIndices();
685
+ if (persistToDisk) {
686
+ this.saveDatabase();
687
+ }
688
+ return true;
689
+ }
623
690
  deleteEntities(entityNames, persistToDisk = true) {
624
691
  const nameSet = new Set(entityNames.map((n) => n.toLowerCase()));
625
692
  const filteredRules = this.database.rules.filter((r) => !nameSet.has(r.title.toLowerCase()) && !nameSet.has(r.id.toLowerCase()));
@@ -684,7 +751,7 @@ class CausalMemoryStore {
684
751
  const invariants = [];
685
752
  const actions = [];
686
753
  const context = [];
687
- const isTrueUserPreference = typeLower === "preference" || typeLower === "user_constraint" || typeLower === "forbidden_rules" || nameLower === "userpreferences" || nameLower === "user_web_navigation_preference" || nameLower === "python_strictly_forbidden" || nameLower === "forbidden_topics_until_mentioned";
754
+ const isTrueUserPreference = !nameLower.includes("biophys") && (typeLower === "preference" || typeLower === "user_constraint" || typeLower === "forbidden_rules" || nameLower === "userpreferences" || nameLower === "user_web_navigation_preference" || nameLower === "python_strictly_forbidden" || nameLower === "forbidden_topics_until_mentioned");
688
755
  if (isTrueUserPreference) {
689
756
  domain = "User_Preferences_And_Rules";
690
757
  triggers.alwaysActive = true;
@@ -746,6 +813,7 @@ class CausalMemoryStore {
746
813
  audit() {
747
814
  const issues = [];
748
815
  const seenIds = new Set;
816
+ const seenTitles = new Set;
749
817
  let alwaysActiveCount = 0;
750
818
  for (const rule of this.database.rules) {
751
819
  if (rule.triggers.alwaysActive) {
@@ -758,6 +826,14 @@ class CausalMemoryStore {
758
826
  } else {
759
827
  seenIds.add(rule.id);
760
828
  }
829
+ const titleLower = (rule.title || "").toLowerCase();
830
+ if (titleLower) {
831
+ if (seenTitles.has(titleLower)) {
832
+ issues.push(`\uC911\uBCF5 \uC81C\uBAA9(Title) \uBC1C\uACAC: ${rule.title}`);
833
+ } else {
834
+ seenTitles.add(titleLower);
835
+ }
836
+ }
761
837
  if (!rule.domain) {
762
838
  issues.push(`\uB3C4\uBA54\uC778 \uB204\uB77D \uADDC\uCE59: ${rule.id}`);
763
839
  }
@@ -773,6 +849,76 @@ class CausalMemoryStore {
773
849
  issues
774
850
  };
775
851
  }
852
+ getRawDatabase() {
853
+ this.reloadIfNeeded();
854
+ return this.database;
855
+ }
856
+ mergeDatabase(incomingRules, incomingRelations = []) {
857
+ this.reloadIfNeeded();
858
+ let addedRules = 0;
859
+ let updatedRules = 0;
860
+ let addedRelations = 0;
861
+ const byTitle = new Map;
862
+ for (const r of this.database.rules) {
863
+ byTitle.set(r.title.toLowerCase(), r);
864
+ }
865
+ for (const incoming of incomingRules) {
866
+ if (!incoming.id || !incoming.title)
867
+ continue;
868
+ const titleKey = incoming.title.toLowerCase();
869
+ const existing = byTitle.get(titleKey);
870
+ if (!existing) {
871
+ byTitle.set(titleKey, incoming);
872
+ addedRules++;
873
+ } else {
874
+ const existingPrio = CausalMemoryStore.getRuleDomainPriority(existing);
875
+ const incomingPrio = CausalMemoryStore.getRuleDomainPriority(incoming);
876
+ const existingDate = existing.lastVerified || "";
877
+ const incomingDate = incoming.lastVerified || "";
878
+ if (incomingPrio > existingPrio) {
879
+ byTitle.set(titleKey, incoming);
880
+ updatedRules++;
881
+ } else if (incomingPrio === existingPrio && incoming.id !== existing.id && incomingDate > existingDate) {
882
+ byTitle.set(titleKey, incoming);
883
+ updatedRules++;
884
+ } else if (incomingPrio === existingPrio && incoming.id === existing.id && incomingDate > existingDate) {
885
+ byTitle.set(titleKey, incoming);
886
+ updatedRules++;
887
+ }
888
+ }
889
+ }
890
+ const relationKey = (rel) => `${rel.from.toLowerCase()}::${rel.relationType.toLowerCase()}::${rel.to.toLowerCase()}`;
891
+ const relMap = new Map;
892
+ for (const rel of this.database.relations) {
893
+ relMap.set(relationKey(rel), rel);
894
+ }
895
+ for (const incomingRel of incomingRelations) {
896
+ const key = relationKey(incomingRel);
897
+ if (!relMap.has(key)) {
898
+ relMap.set(key, incomingRel);
899
+ addedRelations++;
900
+ }
901
+ }
902
+ const mergedRules = Array.from(byTitle.values());
903
+ const mergedRelations = Array.from(relMap.values());
904
+ this.database = {
905
+ ...this.database,
906
+ updatedAt: new Date().toISOString(),
907
+ totalRules: mergedRules.length,
908
+ totalRelations: mergedRelations.length,
909
+ rules: mergedRules,
910
+ relations: mergedRelations
911
+ };
912
+ this.rebuildIndices();
913
+ this.saveDatabase();
914
+ return {
915
+ addedRules,
916
+ updatedRules,
917
+ addedRelations,
918
+ totalRules: mergedRules.length,
919
+ totalRelations: mergedRelations.length
920
+ };
921
+ }
776
922
  }
777
923
  // causal-query.ts
778
924
  var {spawn } = globalThis.Bun;
@@ -1320,6 +1466,17 @@ function startCausalServer(options = {}) {
1320
1466
  const auditResult = store.audit();
1321
1467
  return new Response(JSON.stringify(auditResult), { headers });
1322
1468
  }
1469
+ if (url.pathname === "/sync" && req.method === "GET") {
1470
+ const rawDb = store.getRawDatabase();
1471
+ return new Response(JSON.stringify(rawDb), { headers });
1472
+ }
1473
+ if (url.pathname === "/sync" && req.method === "POST") {
1474
+ const body = await req.json();
1475
+ const incomingRules = Array.isArray(body.rules) ? body.rules : [];
1476
+ const incomingRelations = Array.isArray(body.relations) ? body.relations : [];
1477
+ const mergeResult = store.mergeDatabase(incomingRules, incomingRelations);
1478
+ return new Response(JSON.stringify({ ok: true, ...mergeResult }), { headers });
1479
+ }
1323
1480
  if (url.pathname === "/mcp" && req.method === "POST") {
1324
1481
  const rpc = await req.json();
1325
1482
  const responseObj = await handleMcpJsonRpcRequest(rpc, store, options.dbPath);
@@ -1336,9 +1493,12 @@ function startCausalServer(options = {}) {
1336
1493
  });
1337
1494
  return server;
1338
1495
  }
1339
- async function startCausalMcpStdioServer(dbPath) {
1496
+ async function startCausalMcpStdioServer(dbPath, remoteUrl) {
1340
1497
  const store = new CausalMemoryStore(dbPath);
1341
- const daemonUrl = `http://${CAUSAL_MEMORY_CONFIG.DEFAULT_HOST}:${CAUSAL_MEMORY_CONFIG.DEFAULT_PORT}/mcp`;
1498
+ const targetRemote = remoteUrl || CAUSAL_MEMORY_CONFIG.REMOTE_URL;
1499
+ const isRemoteMode = Boolean(targetRemote && targetRemote.trim().length > 0);
1500
+ const activeDaemonUrl = isRemoteMode ? targetRemote.endsWith("/mcp") ? targetRemote : `${targetRemote.replace(/\/+$/, "")}/mcp` : `http://${CAUSAL_MEMORY_CONFIG.DEFAULT_HOST}:${CAUSAL_MEMORY_CONFIG.DEFAULT_PORT}/mcp`;
1501
+ const timeoutMs = isRemoteMode ? CAUSAL_MEMORY_CONFIG.REMOTE_TIMEOUT_MS : 150;
1342
1502
  const rl = createInterface({
1343
1503
  input: process.stdin,
1344
1504
  terminal: false
@@ -1352,11 +1512,11 @@ async function startCausalMcpStdioServer(dbPath) {
1352
1512
  let responseObj = null;
1353
1513
  if (rpc.method === "tools/call") {
1354
1514
  try {
1355
- const daemonRes = await fetch(daemonUrl, {
1515
+ const daemonRes = await fetch(activeDaemonUrl, {
1356
1516
  method: "POST",
1357
1517
  headers: { "Content-Type": "application/json" },
1358
1518
  body: trimmed,
1359
- signal: AbortSignal.timeout(150)
1519
+ signal: AbortSignal.timeout(timeoutMs)
1360
1520
  });
1361
1521
  if (daemonRes.ok) {
1362
1522
  responseObj = await daemonRes.json();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synapse-causal-memory",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "AI-Native 0-Token Causal Memory Engine 2.0 with SQLite WAL atomic transactions and MCP server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,7 +21,13 @@
21
21
  "build:bundle": "bun build cli.ts --outfile dist/cli.js --target bun && bun build index.ts --outfile dist/index.js --target bun",
22
22
  "build:types": "tsc -p tsconfig.build.json",
23
23
  "build": "bun run build:bundle && bun run build:types",
24
- "prepublishOnly": "bun run build"
24
+ "prepublishOnly": "bun run build",
25
+ "start": "bun run cli.ts",
26
+ "query": "bun run cli.ts query",
27
+ "list": "bun run cli.ts list",
28
+ "audit": "bun run cli.ts audit",
29
+ "serve": "bun run cli.ts serve",
30
+ "bench": "bun run cli.ts benchmark"
25
31
  },
26
32
  "repository": {
27
33
  "type": "git",