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.
- package/CHANGELOG.md +33 -1
- package/README.md +6 -5
- package/README.zh-CN.md +6 -5
- package/dist/cli/index.js +890 -325
- package/dist/index.d.ts +3 -1
- package/dist/index.js +251 -104
- package/dist/maintenance-runner.js +144 -113
- package/dist/media-audio-runner.js +57 -26
- package/dist/media-video-runner.js +57 -26
- package/dist/memcode-runtime/CHANGELOG.md +33 -1
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +1012 -842
- package/dist/types.d.ts +1 -1
- package/dist/vector-backfill-runner.js +57 -26
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
- package/docs/API_REFERENCE.md +3 -1
- package/docs/CONFIGURATION.md +15 -1
- package/docs/DOCKER.md +23 -3
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +26 -1
- package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
- package/docs/SETUP.md +55 -0
- package/package.json +2 -2
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/command-guide.ts +2 -2
- package/src/cli/commands/agent-integrations.ts +57 -0
- package/src/cli/commands/background.ts +11 -2
- package/src/cli/commands/serve-http.ts +193 -42
- package/src/cli/commands/setup.ts +77 -10
- package/src/cli/commands/sync-store.ts +47 -12
- package/src/cli/commands/sync.ts +26 -0
- package/src/dashboard/server.ts +10 -12
- package/src/integrations/registry.ts +1 -1
- package/src/memory/graph.ts +10 -8
- package/src/memory/session.ts +1 -1
- package/src/sdk.ts +24 -0
- package/src/server/mcp-binding-store.ts +14 -3
- package/src/server.ts +2 -2
- package/src/store/file-lock.ts +28 -3
- package/src/store/graph-store.ts +178 -46
- package/src/store/persistence.ts +14 -9
- package/src/store/sqlite-db.ts +6 -3
- package/src/store/sqlite-store.ts +32 -27
- package/src/sync/engine.ts +9 -7
- package/src/sync/namespace.ts +23 -0
- package/src/sync/policy.ts +19 -4
- package/src/sync/store-port.ts +43 -13
- package/src/sync/types.ts +2 -0
- package/src/types.ts +1 -1
- 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
|
-
|
|
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
|
|
3050
|
-
import
|
|
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
|
|
3301
|
-
if (
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
const raw =
|
|
3306
|
-
const
|
|
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
|
-
}
|
|
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
|
-
|
|
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.
|
|
2714
|
-
|
|
2715
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6834
|
-
|
|
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
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
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
|
-
|
|
6844
|
-
this.rawSaveIdCounter(Math.max(
|
|
6845
|
-
|
|
6846
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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.
|
|
1731
|
-
|
|
1732
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5851
|
-
|
|
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
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
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
|
-
|
|
5861
|
-
this.rawSaveIdCounter(Math.max(
|
|
5862
|
-
|
|
5863
|
-
|
|
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
|
-
}
|
|
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
|
-
## [
|
|
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) */
|