memorix 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cli/index.js +1162 -1098
  3. package/dist/index.d.ts +513 -0
  4. package/dist/index.js +14751 -14044
  5. package/dist/maintenance-runner.js +105 -52
  6. package/dist/media-video-runner.js +105 -52
  7. package/dist/memcode-runtime/CHANGELOG.md +23 -0
  8. package/dist/sdk.d.ts +5 -513
  9. package/dist/sdk.js +186 -75
  10. package/dist/vector-backfill-runner.js +105 -52
  11. package/docs/1.3.3-MULTIMODAL-MEDIA-AND-MCP-COMPAT.md +13 -13
  12. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -3
  13. package/package.json +1 -1
  14. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  15. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  16. package/plugins/copilot/memorix/plugin.json +1 -1
  17. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  18. package/plugins/omp/memorix/package.json +1 -1
  19. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  20. package/plugins/pi/memorix/package.json +1 -1
  21. package/server.json +2 -2
  22. package/src/cli/commands/media.ts +10 -2
  23. package/src/cli/commands/serve-http.ts +7 -38
  24. package/src/cli/commands/serve-shared.ts +18 -46
  25. package/src/cli/commands/serve.ts +3 -41
  26. package/src/cli/commands/setup.ts +24 -21
  27. package/src/cli/mcp-root-path.ts +36 -0
  28. package/src/index.ts +37 -55
  29. package/src/media/asset-store.ts +74 -6
  30. package/src/media/attachment.ts +12 -9
  31. package/src/media/media-store.ts +38 -4
  32. package/src/media/video-jobs.ts +39 -14
  33. package/src/memory/observations.ts +21 -2
  34. package/src/orchestrate/adapters/spawn-helper.ts +2 -0
  35. package/src/server/tool-profile.ts +7 -4
  36. package/src/server.ts +6 -2
package/dist/sdk.js CHANGED
@@ -1186,8 +1186,8 @@ async function acquireLock(lockPath) {
1186
1186
  const code = err instanceof Error && "code" in err ? err.code : void 0;
1187
1187
  if (code === "EEXIST" || code === "EPERM") {
1188
1188
  try {
1189
- const stat3 = await fs2.stat(lockPath);
1190
- if (Date.now() - stat3.mtimeMs > LOCK_STALE_MS) {
1189
+ const stat4 = await fs2.stat(lockPath);
1190
+ if (Date.now() - stat4.mtimeMs > LOCK_STALE_MS) {
1191
1191
  await fs2.unlink(lockPath).catch(() => {
1192
1192
  });
1193
1193
  continue;
@@ -9347,6 +9347,10 @@ var init_media_store = __esm({
9347
9347
  `).run(projectId, assetId).changes ?? 0);
9348
9348
  this.db.prepare("DELETE FROM media_derivations WHERE project_id = ? AND asset_id = ?").run(projectId, assetId);
9349
9349
  this.db.prepare("DELETE FROM media_embeddings WHERE project_id = ? AND asset_id = ?").run(projectId, assetId);
9350
+ this.db.prepare(`
9351
+ UPDATE media_jobs SET asset_id = NULL, updated_at = ?
9352
+ WHERE project_id = ? AND asset_id = ?
9353
+ `).run(Date.now(), projectId, assetId);
9350
9354
  const result = this.db.prepare(`
9351
9355
  UPDATE media_assets SET deleted_at = ?, updated_at = ?
9352
9356
  WHERE project_id = ? AND id = ? AND deleted_at IS NULL
@@ -9521,16 +9525,28 @@ var init_media_store = __esm({
9521
9525
  return row ? rowToJob2(row) : void 0;
9522
9526
  }
9523
9527
  updateJob(projectId, id, input) {
9528
+ const updated = this.updateJobInternal(projectId, id, input, false);
9529
+ if (!updated) throw new Error(`Media job not found: ${id}`);
9530
+ return updated;
9531
+ }
9532
+ /**
9533
+ * Atomically preserve a user cancellation against a worker that was already
9534
+ * downloading or attaching media when the cancellation arrived.
9535
+ */
9536
+ updateJobIfNotCancelled(projectId, id, input) {
9537
+ return this.updateJobInternal(projectId, id, input, true);
9538
+ }
9539
+ updateJobInternal(projectId, id, input, rejectCancelled) {
9524
9540
  const existing = this.getJob(projectId, id);
9525
- if (!existing) throw new Error(`Media job not found: ${id}`);
9541
+ if (!existing) return void 0;
9526
9542
  const now3 = Date.now();
9527
9543
  const attempts = existing.attempts + (input.incrementAttempts ? 1 : 0);
9528
9544
  const completedAt = input.status === "completed" ? now3 : null;
9529
- this.db.prepare(`
9545
+ const result = this.db.prepare(`
9530
9546
  UPDATE media_jobs
9531
9547
  SET status = ?, provider_task_id = COALESCE(?, provider_task_id), asset_id = COALESCE(?, asset_id),
9532
9548
  last_error = ?, attempts = ?, updated_at = ?, completed_at = ?
9533
- WHERE project_id = ? AND id = ?
9549
+ WHERE project_id = ? AND id = ? ${rejectCancelled ? "AND status <> 'cancelled'" : ""}
9534
9550
  `).run(
9535
9551
  input.status,
9536
9552
  input.providerTaskId ?? null,
@@ -9542,6 +9558,7 @@ var init_media_store = __esm({
9542
9558
  projectId,
9543
9559
  id
9544
9560
  );
9561
+ if (Number(result.changes ?? 0) !== 1) return void 0;
9545
9562
  return this.getJob(projectId, id);
9546
9563
  }
9547
9564
  cancelJob(projectId, id) {
@@ -9591,6 +9608,7 @@ async function attachMediaAssetToObservation(input) {
9591
9608
  `MIME type: ${asset.mimeType}`,
9592
9609
  ...input.facts ?? []
9593
9610
  ];
9611
+ const store = new MediaStore(input.dataDir);
9594
9612
  const result = await storeObservation({
9595
9613
  entityName: input.entityName?.trim() || sourceLabel4.replace(/\.[^.]+$/, "") || "media-asset",
9596
9614
  type: "discovery",
@@ -9603,13 +9621,15 @@ async function attachMediaAssetToObservation(input) {
9603
9621
  sourceDetail: "explicit",
9604
9622
  ...input.visibility ? { visibility: input.visibility } : {},
9605
9623
  ...input.createdByAgentId ? { createdByAgentId: input.createdByAgentId } : {},
9606
- ...input.visibilityReader ? { visibilityReader: input.visibilityReader } : {}
9607
- });
9608
- new MediaStore(input.dataDir).linkAsset({
9609
- projectId: input.projectId,
9610
- assetId: asset.id,
9611
- observationId: result.observation.id,
9612
- role: input.role ?? "attachment"
9624
+ ...input.visibilityReader ? { visibilityReader: input.visibilityReader } : {},
9625
+ onPersisted: (observation) => {
9626
+ store.linkAsset({
9627
+ projectId: input.projectId,
9628
+ assetId: asset.id,
9629
+ observationId: observation.id,
9630
+ role: input.role ?? "attachment"
9631
+ });
9632
+ }
9613
9633
  });
9614
9634
  return result.observation;
9615
9635
  }
@@ -9637,6 +9657,7 @@ var init_types2 = __esm({
9637
9657
  var asset_store_exports = {};
9638
9658
  __export(asset_store_exports, {
9639
9659
  cleanupMediaQuota: () => cleanupMediaQuota,
9660
+ cleanupMediaTrash: () => cleanupMediaTrash,
9640
9661
  getMediaAssetPath: () => getMediaAssetPath,
9641
9662
  importMediaBuffer: () => importMediaBuffer,
9642
9663
  importMediaFile: () => importMediaFile,
@@ -9644,7 +9665,7 @@ __export(asset_store_exports, {
9644
9665
  removeMediaAsset: () => removeMediaAsset
9645
9666
  });
9646
9667
  import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
9647
- import { copyFile, lstat, mkdir as mkdir3, open, readFile as readFile3, rename, rm, writeFile as writeFile3 } from "fs/promises";
9668
+ import { copyFile, lstat, mkdir as mkdir3, open, readFile as readFile3, readdir, rename, rm, stat, writeFile as writeFile3 } from "fs/promises";
9648
9669
  import path13 from "path";
9649
9670
  function mediaRoot(dataDir) {
9650
9671
  return path13.resolve(dataDir, "media");
@@ -9790,11 +9811,11 @@ async function importKnownMedia(input) {
9790
9811
  async function importMediaFile(input) {
9791
9812
  const maxBytes = input.maxBytes ?? DEFAULT_MAX_MEDIA_BYTES;
9792
9813
  const source = path13.resolve(input.filePath);
9793
- const stat3 = await lstat(source);
9794
- if (stat3.isSymbolicLink()) throw new Error("Media import does not follow symbolic links");
9795
- if (!stat3.isFile()) throw new Error("Media import accepts regular files only");
9796
- if (stat3.size <= 0) throw new Error("Media import rejects empty files");
9797
- if (stat3.size > maxBytes) throw new Error(`Media file is too large (${stat3.size} bytes; limit ${maxBytes})`);
9814
+ const stat4 = await lstat(source);
9815
+ if (stat4.isSymbolicLink()) throw new Error("Media import does not follow symbolic links");
9816
+ if (!stat4.isFile()) throw new Error("Media import accepts regular files only");
9817
+ if (stat4.size <= 0) throw new Error("Media import rejects empty files");
9818
+ if (stat4.size > maxBytes) throw new Error(`Media file is too large (${stat4.size} bytes; limit ${maxBytes})`);
9798
9819
  const header = await readHeader(source);
9799
9820
  const detected = detectMedia(header);
9800
9821
  const sha256 = await sha256File(source);
@@ -9802,7 +9823,7 @@ async function importMediaFile(input) {
9802
9823
  dataDir: input.dataDir,
9803
9824
  projectId: input.projectId,
9804
9825
  sha256,
9805
- bytes: stat3.size,
9826
+ bytes: stat4.size,
9806
9827
  detected,
9807
9828
  persist: (destination) => persistFile(source, destination),
9808
9829
  sourceKind: input.sourceKind ?? "import",
@@ -9865,16 +9886,58 @@ async function removeMediaAsset(input) {
9865
9886
  }
9866
9887
  await rm(stagedPath, { force: true }).catch(() => {
9867
9888
  });
9868
- return { asset, detachedLinks };
9889
+ return { asset, detachedLinks, pendingDelete: await destinationExists(stagedPath) };
9890
+ }
9891
+ async function cleanupMediaTrash(dataDir) {
9892
+ const root = mediaRoot(dataDir);
9893
+ const trashDir = validateUnderRoot(root, path13.join(root, ".trash"));
9894
+ let entries;
9895
+ try {
9896
+ entries = await readdir(trashDir);
9897
+ } catch (error) {
9898
+ if (error?.code === "ENOENT") return { reclaimedBytes: 0, pendingBytes: 0, pendingFiles: 0 };
9899
+ throw error;
9900
+ }
9901
+ let reclaimedBytes = 0;
9902
+ let pendingBytes = 0;
9903
+ let pendingFiles = 0;
9904
+ for (const name of entries) {
9905
+ if (!name.endsWith(".pending-delete")) continue;
9906
+ const candidate = validateUnderRoot(trashDir, path13.join(trashDir, name));
9907
+ try {
9908
+ const metadata = await lstat(candidate);
9909
+ if (!metadata.isFile()) continue;
9910
+ await rm(candidate, { force: true });
9911
+ reclaimedBytes += metadata.size;
9912
+ } catch (error) {
9913
+ if (error?.code === "ENOENT") continue;
9914
+ try {
9915
+ pendingBytes += (await stat(candidate)).size;
9916
+ } catch {
9917
+ }
9918
+ pendingFiles += 1;
9919
+ }
9920
+ }
9921
+ return { reclaimedBytes, pendingBytes, pendingFiles };
9869
9922
  }
9870
9923
  async function cleanupMediaQuota(input) {
9871
9924
  const store = new MediaStore(input.dataDir);
9872
9925
  const maxBytes = input.maxBytes ?? DEFAULT_MEDIA_QUOTA_BYTES;
9873
9926
  if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new Error("maxBytes must be a non-negative integer");
9927
+ const initialTrash = await cleanupMediaTrash(input.dataDir);
9874
9928
  const beforeBytes = store.activeByteSize(input.projectId);
9875
9929
  let currentBytes = beforeBytes;
9876
9930
  const removed = [];
9877
- if (currentBytes <= maxBytes) return { beforeBytes, afterBytes: currentBytes, removed };
9931
+ if (currentBytes <= maxBytes) {
9932
+ return {
9933
+ beforeBytes,
9934
+ afterBytes: currentBytes,
9935
+ removed,
9936
+ reclaimedTrashBytes: initialTrash.reclaimedBytes,
9937
+ pendingTrashBytes: initialTrash.pendingBytes,
9938
+ pendingTrashFiles: initialTrash.pendingFiles
9939
+ };
9940
+ }
9878
9941
  const candidates = store.listUnlinkedAssets(input.projectId);
9879
9942
  for (const asset of candidates) {
9880
9943
  if (currentBytes <= maxBytes) break;
@@ -9887,7 +9950,15 @@ async function cleanupMediaQuota(input) {
9887
9950
  currentBytes -= asset.byteSize;
9888
9951
  removed.push(asset);
9889
9952
  }
9890
- return { beforeBytes, afterBytes: Math.max(0, currentBytes), removed };
9953
+ const finalTrash = await cleanupMediaTrash(input.dataDir);
9954
+ return {
9955
+ beforeBytes,
9956
+ afterBytes: Math.max(0, currentBytes),
9957
+ removed,
9958
+ reclaimedTrashBytes: initialTrash.reclaimedBytes + finalTrash.reclaimedBytes,
9959
+ pendingTrashBytes: finalTrash.pendingBytes,
9960
+ pendingTrashFiles: finalTrash.pendingFiles
9961
+ };
9891
9962
  }
9892
9963
  var init_asset_store = __esm({
9893
9964
  "src/media/asset-store.ts"() {
@@ -60894,7 +60965,7 @@ var require_node_domexception = __commonJS({
60894
60965
  // node_modules/fetch-blob/from.js
60895
60966
  import { statSync as statSync2, createReadStream, promises as fs10 } from "fs";
60896
60967
  import { basename } from "path";
60897
- var import_node_domexception, stat, blobFromSync, blobFrom, fileFrom, fileFromSync, fromBlob, fromFile, BlobDataItem;
60968
+ var import_node_domexception, stat2, blobFromSync, blobFrom, fileFrom, fileFromSync, fromBlob, fromFile, BlobDataItem;
60898
60969
  var init_from = __esm({
60899
60970
  "node_modules/fetch-blob/from.js"() {
60900
60971
  "use strict";
@@ -60902,23 +60973,23 @@ var init_from = __esm({
60902
60973
  import_node_domexception = __toESM(require_node_domexception(), 1);
60903
60974
  init_file();
60904
60975
  init_fetch_blob();
60905
- ({ stat } = fs10);
60976
+ ({ stat: stat2 } = fs10);
60906
60977
  blobFromSync = (path33, type) => fromBlob(statSync2(path33), path33, type);
60907
- blobFrom = (path33, type) => stat(path33).then((stat3) => fromBlob(stat3, path33, type));
60908
- fileFrom = (path33, type) => stat(path33).then((stat3) => fromFile(stat3, path33, type));
60978
+ blobFrom = (path33, type) => stat2(path33).then((stat4) => fromBlob(stat4, path33, type));
60979
+ fileFrom = (path33, type) => stat2(path33).then((stat4) => fromFile(stat4, path33, type));
60909
60980
  fileFromSync = (path33, type) => fromFile(statSync2(path33), path33, type);
60910
- fromBlob = (stat3, path33, type = "") => new fetch_blob_default([new BlobDataItem({
60981
+ fromBlob = (stat4, path33, type = "") => new fetch_blob_default([new BlobDataItem({
60911
60982
  path: path33,
60912
- size: stat3.size,
60913
- lastModified: stat3.mtimeMs,
60983
+ size: stat4.size,
60984
+ lastModified: stat4.mtimeMs,
60914
60985
  start: 0
60915
60986
  })], { type });
60916
- fromFile = (stat3, path33, type = "") => new file_default([new BlobDataItem({
60987
+ fromFile = (stat4, path33, type = "") => new file_default([new BlobDataItem({
60917
60988
  path: path33,
60918
- size: stat3.size,
60919
- lastModified: stat3.mtimeMs,
60989
+ size: stat4.size,
60990
+ lastModified: stat4.mtimeMs,
60920
60991
  start: 0
60921
- })], basename(path33), { type, lastModified: stat3.mtimeMs });
60992
+ })], basename(path33), { type, lastModified: stat4.mtimeMs });
60922
60993
  BlobDataItem = class _BlobDataItem {
60923
60994
  #path;
60924
60995
  #start;
@@ -60941,7 +61012,7 @@ var init_from = __esm({
60941
61012
  });
60942
61013
  }
60943
61014
  async *stream() {
60944
- const { mtimeMs } = await stat(this.#path);
61015
+ const { mtimeMs } = await stat2(this.#path);
60945
61016
  if (mtimeMs > this.lastModified) {
60946
61017
  throw new import_node_domexception.default("The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.", "NotReadableError");
60947
61018
  }
@@ -143218,10 +143289,17 @@ function sourceLabel2(request, providerTaskId) {
143218
143289
  }
143219
143290
  async function finishWithAsset(input) {
143220
143291
  const { dataDir, projectId, store, mediaJob, request, providerTask, dependencies } = input;
143221
- let asset = mediaJob.assetId ? store.getAsset(projectId, mediaJob.assetId) : void 0;
143292
+ let activeJob = store.getJob(projectId, mediaJob.id);
143293
+ if (!activeJob || terminal(activeJob)) return { action: "complete" };
143294
+ let asset = activeJob.assetId ? store.getAsset(projectId, activeJob.assetId) : void 0;
143222
143295
  if (!asset) {
143223
143296
  if (!providerTask.downloadUrl) throw new Error("Succeeded MiniMax video task did not include a download URL");
143224
- store.updateJob(projectId, mediaJob.id, { status: "downloading", providerTaskId: providerTask.taskId });
143297
+ const downloading = store.updateJobIfNotCancelled(projectId, activeJob.id, {
143298
+ status: "downloading",
143299
+ providerTaskId: providerTask.taskId
143300
+ });
143301
+ if (!downloading) return { action: "complete" };
143302
+ activeJob = downloading;
143225
143303
  const bytes = await (dependencies.download ?? ((downloadInput) => downloadProviderMedia(downloadInput)))({
143226
143304
  url: providerTask.downloadUrl,
143227
143305
  maxBytes: request.maxBytes
@@ -143238,21 +143316,30 @@ async function finishWithAsset(input) {
143238
143316
  maxBytes: request.maxBytes
143239
143317
  });
143240
143318
  asset = imported.asset;
143319
+ const persistedAsset = store.updateJobIfNotCancelled(projectId, activeJob.id, {
143320
+ status: "downloading",
143321
+ providerTaskId: providerTask.taskId,
143322
+ assetId: asset.id
143323
+ });
143324
+ if (!persistedAsset) return { action: "complete" };
143325
+ activeJob = persistedAsset;
143241
143326
  }
143242
- if (mediaJob.attachOnComplete) {
143327
+ if (activeJob.attachOnComplete) {
143328
+ activeJob = store.getJob(projectId, activeJob.id);
143329
+ if (!activeJob || terminal(activeJob)) return { action: "complete" };
143243
143330
  const hasAttachment = store.listLinks(projectId, asset.id).some((link) => link.role === "attachment" && link.observationId !== void 0);
143244
143331
  if (!hasAttachment) {
143245
143332
  await (dependencies.attach ?? attachMediaAssetToObservation)({
143246
143333
  dataDir,
143247
143334
  projectId,
143248
143335
  asset,
143249
- title: mediaJob.observationTitle ?? `MiniMax video: ${asset.sourceLabel ?? asset.id}`,
143336
+ title: activeJob.observationTitle ?? `MiniMax video: ${asset.sourceLabel ?? asset.id}`,
143250
143337
  narrative: `Generated with ${request.provider}/${request.model}. Prompt: ${request.prompt}`,
143251
143338
  concepts: ["generated-video", "minimax", request.model]
143252
143339
  });
143253
143340
  }
143254
143341
  }
143255
- store.updateJob(projectId, mediaJob.id, {
143342
+ store.updateJobIfNotCancelled(projectId, activeJob.id, {
143256
143343
  status: "completed",
143257
143344
  providerTaskId: providerTask.taskId,
143258
143345
  assetId: asset.id
@@ -143262,10 +143349,12 @@ async function finishWithAsset(input) {
143262
143349
  function retryAfterProviderError(store, projectId, mediaJob, maintenanceJob, error) {
143263
143350
  const message = safeError2(error);
143264
143351
  if (maintenanceJob.attempts >= maintenanceJob.maxAttempts) {
143265
- store.updateJob(projectId, mediaJob.id, { status: "failed", lastError: message, incrementAttempts: true });
143352
+ store.updateJobIfNotCancelled(projectId, mediaJob.id, { status: "failed", lastError: message, incrementAttempts: true });
143353
+ return { action: "complete" };
143354
+ }
143355
+ if (!store.updateJobIfNotCancelled(projectId, mediaJob.id, { status: "retry", lastError: message, incrementAttempts: true })) {
143266
143356
  return { action: "complete" };
143267
143357
  }
143268
- store.updateJob(projectId, mediaJob.id, { status: "retry", lastError: message, incrementAttempts: true });
143269
143358
  return { action: "reschedule", delayMs: MINI_MAX_VIDEO_POLL_MS, status: "retry", lastError: message };
143270
143359
  }
143271
143360
  function queueMiniMaxVideoGeneration(input) {
@@ -143306,22 +143395,25 @@ async function runMiniMaxVideoGenerationJob(maintenanceJob, context, dependencie
143306
143395
  if (mediaJob.kind !== "minimax-video-generation") throw new Error(`Unsupported media job kind: ${mediaJob.kind}`);
143307
143396
  const request = parseStoredRequest(mediaJob.request);
143308
143397
  if (!mediaJob.providerTaskId) {
143309
- store.updateJob(context.projectId, mediaJob.id, { status: "submitting", incrementAttempts: true });
143398
+ if (!store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "submitting", incrementAttempts: true })) {
143399
+ return { action: "complete" };
143400
+ }
143310
143401
  let submitted;
143311
143402
  try {
143312
143403
  submitted = await (dependencies.createTask ?? createMiniMaxVideoTask)(toVideoRequest(request));
143313
143404
  } catch (error) {
143314
- store.updateJob(context.projectId, mediaJob.id, { status: "failed", lastError: safeError2(error) });
143405
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "failed", lastError: safeError2(error) });
143315
143406
  return { action: "complete" };
143316
143407
  }
143317
143408
  if (submitted.status === "failed") {
143318
- store.updateJob(context.projectId, mediaJob.id, { status: "failed", providerTaskId: submitted.taskId, lastError: submitted.error });
143409
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "failed", providerTaskId: submitted.taskId, lastError: submitted.error });
143319
143410
  return { action: "complete" };
143320
143411
  }
143321
- const pending = store.updateJob(context.projectId, mediaJob.id, {
143412
+ const pending = store.updateJobIfNotCancelled(context.projectId, mediaJob.id, {
143322
143413
  status: submitted.status === "succeeded" ? "downloading" : "provider-pending",
143323
143414
  providerTaskId: submitted.taskId
143324
143415
  });
143416
+ if (!pending) return { action: "complete" };
143325
143417
  if (submitted.status === "succeeded") {
143326
143418
  try {
143327
143419
  return await finishWithAsset({
@@ -143349,11 +143441,13 @@ async function runMiniMaxVideoGenerationJob(maintenanceJob, context, dependencie
143349
143441
  return retryAfterProviderError(store, context.projectId, mediaJob, maintenanceJob, error);
143350
143442
  }
143351
143443
  if (task.status === "pending") {
143352
- store.updateJob(context.projectId, mediaJob.id, { status: "provider-pending", providerTaskId: task.taskId });
143444
+ if (!store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "provider-pending", providerTaskId: task.taskId })) {
143445
+ return { action: "complete" };
143446
+ }
143353
143447
  return { action: "reschedule", delayMs: MINI_MAX_VIDEO_POLL_MS, resetAttempts: true, clearLastError: true };
143354
143448
  }
143355
143449
  if (task.status === "failed") {
143356
- store.updateJob(context.projectId, mediaJob.id, { status: "failed", providerTaskId: task.taskId, lastError: task.error, incrementAttempts: true });
143450
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "failed", providerTaskId: task.taskId, lastError: task.error, incrementAttempts: true });
143357
143451
  return { action: "complete" };
143358
143452
  }
143359
143453
  try {
@@ -144636,7 +144730,7 @@ function indexFileLite(projectId, projectRoot, abs, indexedAt, fileStat) {
144636
144730
  const rel = normalizeCodePath(relative(projectRoot, abs));
144637
144731
  try {
144638
144732
  const text = readFileSync6(abs, "utf-8");
144639
- const stat3 = fileStat ?? statSync3(abs);
144733
+ const stat4 = fileStat ?? statSync3(abs);
144640
144734
  const file = {
144641
144735
  id: makeCodeFileId(projectId, rel),
144642
144736
  projectId,
@@ -144645,8 +144739,8 @@ function indexFileLite(projectId, projectRoot, abs, indexedAt, fileStat) {
144645
144739
  contentHash: hashText(text),
144646
144740
  // Preserve filesystem precision. Rounding here made same-size edits in
144647
144741
  // a short Windows timestamp window indistinguishable to refresh.
144648
- mtimeMs: Number(stat3.mtimeMs),
144649
- sizeBytes: Number(stat3.size),
144742
+ mtimeMs: Number(stat4.mtimeMs),
144743
+ sizeBytes: Number(stat4.size),
144650
144744
  indexedAt
144651
144745
  };
144652
144746
  return {
@@ -144673,18 +144767,18 @@ async function indexProjectLite(options) {
144673
144767
  let skippedOversizedFiles = 0;
144674
144768
  let unreadableFiles = 0;
144675
144769
  for (const abs of paths) {
144676
- let stat3;
144770
+ let stat4;
144677
144771
  try {
144678
- stat3 = statSync3(abs);
144772
+ stat4 = statSync3(abs);
144679
144773
  } catch {
144680
144774
  unreadableFiles++;
144681
144775
  continue;
144682
144776
  }
144683
- if (Number(stat3.size) > maxFileBytes) {
144777
+ if (Number(stat4.size) > maxFileBytes) {
144684
144778
  skippedOversizedFiles++;
144685
144779
  continue;
144686
144780
  }
144687
- const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat3);
144781
+ const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat4);
144688
144782
  if (!attempt.delta) {
144689
144783
  if (attempt.unreadable) unreadableFiles++;
144690
144784
  continue;
@@ -144712,16 +144806,16 @@ async function refreshProjectLite(store, options) {
144712
144806
  for (const abs of paths) {
144713
144807
  const rel = normalizeCodePath(relative(options.projectRoot, abs));
144714
144808
  seenPaths.add(rel);
144715
- let stat3;
144809
+ let stat4;
144716
144810
  try {
144717
- stat3 = statSync3(abs);
144811
+ stat4 = statSync3(abs);
144718
144812
  } catch {
144719
144813
  unreadableFiles++;
144720
144814
  continue;
144721
144815
  }
144722
144816
  const existing = existingByPath.get(rel);
144723
- const mtimeMs = Number(stat3.mtimeMs);
144724
- const sizeBytes = Number(stat3.size);
144817
+ const mtimeMs = Number(stat4.mtimeMs);
144818
+ const sizeBytes = Number(stat4.size);
144725
144819
  if (sizeBytes > maxFileBytes) {
144726
144820
  skippedOversizedFiles++;
144727
144821
  if (existing) oversizedExistingFileIds.add(existing.id);
@@ -144731,7 +144825,7 @@ async function refreshProjectLite(store, options) {
144731
144825
  unchangedFiles++;
144732
144826
  continue;
144733
144827
  }
144734
- const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat3);
144828
+ const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat4);
144735
144829
  if (!attempt.delta) {
144736
144830
  if (attempt.unreadable) unreadableFiles++;
144737
144831
  continue;
@@ -148513,7 +148607,9 @@ async function storeObservation(input) {
148513
148607
  if (input.visibilityReader && !canManageObservation(existing, input.visibilityReader)) {
148514
148608
  throw new Error("Cannot update a memory outside this session's write scope.");
148515
148609
  }
148516
- return { observation: await upsertObservation(existing, input, now3), upserted: true };
148610
+ const observation2 = await upsertObservation(existing, input, now3);
148611
+ await input.onPersisted?.(observation2);
148612
+ return { observation: observation2, upserted: true };
148517
148613
  }
148518
148614
  }
148519
148615
  const contentForExtraction = [input.title, input.narrative, ...input.facts ?? []].join(" ");
@@ -148538,6 +148634,7 @@ async function storeObservation(input) {
148538
148634
  let doc;
148539
148635
  let upsertedInsideLock = false;
148540
148636
  let reloadCacheAfterCommit = false;
148637
+ let persistedCallbackInvoked = false;
148541
148638
  const assignAndPersist = async () => {
148542
148639
  if (projectDir) {
148543
148640
  const store = getObservationStore();
@@ -148593,6 +148690,8 @@ async function storeObservation(input) {
148593
148690
  writeGeneration: transactionGeneration + 1
148594
148691
  };
148595
148692
  await tx.insert(observation);
148693
+ await input.onPersisted?.(observation);
148694
+ persistedCallbackInvoked = true;
148596
148695
  await tx.saveIdCounter(id + 1);
148597
148696
  });
148598
148697
  observation.writeGeneration = store.getGeneration();
@@ -148642,6 +148741,8 @@ async function storeObservation(input) {
148642
148741
  writeGeneration: 0
148643
148742
  };
148644
148743
  observations.push(observation);
148744
+ await input.onPersisted?.(observation);
148745
+ persistedCallbackInvoked = true;
148645
148746
  }
148646
148747
  doc = {
148647
148748
  id: makeOramaObservationId(input.projectId, observation.id),
@@ -148673,8 +148774,11 @@ async function storeObservation(input) {
148673
148774
  };
148674
148775
  await assignAndPersist();
148675
148776
  if (upsertedInsideLock) {
148676
- return { observation: await upsertObservation(observation, input, now3), upserted: true };
148777
+ const updated = await upsertObservation(observation, input, now3);
148778
+ await input.onPersisted?.(updated);
148779
+ return { observation: updated, upserted: true };
148677
148780
  }
148781
+ if (!persistedCallbackInvoked) await input.onPersisted?.(observation);
148678
148782
  await bindObservationCodeRefsBestEffort(observation);
148679
148783
  queueObservationQualification(observation);
148680
148784
  if (resolveObservationVisibility(observation) === "project") {
@@ -153826,9 +153930,9 @@ function existingLensCandidates(rootPath, lens) {
153826
153930
  const absolute = path27.join(rootPath, candidate);
153827
153931
  try {
153828
153932
  if (!existsSync15(absolute)) continue;
153829
- const stat3 = statSync4(absolute);
153830
- if (stat3.isFile()) out.push(candidate);
153831
- if (stat3.isDirectory()) out.push(candidate.replace(/\\/g, "/"));
153933
+ const stat4 = statSync4(absolute);
153934
+ if (stat4.isFile()) out.push(candidate);
153935
+ if (stat4.isDirectory()) out.push(candidate.replace(/\\/g, "/"));
153832
153936
  } catch {
153833
153937
  }
153834
153938
  }
@@ -159965,11 +160069,11 @@ import path32 from "path";
159965
160069
  function resolveGitDir(projectRoot) {
159966
160070
  const dotGit = path32.join(projectRoot, ".git");
159967
160071
  if (!existsSync18(dotGit)) return null;
159968
- const stat3 = statSync5(dotGit);
159969
- if (stat3.isDirectory()) {
160072
+ const stat4 = statSync5(dotGit);
160073
+ if (stat4.isDirectory()) {
159970
160074
  return dotGit;
159971
160075
  }
159972
- if (stat3.isFile()) {
160076
+ if (stat4.isFile()) {
159973
160077
  try {
159974
160078
  const content = readFileSync13(dotGit, "utf-8").trim();
159975
160079
  const match2 = content.match(/^gitdir:\s*(.+)$/);
@@ -162007,8 +162111,8 @@ var RulesSyncer = class {
162007
162111
  const dir = path23.dirname(pattern);
162008
162112
  const fileGlob = path23.basename(pattern);
162009
162113
  try {
162010
- const stat3 = await fs16.stat(dir);
162011
- if (!stat3.isDirectory()) {
162114
+ const stat4 = await fs16.stat(dir);
162115
+ if (!stat4.isDirectory()) {
162012
162116
  try {
162013
162117
  await fs16.access(pattern);
162014
162118
  return [pattern];
@@ -163473,18 +163577,21 @@ var TOOL_PROFILES = Object.freeze({
163473
163577
  memorix_context_pack: ["micro", "lite", "team", "full"],
163474
163578
  memorix_codegraph_status: ["micro", "lite", "team", "full"],
163475
163579
  memorix_resolve: ["micro", "lite", "team", "full"],
163580
+ // A fail-closed stdio server needs an explicit recovery path when a client
163581
+ // did not launch it from the workspace. The unified media tool keeps
163582
+ // multimodal support to one additional schema rather than enabling lite.
163583
+ memorix_session_start: ["micro", "lite", "team", "full"],
163584
+ memorix_media: ["micro", "lite", "team", "full"],
163476
163585
  // ── lite: extended solo memory surface ────────────────────────────
163477
163586
  memorix_graph_context: ["lite", "team", "full"],
163478
163587
  memorix_timeline: ["lite", "team", "full"],
163479
163588
  memorix_suggest_topic_key: ["lite", "team", "full"],
163480
- memorix_session_start: ["lite", "team", "full"],
163481
163589
  memorix_session_end: ["lite", "team", "full"],
163482
163590
  memorix_session_context: ["lite", "team", "full"],
163483
163591
  memorix_store_reasoning: ["lite", "team", "full"],
163484
163592
  memorix_search_reasoning: ["lite", "team", "full"],
163485
163593
  memorix_transfer: ["lite", "team", "full"],
163486
163594
  memorix_retention: ["lite", "team", "full"],
163487
- memorix_media: ["lite", "team", "full"],
163488
163595
  // ── team: orchestration coordination surfaces — HTTP default ──────
163489
163596
  memorix_dashboard: ["team", "full"],
163490
163597
  memorix_knowledge: ["team", "full"],
@@ -163537,7 +163644,7 @@ function resolveToolProfile(opts) {
163537
163644
  function describeProfile(profile) {
163538
163645
  switch (profile) {
163539
163646
  case "micro":
163540
- return "micro (agent-ready context + basic memory, ~7 tools)";
163647
+ return "micro (agent-ready context + bounded recovery/media, ~9 tools)";
163541
163648
  case "lite":
163542
163649
  return "lite (core memory + sessions, ~18 tools)";
163543
163650
  case "team":
@@ -164022,7 +164129,7 @@ The path should point to a directory containing a .git folder.`
164022
164129
  };
164023
164130
  const server = existingServer ?? new McpServer({
164024
164131
  name: "memorix",
164025
- version: true ? "1.3.3" : "1.0.1"
164132
+ version: true ? "1.4.0" : "1.0.1"
164026
164133
  });
164027
164134
  const originalRegisterTool = server.registerTool.bind(server);
164028
164135
  server.registerTool = ((name, ...args) => {
@@ -167709,9 +167816,9 @@ ${lines.join("\n")}` }] };
167709
167816
  "memorix_media",
167710
167817
  {
167711
167818
  title: "Manage Controlled Media",
167712
- description: "Use the controlled local media library for an explicit import, attachment, inspection, or MiniMax generation request. Assets stay outside the Git worktree and enter normal memory only when attach is explicitly true. Use the CLI for destructive removal, quota cleanup, job cancellation, and direct generation. MCP generation is disabled by default and requires MEMORIX_MCP_MEDIA_GENERATION=1 after the operator reviews provider billing.",
167819
+ description: "Use the controlled local media library for an explicit import, attachment, inspection, or MiniMax generation request. Assets stay outside the Git worktree and enter normal memory only when attach is explicitly true. Use the CLI for destructive removal, quota cleanup, and direct generation. MCP generation is disabled by default and requires MEMORIX_MCP_MEDIA_GENERATION=1 after the operator reviews provider billing.",
167713
167820
  inputSchema: {
167714
- action: z626.enum(["import", "attach", "list", "show", "generate-image", "generate-video", "status"]),
167821
+ action: z626.enum(["import", "attach", "list", "show", "generate-image", "generate-video", "status", "cancel"]),
167715
167822
  path: z626.string().optional().describe("Explicit local image/audio/video/PDF path for import."),
167716
167823
  assetId: z626.string().optional().describe("Controlled MediaAsset ID for attach/show."),
167717
167824
  jobId: z626.string().optional().describe("Durable media job ID for status."),
@@ -167783,6 +167890,10 @@ ${lines.join("\n")}` }] };
167783
167890
  if (!job) throw new Error(`Media job not found: ${jobId}`);
167784
167891
  return result({ action, projectId: project.id, job });
167785
167892
  }
167893
+ if (action === "cancel") {
167894
+ if (!jobId?.trim()) throw new Error("jobId is required for media cancel");
167895
+ return result({ action, projectId: project.id, job: store.cancelJob(project.id, jobId) });
167896
+ }
167786
167897
  if (action === "attach") {
167787
167898
  if (!assetId?.trim()) throw new Error("assetId is required for media attach");
167788
167899
  const asset = store.getAsset(project.id, assetId);