memorix 1.9.3 → 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 (65) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/README.md +7 -6
  3. package/README.zh-CN.md +7 -6
  4. package/dist/cli/index.js +930 -330
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.js +289 -109
  7. package/dist/maintenance-runner.js +179 -117
  8. package/dist/media-audio-runner.js +92 -30
  9. package/dist/media-video-runner.js +92 -30
  10. package/dist/memcode-runtime/CHANGELOG.md +49 -1
  11. package/dist/sdk.d.ts +1 -0
  12. package/dist/sdk.js +1050 -847
  13. package/dist/types.d.ts +1 -1
  14. package/dist/vector-backfill-runner.js +92 -30
  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/DEVELOPMENT.md +1 -1
  19. package/docs/DOCKER.md +23 -3
  20. package/docs/INTEGRATIONS.md +1 -1
  21. package/docs/KNOWN_ISSUES_AND_ROADMAP.md +26 -1
  22. package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
  23. package/docs/README.md +1 -1
  24. package/docs/SETUP.md +55 -0
  25. package/package.json +2 -2
  26. package/plugins/antigravity/memorix/plugin.json +1 -1
  27. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  28. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  29. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  30. package/plugins/copilot/memorix/plugin.json +1 -1
  31. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  32. package/plugins/hermes/memorix/plugin.yaml +1 -1
  33. package/plugins/omp/memorix/package.json +1 -1
  34. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  35. package/plugins/pi/memorix/package.json +1 -1
  36. package/server.json +2 -2
  37. package/src/cli/command-guide.ts +2 -2
  38. package/src/cli/commands/agent-integrations.ts +57 -0
  39. package/src/cli/commands/background.ts +11 -2
  40. package/src/cli/commands/media.ts +2 -0
  41. package/src/cli/commands/serve-http.ts +193 -42
  42. package/src/cli/commands/setup.ts +77 -10
  43. package/src/cli/commands/sync-store.ts +47 -12
  44. package/src/cli/commands/sync.ts +26 -0
  45. package/src/dashboard/server.ts +10 -12
  46. package/src/integrations/registry.ts +1 -1
  47. package/src/media/minimax.ts +26 -2
  48. package/src/media/video-jobs.ts +9 -1
  49. package/src/memory/graph.ts +10 -8
  50. package/src/memory/session.ts +1 -1
  51. package/src/sdk.ts +24 -0
  52. package/src/server/mcp-binding-store.ts +14 -3
  53. package/src/server.ts +5 -3
  54. package/src/store/file-lock.ts +28 -3
  55. package/src/store/graph-store.ts +178 -46
  56. package/src/store/persistence.ts +14 -9
  57. package/src/store/sqlite-db.ts +6 -3
  58. package/src/store/sqlite-store.ts +32 -27
  59. package/src/sync/engine.ts +9 -7
  60. package/src/sync/namespace.ts +23 -0
  61. package/src/sync/policy.ts +19 -4
  62. package/src/sync/store-port.ts +43 -13
  63. package/src/sync/types.ts +2 -0
  64. package/src/types.ts +1 -1
  65. 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";
@@ -143484,7 +143515,20 @@ function normalizeMiniMaxVideoRequest(input) {
143484
143515
  if (ratio !== "adaptive" && ratio !== "16:9" && ratio !== "9:16" && ratio !== "1:1") {
143485
143516
  throw new Error("MiniMax video ratio must be adaptive, 16:9, 9:16, or 1:1");
143486
143517
  }
143487
- return { region, model, prompt, resolution, duration, ratio };
143518
+ let firstFrameImageUrl;
143519
+ if (input.firstFrameImageUrl !== void 0) {
143520
+ firstFrameImageUrl = input.firstFrameImageUrl.trim();
143521
+ let parsed;
143522
+ try {
143523
+ parsed = new URL(firstFrameImageUrl);
143524
+ } catch {
143525
+ throw new Error("MiniMax first-frame image must be a public HTTPS URL");
143526
+ }
143527
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || sanitizeCredentials(firstFrameImageUrl) !== firstFrameImageUrl) {
143528
+ throw new Error("MiniMax first-frame image must be an HTTPS URL without credentials");
143529
+ }
143530
+ }
143531
+ return { region, model, prompt, resolution, duration, ratio, ...firstFrameImageUrl ? { firstFrameImageUrl } : {} };
143488
143532
  }
143489
143533
  function videoRequestConfig(input) {
143490
143534
  const normalized = normalizeMiniMaxVideoRequest(input);
@@ -143561,7 +143605,14 @@ async function createMiniMaxVideoTask(input, dependencies = {}) {
143561
143605
  headers: videoHeaders(config.apiKey),
143562
143606
  body: JSON.stringify({
143563
143607
  model: config.model,
143564
- content: [{ type: "text", text: config.prompt }],
143608
+ content: [
143609
+ { type: "text", text: config.prompt },
143610
+ ...config.firstFrameImageUrl ? [{
143611
+ type: "image_url",
143612
+ image_url: { url: config.firstFrameImageUrl },
143613
+ role: "first_frame"
143614
+ }] : []
143615
+ ],
143565
143616
  resolution: config.resolution,
143566
143617
  duration: config.duration,
143567
143618
  ratio: config.ratio
@@ -143722,10 +143773,19 @@ function parseStoredRequest(value) {
143722
143773
  const duration = value.duration;
143723
143774
  const ratio = value.ratio;
143724
143775
  const maxBytes = value.maxBytes;
143725
- if (provider2 !== "minimax" && provider2 !== "minimax-cn" || region !== "global" && region !== "cn" || model !== "MiniMax-H3" || typeof prompt !== "string" || resolution !== "2K" || duration !== 5 && duration !== 10 || ratio !== "adaptive" && ratio !== "16:9" && ratio !== "9:16" && ratio !== "1:1" || typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 1) {
143776
+ if (provider2 !== "minimax" && provider2 !== "minimax-cn" || region !== "global" && region !== "cn" || model !== "MiniMax-H3" || typeof prompt !== "string" || value.firstFrameImageUrl !== void 0 && typeof value.firstFrameImageUrl !== "string" || resolution !== "2K" || duration !== 5 && duration !== 10 || ratio !== "adaptive" && ratio !== "16:9" && ratio !== "9:16" && ratio !== "1:1" || typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 1) {
143726
143777
  throw new Error("Media video job has an invalid request payload");
143727
143778
  }
143728
- return { provider: provider2, region, model, prompt, resolution, duration, ratio, maxBytes };
143779
+ const normalized = normalizeMiniMaxVideoRequest({
143780
+ region,
143781
+ model,
143782
+ prompt,
143783
+ resolution,
143784
+ duration,
143785
+ ratio,
143786
+ firstFrameImageUrl: value.firstFrameImageUrl
143787
+ });
143788
+ return { provider: provider2, ...normalized, maxBytes };
143729
143789
  }
143730
143790
  function mediaJobIdFromMaintenance(job) {
143731
143791
  const mediaJobId = job.payload.mediaJobId;
@@ -143737,6 +143797,7 @@ function mediaJobIdFromMaintenance(job) {
143737
143797
  function toVideoRequest(request) {
143738
143798
  return {
143739
143799
  prompt: request.prompt,
143800
+ firstFrameImageUrl: request.firstFrameImageUrl,
143740
143801
  model: request.model,
143741
143802
  region: request.region,
143742
143803
  resolution: request.resolution,
@@ -143828,6 +143889,7 @@ function queueMiniMaxVideoGeneration(input) {
143828
143889
  region: normalized.region,
143829
143890
  model: normalized.model,
143830
143891
  prompt: normalized.prompt,
143892
+ ...normalized.firstFrameImageUrl ? { firstFrameImageUrl: normalized.firstFrameImageUrl } : {},
143831
143893
  resolution: normalized.resolution,
143832
143894
  duration: normalized.duration,
143833
143895
  ratio: normalized.ratio,
@@ -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() {
@@ -143409,7 +143440,20 @@ function normalizeMiniMaxVideoRequest(input) {
143409
143440
  if (ratio !== "adaptive" && ratio !== "16:9" && ratio !== "9:16" && ratio !== "1:1") {
143410
143441
  throw new Error("MiniMax video ratio must be adaptive, 16:9, 9:16, or 1:1");
143411
143442
  }
143412
- return { region, model, prompt, resolution, duration, ratio };
143443
+ let firstFrameImageUrl;
143444
+ if (input.firstFrameImageUrl !== void 0) {
143445
+ firstFrameImageUrl = input.firstFrameImageUrl.trim();
143446
+ let parsed;
143447
+ try {
143448
+ parsed = new URL(firstFrameImageUrl);
143449
+ } catch {
143450
+ throw new Error("MiniMax first-frame image must be a public HTTPS URL");
143451
+ }
143452
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || sanitizeCredentials(firstFrameImageUrl) !== firstFrameImageUrl) {
143453
+ throw new Error("MiniMax first-frame image must be an HTTPS URL without credentials");
143454
+ }
143455
+ }
143456
+ return { region, model, prompt, resolution, duration, ratio, ...firstFrameImageUrl ? { firstFrameImageUrl } : {} };
143413
143457
  }
143414
143458
  function videoRequestConfig(input) {
143415
143459
  const normalized = normalizeMiniMaxVideoRequest(input);
@@ -143486,7 +143530,14 @@ async function createMiniMaxVideoTask(input, dependencies = {}) {
143486
143530
  headers: videoHeaders(config.apiKey),
143487
143531
  body: JSON.stringify({
143488
143532
  model: config.model,
143489
- content: [{ type: "text", text: config.prompt }],
143533
+ content: [
143534
+ { type: "text", text: config.prompt },
143535
+ ...config.firstFrameImageUrl ? [{
143536
+ type: "image_url",
143537
+ image_url: { url: config.firstFrameImageUrl },
143538
+ role: "first_frame"
143539
+ }] : []
143540
+ ],
143490
143541
  resolution: config.resolution,
143491
143542
  duration: config.duration,
143492
143543
  ratio: config.ratio
@@ -143647,10 +143698,19 @@ function parseStoredRequest(value) {
143647
143698
  const duration = value.duration;
143648
143699
  const ratio = value.ratio;
143649
143700
  const maxBytes = value.maxBytes;
143650
- if (provider2 !== "minimax" && provider2 !== "minimax-cn" || region !== "global" && region !== "cn" || model !== "MiniMax-H3" || typeof prompt !== "string" || resolution !== "2K" || duration !== 5 && duration !== 10 || ratio !== "adaptive" && ratio !== "16:9" && ratio !== "9:16" && ratio !== "1:1" || typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 1) {
143701
+ if (provider2 !== "minimax" && provider2 !== "minimax-cn" || region !== "global" && region !== "cn" || model !== "MiniMax-H3" || typeof prompt !== "string" || value.firstFrameImageUrl !== void 0 && typeof value.firstFrameImageUrl !== "string" || resolution !== "2K" || duration !== 5 && duration !== 10 || ratio !== "adaptive" && ratio !== "16:9" && ratio !== "9:16" && ratio !== "1:1" || typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 1) {
143651
143702
  throw new Error("Media video job has an invalid request payload");
143652
143703
  }
143653
- return { provider: provider2, region, model, prompt, resolution, duration, ratio, maxBytes };
143704
+ const normalized = normalizeMiniMaxVideoRequest({
143705
+ region,
143706
+ model,
143707
+ prompt,
143708
+ resolution,
143709
+ duration,
143710
+ ratio,
143711
+ firstFrameImageUrl: value.firstFrameImageUrl
143712
+ });
143713
+ return { provider: provider2, ...normalized, maxBytes };
143654
143714
  }
143655
143715
  function mediaJobIdFromMaintenance(job) {
143656
143716
  const mediaJobId = job.payload.mediaJobId;
@@ -143662,6 +143722,7 @@ function mediaJobIdFromMaintenance(job) {
143662
143722
  function toVideoRequest(request) {
143663
143723
  return {
143664
143724
  prompt: request.prompt,
143725
+ firstFrameImageUrl: request.firstFrameImageUrl,
143665
143726
  model: request.model,
143666
143727
  region: request.region,
143667
143728
  resolution: request.resolution,
@@ -143753,6 +143814,7 @@ function queueMiniMaxVideoGeneration(input) {
143753
143814
  region: normalized.region,
143754
143815
  model: normalized.model,
143755
143816
  prompt: normalized.prompt,
143817
+ ...normalized.firstFrameImageUrl ? { firstFrameImageUrl: normalized.firstFrameImageUrl } : {},
143756
143818
  resolution: normalized.resolution,
143757
143819
  duration: normalized.duration,
143758
143820
  ratio: normalized.ratio,