memorix 1.9.4 → 1.9.5

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +6 -5
  3. package/README.zh-CN.md +6 -5
  4. package/dist/cli/index.js +890 -325
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.js +251 -104
  7. package/dist/maintenance-runner.js +144 -113
  8. package/dist/media-audio-runner.js +57 -26
  9. package/dist/media-video-runner.js +57 -26
  10. package/dist/memcode-runtime/CHANGELOG.md +33 -1
  11. package/dist/sdk.d.ts +1 -0
  12. package/dist/sdk.js +1012 -842
  13. package/dist/types.d.ts +1 -1
  14. package/dist/vector-backfill-runner.js +57 -26
  15. package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
  16. package/docs/API_REFERENCE.md +3 -1
  17. package/docs/CONFIGURATION.md +15 -1
  18. package/docs/DOCKER.md +23 -3
  19. package/docs/INTEGRATIONS.md +1 -1
  20. package/docs/KNOWN_ISSUES_AND_ROADMAP.md +26 -1
  21. package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
  22. package/docs/SETUP.md +55 -0
  23. package/package.json +2 -2
  24. package/plugins/antigravity/memorix/plugin.json +1 -1
  25. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  26. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  27. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  28. package/plugins/copilot/memorix/plugin.json +1 -1
  29. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  30. package/plugins/hermes/memorix/plugin.yaml +1 -1
  31. package/plugins/omp/memorix/package.json +1 -1
  32. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  33. package/plugins/pi/memorix/package.json +1 -1
  34. package/server.json +2 -2
  35. package/src/cli/command-guide.ts +2 -2
  36. package/src/cli/commands/agent-integrations.ts +57 -0
  37. package/src/cli/commands/background.ts +11 -2
  38. package/src/cli/commands/serve-http.ts +193 -42
  39. package/src/cli/commands/setup.ts +77 -10
  40. package/src/cli/commands/sync-store.ts +47 -12
  41. package/src/cli/commands/sync.ts +26 -0
  42. package/src/dashboard/server.ts +10 -12
  43. package/src/integrations/registry.ts +1 -1
  44. package/src/memory/graph.ts +10 -8
  45. package/src/memory/session.ts +1 -1
  46. package/src/sdk.ts +24 -0
  47. package/src/server/mcp-binding-store.ts +14 -3
  48. package/src/server.ts +2 -2
  49. package/src/store/file-lock.ts +28 -3
  50. package/src/store/graph-store.ts +178 -46
  51. package/src/store/persistence.ts +14 -9
  52. package/src/store/sqlite-db.ts +6 -3
  53. package/src/store/sqlite-store.ts +32 -27
  54. package/src/sync/engine.ts +9 -7
  55. package/src/sync/namespace.ts +23 -0
  56. package/src/sync/policy.ts +19 -4
  57. package/src/sync/store-port.ts +43 -13
  58. package/src/sync/types.ts +2 -0
  59. package/src/types.ts +1 -1
  60. package/src/workspace/mcp-adapters/grok.ts +32 -0
@@ -643,18 +643,21 @@ CREATE TABLE IF NOT EXISTS chat_transcript (
643
643
  `;
644
644
  CREATE_GRAPH_ENTITIES_TABLE = `
645
645
  CREATE TABLE IF NOT EXISTS graph_entities (
646
- name TEXT PRIMARY KEY,
646
+ projectId TEXT NOT NULL,
647
+ name TEXT NOT NULL,
647
648
  entityType TEXT NOT NULL DEFAULT '',
648
- observations TEXT NOT NULL DEFAULT '[]'
649
+ observations TEXT NOT NULL DEFAULT '[]',
650
+ PRIMARY KEY (projectId, name)
649
651
  );
650
652
  `;
651
653
  CREATE_GRAPH_RELATIONS_TABLE = `
652
654
  CREATE TABLE IF NOT EXISTS graph_relations (
653
655
  id INTEGER PRIMARY KEY AUTOINCREMENT,
656
+ projectId TEXT NOT NULL,
654
657
  from_entity TEXT NOT NULL,
655
658
  to_entity TEXT NOT NULL,
656
659
  relationType TEXT NOT NULL DEFAULT '',
657
- UNIQUE(from_entity, to_entity, relationType)
660
+ UNIQUE(projectId, from_entity, to_entity, relationType)
658
661
  );
659
662
  `;
660
663
  CREATE_CODE_FILES_TABLE = `
@@ -3041,13 +3044,120 @@ var init_sqlite_reliability = __esm({
3041
3044
  }
3042
3045
  });
3043
3046
 
3047
+ // src/store/file-lock.ts
3048
+ import { promises as fs2 } from "fs";
3049
+ import { randomUUID } from "crypto";
3050
+ import path5 from "path";
3051
+ function canReclaimStaleLock(raw) {
3052
+ try {
3053
+ const parsed = JSON.parse(raw);
3054
+ const pid = Number(parsed.pid);
3055
+ if (!Number.isSafeInteger(pid) || pid < 1) return true;
3056
+ try {
3057
+ process.kill(pid, 0);
3058
+ return false;
3059
+ } catch (error) {
3060
+ return error.code !== "EPERM";
3061
+ }
3062
+ } catch {
3063
+ return true;
3064
+ }
3065
+ }
3066
+ async function acquireLock(lockPath) {
3067
+ for (let i2 = 0; i2 < MAX_RETRIES; i2++) {
3068
+ try {
3069
+ const fd = await fs2.open(lockPath, "wx");
3070
+ await fd.writeFile(JSON.stringify({ pid: process.pid, time: Date.now() }));
3071
+ await fd.close();
3072
+ return;
3073
+ } catch (err) {
3074
+ const code = err instanceof Error && "code" in err ? err.code : void 0;
3075
+ if (code === "EEXIST" || code === "EPERM") {
3076
+ try {
3077
+ const stat5 = await fs2.stat(lockPath);
3078
+ if (Date.now() - stat5.mtimeMs > LOCK_STALE_MS) {
3079
+ const raw2 = await fs2.readFile(lockPath, "utf8").catch(() => "");
3080
+ if (canReclaimStaleLock(raw2)) {
3081
+ await fs2.unlink(lockPath).catch(() => {
3082
+ });
3083
+ continue;
3084
+ }
3085
+ }
3086
+ } catch {
3087
+ continue;
3088
+ }
3089
+ await new Promise((r2) => setTimeout(r2, RETRY_INTERVAL_MS));
3090
+ } else {
3091
+ throw err;
3092
+ }
3093
+ }
3094
+ }
3095
+ const raw = await fs2.readFile(lockPath, "utf8").catch(() => "");
3096
+ if (!canReclaimStaleLock(raw)) {
3097
+ throw new Error(`Failed to acquire lock: ${lockPath} (owner is still running)`);
3098
+ }
3099
+ await fs2.unlink(lockPath).catch(() => {
3100
+ });
3101
+ try {
3102
+ const fd = await fs2.open(lockPath, "wx");
3103
+ await fd.writeFile(JSON.stringify({ pid: process.pid, time: Date.now() }));
3104
+ await fd.close();
3105
+ return;
3106
+ } catch {
3107
+ throw new Error(`Failed to acquire lock: ${lockPath} (timeout after ${MAX_RETRIES * RETRY_INTERVAL_MS}ms)`);
3108
+ }
3109
+ }
3110
+ async function releaseLock(lockPath) {
3111
+ await fs2.unlink(lockPath).catch(() => {
3112
+ });
3113
+ }
3114
+ async function withFileLock(projectDir2, fn) {
3115
+ const lockPath = path5.join(projectDir2, ".memorix.lock");
3116
+ await acquireLock(lockPath);
3117
+ try {
3118
+ return await fn();
3119
+ } finally {
3120
+ await releaseLock(lockPath);
3121
+ }
3122
+ }
3123
+ async function atomicWriteFileOnce(filePath, data) {
3124
+ const tmpPath = filePath + `.tmp.${process.pid}.${randomUUID()}`;
3125
+ try {
3126
+ await fs2.writeFile(tmpPath, data, "utf-8");
3127
+ await fs2.rename(tmpPath, filePath);
3128
+ } catch (error) {
3129
+ await fs2.unlink(tmpPath).catch(() => {
3130
+ });
3131
+ throw error;
3132
+ }
3133
+ }
3134
+ function atomicWriteFile(filePath, data) {
3135
+ const previous = atomicWriteTails.get(filePath) ?? Promise.resolve();
3136
+ const next = previous.catch(() => void 0).then(() => atomicWriteFileOnce(filePath, data));
3137
+ atomicWriteTails.set(filePath, next);
3138
+ return next.finally(() => {
3139
+ if (atomicWriteTails.get(filePath) === next) atomicWriteTails.delete(filePath);
3140
+ });
3141
+ }
3142
+ var LOCK_STALE_MS, RETRY_INTERVAL_MS, MAX_RETRIES, atomicWriteTails;
3143
+ var init_file_lock = __esm({
3144
+ "src/store/file-lock.ts"() {
3145
+ "use strict";
3146
+ init_esm_shims();
3147
+ LOCK_STALE_MS = 1e4;
3148
+ RETRY_INTERVAL_MS = 50;
3149
+ MAX_RETRIES = 60;
3150
+ atomicWriteTails = /* @__PURE__ */ new Map();
3151
+ }
3152
+ });
3153
+
3044
3154
  // src/store/sqlite-store.ts
3045
3155
  var sqlite_store_exports = {};
3046
3156
  __export(sqlite_store_exports, {
3047
3157
  SqliteBackend: () => SqliteBackend
3048
3158
  });
3049
- import path5 from "path";
3050
- import fs2 from "fs";
3159
+ import path6 from "path";
3160
+ import fs3 from "fs";
3051
3161
  function obsToRow(obs) {
3052
3162
  return {
3053
3163
  id: obs.id,
@@ -3134,6 +3244,7 @@ var init_sqlite_store = __esm({
3134
3244
  init_sqlite_db();
3135
3245
  init_sqlite_reliability();
3136
3246
  init_sqlite_fts();
3247
+ init_file_lock();
3137
3248
  SqliteBackend = class {
3138
3249
  db = null;
3139
3250
  dataDir = "";
@@ -3297,38 +3408,43 @@ var init_sqlite_store = __esm({
3297
3408
  }
3298
3409
  // ── Migration ────────────────────────────────────────────────────
3299
3410
  async migrateFromJsonIfNeeded() {
3300
- const count2 = this.db.prepare(`SELECT COUNT(*) AS cnt FROM observations`).get();
3301
- if (count2.cnt > 0) return;
3302
- const jsonPath = path5.join(this.dataDir, "observations.json");
3303
- if (!fs2.existsSync(jsonPath)) return;
3304
- try {
3305
- const raw = fs2.readFileSync(jsonPath, "utf-8");
3306
- const observations2 = JSON.parse(raw);
3411
+ const jsonPath = path6.join(this.dataDir, "observations.json");
3412
+ if (!fs3.existsSync(jsonPath)) return;
3413
+ await withFileLock(this.dataDir, async () => {
3414
+ const count2 = this.db.prepare(`SELECT COUNT(*) AS cnt FROM observations`).get();
3415
+ if (count2.cnt > 0) return;
3416
+ const raw = fs3.readFileSync(jsonPath, "utf-8");
3417
+ const parsed = JSON.parse(raw);
3418
+ if (!Array.isArray(parsed)) {
3419
+ throw new Error("[memorix] observations.json migration refused: expected a JSON array");
3420
+ }
3421
+ const observations2 = parsed;
3307
3422
  if (observations2.length === 0) return;
3423
+ if (observations2.some((obs) => !obs || !Number.isSafeInteger(obs.id) || obs.id < 1 || typeof obs.projectId !== "string")) {
3424
+ throw new Error("[memorix] observations.json migration refused: invalid observation record");
3425
+ }
3308
3426
  console.error(`[memorix] Migrating ${observations2.length} observations from JSON to SQLite...`);
3309
3427
  const insertMany = this.db.transaction((obsList) => {
3310
3428
  for (const obs of obsList) {
3311
3429
  this.stmtInsert.run(obsToRow(obs));
3312
3430
  }
3431
+ const counterPath = path6.join(this.dataDir, "counter.json");
3432
+ let counterNextId;
3433
+ if (fs3.existsSync(counterPath)) {
3434
+ const counterData = JSON.parse(fs3.readFileSync(counterPath, "utf-8"));
3435
+ const candidate = counterData.nextId ?? counterData.next_id;
3436
+ if (candidate !== void 0 && (!Number.isSafeInteger(candidate) || Number(candidate) < 1)) {
3437
+ throw new Error("[memorix] counter.json migration refused: nextId must be a positive integer");
3438
+ }
3439
+ counterNextId = candidate === void 0 ? void 0 : Number(candidate);
3440
+ }
3441
+ const maxId = Math.max(...obsList.map((obs) => obs.id));
3442
+ this.rawSaveIdCounter(Math.max(counterNextId ?? 1, maxId + 1));
3443
+ this.bumpGeneration();
3313
3444
  });
3314
3445
  insertMany(observations2);
3315
- const counterPath = path5.join(this.dataDir, "counter.json");
3316
- if (fs2.existsSync(counterPath)) {
3317
- try {
3318
- const counterData = JSON.parse(fs2.readFileSync(counterPath, "utf-8"));
3319
- const nextId2 = counterData.nextId ?? counterData.next_id ?? Math.max(...observations2.map((o) => o.id)) + 1;
3320
- this.rawSaveIdCounter(nextId2);
3321
- } catch {
3322
- this.rawSaveIdCounter(Math.max(...observations2.map((o) => o.id)) + 1);
3323
- }
3324
- } else {
3325
- this.rawSaveIdCounter(Math.max(...observations2.map((o) => o.id)) + 1);
3326
- }
3327
- this.bumpGeneration();
3328
3446
  console.error(`[memorix] Migration complete. ${observations2.length} observations now in SQLite.`);
3329
- } catch (err) {
3330
- console.error(`[memorix] JSON\u2192SQLite migration failed (non-fatal, data preserved in JSON): ${err}`);
3331
- }
3447
+ });
3332
3448
  }
3333
3449
  // ── Public read ──────────────────────────────────────────────────
3334
3450
  async loadAll() {
@@ -3657,91 +3773,6 @@ var init_types = __esm({
3657
3773
  }
3658
3774
  });
3659
3775
 
3660
- // src/store/file-lock.ts
3661
- import { promises as fs3 } from "fs";
3662
- import { randomUUID } from "crypto";
3663
- import path6 from "path";
3664
- async function acquireLock(lockPath) {
3665
- for (let i2 = 0; i2 < MAX_RETRIES; i2++) {
3666
- try {
3667
- const fd = await fs3.open(lockPath, "wx");
3668
- await fd.writeFile(JSON.stringify({ pid: process.pid, time: Date.now() }));
3669
- await fd.close();
3670
- return;
3671
- } catch (err) {
3672
- const code = err instanceof Error && "code" in err ? err.code : void 0;
3673
- if (code === "EEXIST" || code === "EPERM") {
3674
- try {
3675
- const stat5 = await fs3.stat(lockPath);
3676
- if (Date.now() - stat5.mtimeMs > LOCK_STALE_MS) {
3677
- await fs3.unlink(lockPath).catch(() => {
3678
- });
3679
- continue;
3680
- }
3681
- } catch {
3682
- continue;
3683
- }
3684
- await new Promise((r2) => setTimeout(r2, RETRY_INTERVAL_MS));
3685
- } else {
3686
- throw err;
3687
- }
3688
- }
3689
- }
3690
- await fs3.unlink(lockPath).catch(() => {
3691
- });
3692
- try {
3693
- const fd = await fs3.open(lockPath, "wx");
3694
- await fd.writeFile(JSON.stringify({ pid: process.pid, time: Date.now() }));
3695
- await fd.close();
3696
- return;
3697
- } catch {
3698
- throw new Error(`Failed to acquire lock: ${lockPath} (timeout after ${MAX_RETRIES * RETRY_INTERVAL_MS}ms)`);
3699
- }
3700
- }
3701
- async function releaseLock(lockPath) {
3702
- await fs3.unlink(lockPath).catch(() => {
3703
- });
3704
- }
3705
- async function withFileLock(projectDir2, fn) {
3706
- const lockPath = path6.join(projectDir2, ".memorix.lock");
3707
- await acquireLock(lockPath);
3708
- try {
3709
- return await fn();
3710
- } finally {
3711
- await releaseLock(lockPath);
3712
- }
3713
- }
3714
- async function atomicWriteFileOnce(filePath, data) {
3715
- const tmpPath = filePath + `.tmp.${process.pid}.${randomUUID()}`;
3716
- try {
3717
- await fs3.writeFile(tmpPath, data, "utf-8");
3718
- await fs3.rename(tmpPath, filePath);
3719
- } catch (error) {
3720
- await fs3.unlink(tmpPath).catch(() => {
3721
- });
3722
- throw error;
3723
- }
3724
- }
3725
- function atomicWriteFile(filePath, data) {
3726
- const previous = atomicWriteTails.get(filePath) ?? Promise.resolve();
3727
- const next = previous.catch(() => void 0).then(() => atomicWriteFileOnce(filePath, data));
3728
- atomicWriteTails.set(filePath, next);
3729
- return next.finally(() => {
3730
- if (atomicWriteTails.get(filePath) === next) atomicWriteTails.delete(filePath);
3731
- });
3732
- }
3733
- var LOCK_STALE_MS, RETRY_INTERVAL_MS, MAX_RETRIES, atomicWriteTails;
3734
- var init_file_lock = __esm({
3735
- "src/store/file-lock.ts"() {
3736
- "use strict";
3737
- init_esm_shims();
3738
- LOCK_STALE_MS = 1e4;
3739
- RETRY_INTERVAL_MS = 50;
3740
- MAX_RETRIES = 60;
3741
- atomicWriteTails = /* @__PURE__ */ new Map();
3742
- }
3743
- });
3744
-
3745
3776
  // src/store/mini-skill-store.ts
3746
3777
  import path7 from "path";
3747
3778
  import fs4 from "fs";
@@ -805,18 +805,21 @@ CREATE TABLE IF NOT EXISTS chat_transcript (
805
805
  `;
806
806
  CREATE_GRAPH_ENTITIES_TABLE = `
807
807
  CREATE TABLE IF NOT EXISTS graph_entities (
808
- name TEXT PRIMARY KEY,
808
+ projectId TEXT NOT NULL,
809
+ name TEXT NOT NULL,
809
810
  entityType TEXT NOT NULL DEFAULT '',
810
- observations TEXT NOT NULL DEFAULT '[]'
811
+ observations TEXT NOT NULL DEFAULT '[]',
812
+ PRIMARY KEY (projectId, name)
811
813
  );
812
814
  `;
813
815
  CREATE_GRAPH_RELATIONS_TABLE = `
814
816
  CREATE TABLE IF NOT EXISTS graph_relations (
815
817
  id INTEGER PRIMARY KEY AUTOINCREMENT,
818
+ projectId TEXT NOT NULL,
816
819
  from_entity TEXT NOT NULL,
817
820
  to_entity TEXT NOT NULL,
818
821
  relationType TEXT NOT NULL DEFAULT '',
819
- UNIQUE(from_entity, to_entity, relationType)
822
+ UNIQUE(projectId, from_entity, to_entity, relationType)
820
823
  );
821
824
  `;
822
825
  CREATE_CODE_FILES_TABLE = `
@@ -2697,6 +2700,21 @@ var init_sqlite_reliability = __esm({
2697
2700
  import { promises as fs2 } from "fs";
2698
2701
  import { randomUUID as randomUUID3 } from "crypto";
2699
2702
  import path4 from "path";
2703
+ function canReclaimStaleLock(raw) {
2704
+ try {
2705
+ const parsed = JSON.parse(raw);
2706
+ const pid = Number(parsed.pid);
2707
+ if (!Number.isSafeInteger(pid) || pid < 1) return true;
2708
+ try {
2709
+ process.kill(pid, 0);
2710
+ return false;
2711
+ } catch (error) {
2712
+ return error.code !== "EPERM";
2713
+ }
2714
+ } catch {
2715
+ return true;
2716
+ }
2717
+ }
2700
2718
  async function acquireLock(lockPath) {
2701
2719
  for (let i2 = 0; i2 < MAX_RETRIES; i2++) {
2702
2720
  try {
@@ -2710,9 +2728,12 @@ async function acquireLock(lockPath) {
2710
2728
  try {
2711
2729
  const stat5 = await fs2.stat(lockPath);
2712
2730
  if (Date.now() - stat5.mtimeMs > LOCK_STALE_MS) {
2713
- await fs2.unlink(lockPath).catch(() => {
2714
- });
2715
- continue;
2731
+ const raw2 = await fs2.readFile(lockPath, "utf8").catch(() => "");
2732
+ if (canReclaimStaleLock(raw2)) {
2733
+ await fs2.unlink(lockPath).catch(() => {
2734
+ });
2735
+ continue;
2736
+ }
2716
2737
  }
2717
2738
  } catch {
2718
2739
  continue;
@@ -2723,6 +2744,10 @@ async function acquireLock(lockPath) {
2723
2744
  }
2724
2745
  }
2725
2746
  }
2747
+ const raw = await fs2.readFile(lockPath, "utf8").catch(() => "");
2748
+ if (!canReclaimStaleLock(raw)) {
2749
+ throw new Error(`Failed to acquire lock: ${lockPath} (owner is still running)`);
2750
+ }
2726
2751
  await fs2.unlink(lockPath).catch(() => {
2727
2752
  });
2728
2753
  try {
@@ -6653,6 +6678,7 @@ var init_sqlite_store = __esm({
6653
6678
  init_sqlite_db();
6654
6679
  init_sqlite_reliability();
6655
6680
  init_sqlite_fts();
6681
+ init_file_lock();
6656
6682
  SqliteBackend = class {
6657
6683
  db = null;
6658
6684
  dataDir = "";
@@ -6816,38 +6842,43 @@ var init_sqlite_store = __esm({
6816
6842
  }
6817
6843
  // ── Migration ────────────────────────────────────────────────────
6818
6844
  async migrateFromJsonIfNeeded() {
6819
- const count2 = this.db.prepare(`SELECT COUNT(*) AS cnt FROM observations`).get();
6820
- if (count2.cnt > 0) return;
6821
6845
  const jsonPath = path7.join(this.dataDir, "observations.json");
6822
6846
  if (!fs4.existsSync(jsonPath)) return;
6823
- try {
6847
+ await withFileLock(this.dataDir, async () => {
6848
+ const count2 = this.db.prepare(`SELECT COUNT(*) AS cnt FROM observations`).get();
6849
+ if (count2.cnt > 0) return;
6824
6850
  const raw = fs4.readFileSync(jsonPath, "utf-8");
6825
- const observations2 = JSON.parse(raw);
6851
+ const parsed = JSON.parse(raw);
6852
+ if (!Array.isArray(parsed)) {
6853
+ throw new Error("[memorix] observations.json migration refused: expected a JSON array");
6854
+ }
6855
+ const observations2 = parsed;
6826
6856
  if (observations2.length === 0) return;
6857
+ if (observations2.some((obs) => !obs || !Number.isSafeInteger(obs.id) || obs.id < 1 || typeof obs.projectId !== "string")) {
6858
+ throw new Error("[memorix] observations.json migration refused: invalid observation record");
6859
+ }
6827
6860
  console.error(`[memorix] Migrating ${observations2.length} observations from JSON to SQLite...`);
6828
6861
  const insertMany = this.db.transaction((obsList) => {
6829
6862
  for (const obs of obsList) {
6830
6863
  this.stmtInsert.run(obsToRow(obs));
6831
6864
  }
6832
- });
6833
- insertMany(observations2);
6834
- const counterPath = path7.join(this.dataDir, "counter.json");
6835
- if (fs4.existsSync(counterPath)) {
6836
- try {
6865
+ const counterPath = path7.join(this.dataDir, "counter.json");
6866
+ let counterNextId;
6867
+ if (fs4.existsSync(counterPath)) {
6837
6868
  const counterData = JSON.parse(fs4.readFileSync(counterPath, "utf-8"));
6838
- const nextId2 = counterData.nextId ?? counterData.next_id ?? Math.max(...observations2.map((o) => o.id)) + 1;
6839
- this.rawSaveIdCounter(nextId2);
6840
- } catch {
6841
- this.rawSaveIdCounter(Math.max(...observations2.map((o) => o.id)) + 1);
6869
+ const candidate = counterData.nextId ?? counterData.next_id;
6870
+ if (candidate !== void 0 && (!Number.isSafeInteger(candidate) || Number(candidate) < 1)) {
6871
+ throw new Error("[memorix] counter.json migration refused: nextId must be a positive integer");
6872
+ }
6873
+ counterNextId = candidate === void 0 ? void 0 : Number(candidate);
6842
6874
  }
6843
- } else {
6844
- this.rawSaveIdCounter(Math.max(...observations2.map((o) => o.id)) + 1);
6845
- }
6846
- this.bumpGeneration();
6875
+ const maxId = Math.max(...obsList.map((obs) => obs.id));
6876
+ this.rawSaveIdCounter(Math.max(counterNextId ?? 1, maxId + 1));
6877
+ this.bumpGeneration();
6878
+ });
6879
+ insertMany(observations2);
6847
6880
  console.error(`[memorix] Migration complete. ${observations2.length} observations now in SQLite.`);
6848
- } catch (err) {
6849
- console.error(`[memorix] JSON\u2192SQLite migration failed (non-fatal, data preserved in JSON): ${err}`);
6850
- }
6881
+ });
6851
6882
  }
6852
6883
  // ── Public read ──────────────────────────────────────────────────
6853
6884
  async loadAll() {
@@ -835,18 +835,21 @@ CREATE TABLE IF NOT EXISTS chat_transcript (
835
835
  `;
836
836
  CREATE_GRAPH_ENTITIES_TABLE = `
837
837
  CREATE TABLE IF NOT EXISTS graph_entities (
838
- name TEXT PRIMARY KEY,
838
+ projectId TEXT NOT NULL,
839
+ name TEXT NOT NULL,
839
840
  entityType TEXT NOT NULL DEFAULT '',
840
- observations TEXT NOT NULL DEFAULT '[]'
841
+ observations TEXT NOT NULL DEFAULT '[]',
842
+ PRIMARY KEY (projectId, name)
841
843
  );
842
844
  `;
843
845
  CREATE_GRAPH_RELATIONS_TABLE = `
844
846
  CREATE TABLE IF NOT EXISTS graph_relations (
845
847
  id INTEGER PRIMARY KEY AUTOINCREMENT,
848
+ projectId TEXT NOT NULL,
846
849
  from_entity TEXT NOT NULL,
847
850
  to_entity TEXT NOT NULL,
848
851
  relationType TEXT NOT NULL DEFAULT '',
849
- UNIQUE(from_entity, to_entity, relationType)
852
+ UNIQUE(projectId, from_entity, to_entity, relationType)
850
853
  );
851
854
  `;
852
855
  CREATE_CODE_FILES_TABLE = `
@@ -1714,6 +1717,21 @@ var init_sqlite_reliability = __esm({
1714
1717
  import { promises as fs2 } from "fs";
1715
1718
  import { randomUUID } from "crypto";
1716
1719
  import path4 from "path";
1720
+ function canReclaimStaleLock(raw) {
1721
+ try {
1722
+ const parsed = JSON.parse(raw);
1723
+ const pid = Number(parsed.pid);
1724
+ if (!Number.isSafeInteger(pid) || pid < 1) return true;
1725
+ try {
1726
+ process.kill(pid, 0);
1727
+ return false;
1728
+ } catch (error) {
1729
+ return error.code !== "EPERM";
1730
+ }
1731
+ } catch {
1732
+ return true;
1733
+ }
1734
+ }
1717
1735
  async function acquireLock(lockPath) {
1718
1736
  for (let i2 = 0; i2 < MAX_RETRIES; i2++) {
1719
1737
  try {
@@ -1727,9 +1745,12 @@ async function acquireLock(lockPath) {
1727
1745
  try {
1728
1746
  const stat5 = await fs2.stat(lockPath);
1729
1747
  if (Date.now() - stat5.mtimeMs > LOCK_STALE_MS) {
1730
- await fs2.unlink(lockPath).catch(() => {
1731
- });
1732
- continue;
1748
+ const raw2 = await fs2.readFile(lockPath, "utf8").catch(() => "");
1749
+ if (canReclaimStaleLock(raw2)) {
1750
+ await fs2.unlink(lockPath).catch(() => {
1751
+ });
1752
+ continue;
1753
+ }
1733
1754
  }
1734
1755
  } catch {
1735
1756
  continue;
@@ -1740,6 +1761,10 @@ async function acquireLock(lockPath) {
1740
1761
  }
1741
1762
  }
1742
1763
  }
1764
+ const raw = await fs2.readFile(lockPath, "utf8").catch(() => "");
1765
+ if (!canReclaimStaleLock(raw)) {
1766
+ throw new Error(`Failed to acquire lock: ${lockPath} (owner is still running)`);
1767
+ }
1743
1768
  await fs2.unlink(lockPath).catch(() => {
1744
1769
  });
1745
1770
  try {
@@ -5670,6 +5695,7 @@ var init_sqlite_store = __esm({
5670
5695
  init_sqlite_db();
5671
5696
  init_sqlite_reliability();
5672
5697
  init_sqlite_fts();
5698
+ init_file_lock();
5673
5699
  SqliteBackend = class {
5674
5700
  db = null;
5675
5701
  dataDir = "";
@@ -5833,38 +5859,43 @@ var init_sqlite_store = __esm({
5833
5859
  }
5834
5860
  // ── Migration ────────────────────────────────────────────────────
5835
5861
  async migrateFromJsonIfNeeded() {
5836
- const count2 = this.db.prepare(`SELECT COUNT(*) AS cnt FROM observations`).get();
5837
- if (count2.cnt > 0) return;
5838
5862
  const jsonPath = path7.join(this.dataDir, "observations.json");
5839
5863
  if (!fs4.existsSync(jsonPath)) return;
5840
- try {
5864
+ await withFileLock(this.dataDir, async () => {
5865
+ const count2 = this.db.prepare(`SELECT COUNT(*) AS cnt FROM observations`).get();
5866
+ if (count2.cnt > 0) return;
5841
5867
  const raw = fs4.readFileSync(jsonPath, "utf-8");
5842
- const observations2 = JSON.parse(raw);
5868
+ const parsed = JSON.parse(raw);
5869
+ if (!Array.isArray(parsed)) {
5870
+ throw new Error("[memorix] observations.json migration refused: expected a JSON array");
5871
+ }
5872
+ const observations2 = parsed;
5843
5873
  if (observations2.length === 0) return;
5874
+ if (observations2.some((obs) => !obs || !Number.isSafeInteger(obs.id) || obs.id < 1 || typeof obs.projectId !== "string")) {
5875
+ throw new Error("[memorix] observations.json migration refused: invalid observation record");
5876
+ }
5844
5877
  console.error(`[memorix] Migrating ${observations2.length} observations from JSON to SQLite...`);
5845
5878
  const insertMany = this.db.transaction((obsList) => {
5846
5879
  for (const obs of obsList) {
5847
5880
  this.stmtInsert.run(obsToRow(obs));
5848
5881
  }
5849
- });
5850
- insertMany(observations2);
5851
- const counterPath = path7.join(this.dataDir, "counter.json");
5852
- if (fs4.existsSync(counterPath)) {
5853
- try {
5882
+ const counterPath = path7.join(this.dataDir, "counter.json");
5883
+ let counterNextId;
5884
+ if (fs4.existsSync(counterPath)) {
5854
5885
  const counterData = JSON.parse(fs4.readFileSync(counterPath, "utf-8"));
5855
- const nextId2 = counterData.nextId ?? counterData.next_id ?? Math.max(...observations2.map((o) => o.id)) + 1;
5856
- this.rawSaveIdCounter(nextId2);
5857
- } catch {
5858
- this.rawSaveIdCounter(Math.max(...observations2.map((o) => o.id)) + 1);
5886
+ const candidate = counterData.nextId ?? counterData.next_id;
5887
+ if (candidate !== void 0 && (!Number.isSafeInteger(candidate) || Number(candidate) < 1)) {
5888
+ throw new Error("[memorix] counter.json migration refused: nextId must be a positive integer");
5889
+ }
5890
+ counterNextId = candidate === void 0 ? void 0 : Number(candidate);
5859
5891
  }
5860
- } else {
5861
- this.rawSaveIdCounter(Math.max(...observations2.map((o) => o.id)) + 1);
5862
- }
5863
- this.bumpGeneration();
5892
+ const maxId = Math.max(...obsList.map((obs) => obs.id));
5893
+ this.rawSaveIdCounter(Math.max(counterNextId ?? 1, maxId + 1));
5894
+ this.bumpGeneration();
5895
+ });
5896
+ insertMany(observations2);
5864
5897
  console.error(`[memorix] Migration complete. ${observations2.length} observations now in SQLite.`);
5865
- } catch (err) {
5866
- console.error(`[memorix] JSON\u2192SQLite migration failed (non-fatal, data preserved in JSON): ${err}`);
5867
- }
5898
+ });
5868
5899
  }
5869
5900
  // ── Public read ──────────────────────────────────────────────────
5870
5901
  async loadAll() {
@@ -2,7 +2,39 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [Unreleased]
5
+ ## [1.9.5] - 2026-09-20
6
+
7
+ ### Added
8
+ - **Setup HTTP MCP** -- `memorix setup --mcp http` honors Grok (writes a
9
+ `[mcp_servers.memorix]` URL in Grok `config.toml`) and rewrites installed
10
+ plugin/extension bundled MCP manifests from `memorix serve` to
11
+ `http://localhost:3211/mcp`. Default Grok MCP stays host-owned. Docs include
12
+ generic systemd user unit and LaunchAgent KeepAlive examples for
13
+ `memorix serve-http`.
14
+ - **User-global store sync** -- `memorix sync store push|pull|status --scope user`
15
+ replicates every local project's eligible observations through the existing
16
+ opt-in relay. Default `--scope project` is unchanged. Privacy filters still
17
+ drop personal, agent-targeted, candidate, and ephemeral rows. The live
18
+ SQLite file is never copied.
19
+
20
+ ### Fixed
21
+ - **HTTP control-plane safety** -- JSON request bodies are bounded, non-loopback
22
+ binds require an explicit Bearer token or unsafe opt-in, listen failures are
23
+ reported synchronously, background startup removes unverified state, and
24
+ shutdown waits for HTTP and SQLite resources to close.
25
+ - **Project-scoped graph storage** -- graph entities and relations now carry a
26
+ project key, legacy JSONL migration recovers ownership from observation
27
+ references, and Dashboard/MCP graph access no longer crosses projects.
28
+ - **Migration and lifecycle reliability** -- legacy observation/subdirectory
29
+ migrations use locks and atomic writes, malformed observation JSON fails
30
+ closed, stale file locks cannot evict a live writer, stateless MCP bindings
31
+ expire and are cleaned up, and SDK clients share resources safely.
32
+ - **User-scope sync correctness** -- imported observation IDs are remapped on
33
+ collision, tombstones validate their source project, and user namespaces
34
+ must be explicit per-user values.
35
+ - **Release contract** -- publish workflow validates tag/version/commit
36
+ identity, waits for npm visibility before MCP Registry publication, and
37
+ keeps prepublish checks non-mutating.
6
38
 
7
39
  ## [1.9.4] - 2026-09-18
8
40
 
package/dist/sdk.d.ts CHANGED
@@ -120,6 +120,7 @@ declare class MemoryClient {
120
120
  private _oramaStore;
121
121
  private _obsStore;
122
122
  private _freshness;
123
+ private _sdkRefRegistered;
123
124
  /** @internal — use createMemoryClient() instead */
124
125
  constructor(projectId: string, projectRoot: string, dataDir: string);
125
126
  /** The canonical project ID (derived from Git remote or local path) */