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
@@ -1318,8 +1318,8 @@ async function acquireLock(lockPath) {
1318
1318
  const code = err instanceof Error && "code" in err ? err.code : void 0;
1319
1319
  if (code === "EEXIST" || code === "EPERM") {
1320
1320
  try {
1321
- const stat3 = await fs2.stat(lockPath);
1322
- if (Date.now() - stat3.mtimeMs > LOCK_STALE_MS) {
1321
+ const stat4 = await fs2.stat(lockPath);
1322
+ if (Date.now() - stat4.mtimeMs > LOCK_STALE_MS) {
1323
1323
  await fs2.unlink(lockPath).catch(() => {
1324
1324
  });
1325
1325
  continue;
@@ -7938,7 +7938,9 @@ async function storeObservation(input) {
7938
7938
  if (input.visibilityReader && !canManageObservation(existing, input.visibilityReader)) {
7939
7939
  throw new Error("Cannot update a memory outside this session's write scope.");
7940
7940
  }
7941
- return { observation: await upsertObservation(existing, input, now3), upserted: true };
7941
+ const observation2 = await upsertObservation(existing, input, now3);
7942
+ await input.onPersisted?.(observation2);
7943
+ return { observation: observation2, upserted: true };
7942
7944
  }
7943
7945
  }
7944
7946
  const contentForExtraction = [input.title, input.narrative, ...input.facts ?? []].join(" ");
@@ -7963,6 +7965,7 @@ async function storeObservation(input) {
7963
7965
  let doc;
7964
7966
  let upsertedInsideLock = false;
7965
7967
  let reloadCacheAfterCommit = false;
7968
+ let persistedCallbackInvoked = false;
7966
7969
  const assignAndPersist = async () => {
7967
7970
  if (projectDir) {
7968
7971
  const store = getObservationStore();
@@ -8018,6 +8021,8 @@ async function storeObservation(input) {
8018
8021
  writeGeneration: transactionGeneration + 1
8019
8022
  };
8020
8023
  await tx.insert(observation);
8024
+ await input.onPersisted?.(observation);
8025
+ persistedCallbackInvoked = true;
8021
8026
  await tx.saveIdCounter(id + 1);
8022
8027
  });
8023
8028
  observation.writeGeneration = store.getGeneration();
@@ -8067,6 +8072,8 @@ async function storeObservation(input) {
8067
8072
  writeGeneration: 0
8068
8073
  };
8069
8074
  observations.push(observation);
8075
+ await input.onPersisted?.(observation);
8076
+ persistedCallbackInvoked = true;
8070
8077
  }
8071
8078
  doc = {
8072
8079
  id: makeOramaObservationId(input.projectId, observation.id),
@@ -8098,8 +8105,11 @@ async function storeObservation(input) {
8098
8105
  };
8099
8106
  await assignAndPersist();
8100
8107
  if (upsertedInsideLock) {
8101
- return { observation: await upsertObservation(observation, input, now3), upserted: true };
8108
+ const updated = await upsertObservation(observation, input, now3);
8109
+ await input.onPersisted?.(updated);
8110
+ return { observation: updated, upserted: true };
8102
8111
  }
8112
+ if (!persistedCallbackInvoked) await input.onPersisted?.(observation);
8103
8113
  await bindObservationCodeRefsBestEffort(observation);
8104
8114
  queueObservationQualification(observation);
8105
8115
  if (resolveObservationVisibility(observation) === "project") {
@@ -9683,6 +9693,10 @@ var init_media_store = __esm({
9683
9693
  `).run(projectId, assetId).changes ?? 0);
9684
9694
  this.db.prepare("DELETE FROM media_derivations WHERE project_id = ? AND asset_id = ?").run(projectId, assetId);
9685
9695
  this.db.prepare("DELETE FROM media_embeddings WHERE project_id = ? AND asset_id = ?").run(projectId, assetId);
9696
+ this.db.prepare(`
9697
+ UPDATE media_jobs SET asset_id = NULL, updated_at = ?
9698
+ WHERE project_id = ? AND asset_id = ?
9699
+ `).run(Date.now(), projectId, assetId);
9686
9700
  const result = this.db.prepare(`
9687
9701
  UPDATE media_assets SET deleted_at = ?, updated_at = ?
9688
9702
  WHERE project_id = ? AND id = ? AND deleted_at IS NULL
@@ -9857,16 +9871,28 @@ var init_media_store = __esm({
9857
9871
  return row ? rowToJob2(row) : void 0;
9858
9872
  }
9859
9873
  updateJob(projectId, id, input) {
9874
+ const updated = this.updateJobInternal(projectId, id, input, false);
9875
+ if (!updated) throw new Error(`Media job not found: ${id}`);
9876
+ return updated;
9877
+ }
9878
+ /**
9879
+ * Atomically preserve a user cancellation against a worker that was already
9880
+ * downloading or attaching media when the cancellation arrived.
9881
+ */
9882
+ updateJobIfNotCancelled(projectId, id, input) {
9883
+ return this.updateJobInternal(projectId, id, input, true);
9884
+ }
9885
+ updateJobInternal(projectId, id, input, rejectCancelled) {
9860
9886
  const existing = this.getJob(projectId, id);
9861
- if (!existing) throw new Error(`Media job not found: ${id}`);
9887
+ if (!existing) return void 0;
9862
9888
  const now3 = Date.now();
9863
9889
  const attempts = existing.attempts + (input.incrementAttempts ? 1 : 0);
9864
9890
  const completedAt = input.status === "completed" ? now3 : null;
9865
- this.db.prepare(`
9891
+ const result = this.db.prepare(`
9866
9892
  UPDATE media_jobs
9867
9893
  SET status = ?, provider_task_id = COALESCE(?, provider_task_id), asset_id = COALESCE(?, asset_id),
9868
9894
  last_error = ?, attempts = ?, updated_at = ?, completed_at = ?
9869
- WHERE project_id = ? AND id = ?
9895
+ WHERE project_id = ? AND id = ? ${rejectCancelled ? "AND status <> 'cancelled'" : ""}
9870
9896
  `).run(
9871
9897
  input.status,
9872
9898
  input.providerTaskId ?? null,
@@ -9878,6 +9904,7 @@ var init_media_store = __esm({
9878
9904
  projectId,
9879
9905
  id
9880
9906
  );
9907
+ if (Number(result.changes ?? 0) !== 1) return void 0;
9881
9908
  return this.getJob(projectId, id);
9882
9909
  }
9883
9910
  cancelJob(projectId, id) {
@@ -9923,6 +9950,7 @@ async function attachMediaAssetToObservation(input) {
9923
9950
  `MIME type: ${asset.mimeType}`,
9924
9951
  ...input.facts ?? []
9925
9952
  ];
9953
+ const store = new MediaStore(input.dataDir);
9926
9954
  const result = await storeObservation({
9927
9955
  entityName: input.entityName?.trim() || sourceLabel2.replace(/\.[^.]+$/, "") || "media-asset",
9928
9956
  type: "discovery",
@@ -9935,13 +9963,15 @@ async function attachMediaAssetToObservation(input) {
9935
9963
  sourceDetail: "explicit",
9936
9964
  ...input.visibility ? { visibility: input.visibility } : {},
9937
9965
  ...input.createdByAgentId ? { createdByAgentId: input.createdByAgentId } : {},
9938
- ...input.visibilityReader ? { visibilityReader: input.visibilityReader } : {}
9939
- });
9940
- new MediaStore(input.dataDir).linkAsset({
9941
- projectId: input.projectId,
9942
- assetId: asset.id,
9943
- observationId: result.observation.id,
9944
- role: input.role ?? "attachment"
9966
+ ...input.visibilityReader ? { visibilityReader: input.visibilityReader } : {},
9967
+ onPersisted: (observation) => {
9968
+ store.linkAsset({
9969
+ projectId: input.projectId,
9970
+ assetId: asset.id,
9971
+ observationId: observation.id,
9972
+ role: input.role ?? "attachment"
9973
+ });
9974
+ }
9945
9975
  });
9946
9976
  return result.observation;
9947
9977
  }
@@ -9967,7 +9997,7 @@ var init_types2 = __esm({
9967
9997
 
9968
9998
  // src/media/asset-store.ts
9969
9999
  import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
9970
- import { copyFile, lstat, mkdir as mkdir3, open, readFile as readFile3, rename, rm, writeFile as writeFile3 } from "fs/promises";
10000
+ import { copyFile, lstat, mkdir as mkdir3, open, readFile as readFile3, readdir, rename, rm, stat, writeFile as writeFile3 } from "fs/promises";
9971
10001
  import path9 from "path";
9972
10002
  function mediaRoot(dataDir) {
9973
10003
  return path9.resolve(dataDir, "media");
@@ -61080,7 +61110,7 @@ var require_node_domexception = __commonJS({
61080
61110
  // node_modules/fetch-blob/from.js
61081
61111
  import { statSync as statSync2, createReadStream, promises as fs7 } from "fs";
61082
61112
  import { basename } from "path";
61083
- var import_node_domexception, stat, blobFromSync, blobFrom, fileFrom, fileFromSync, fromBlob, fromFile, BlobDataItem;
61113
+ var import_node_domexception, stat2, blobFromSync, blobFrom, fileFrom, fileFromSync, fromBlob, fromFile, BlobDataItem;
61084
61114
  var init_from = __esm({
61085
61115
  "node_modules/fetch-blob/from.js"() {
61086
61116
  "use strict";
@@ -61088,23 +61118,23 @@ var init_from = __esm({
61088
61118
  import_node_domexception = __toESM(require_node_domexception(), 1);
61089
61119
  init_file();
61090
61120
  init_fetch_blob();
61091
- ({ stat } = fs7);
61121
+ ({ stat: stat2 } = fs7);
61092
61122
  blobFromSync = (path18, type) => fromBlob(statSync2(path18), path18, type);
61093
- blobFrom = (path18, type) => stat(path18).then((stat3) => fromBlob(stat3, path18, type));
61094
- fileFrom = (path18, type) => stat(path18).then((stat3) => fromFile(stat3, path18, type));
61123
+ blobFrom = (path18, type) => stat2(path18).then((stat4) => fromBlob(stat4, path18, type));
61124
+ fileFrom = (path18, type) => stat2(path18).then((stat4) => fromFile(stat4, path18, type));
61095
61125
  fileFromSync = (path18, type) => fromFile(statSync2(path18), path18, type);
61096
- fromBlob = (stat3, path18, type = "") => new fetch_blob_default([new BlobDataItem({
61126
+ fromBlob = (stat4, path18, type = "") => new fetch_blob_default([new BlobDataItem({
61097
61127
  path: path18,
61098
- size: stat3.size,
61099
- lastModified: stat3.mtimeMs,
61128
+ size: stat4.size,
61129
+ lastModified: stat4.mtimeMs,
61100
61130
  start: 0
61101
61131
  })], { type });
61102
- fromFile = (stat3, path18, type = "") => new file_default([new BlobDataItem({
61132
+ fromFile = (stat4, path18, type = "") => new file_default([new BlobDataItem({
61103
61133
  path: path18,
61104
- size: stat3.size,
61105
- lastModified: stat3.mtimeMs,
61134
+ size: stat4.size,
61135
+ lastModified: stat4.mtimeMs,
61106
61136
  start: 0
61107
- })], basename(path18), { type, lastModified: stat3.mtimeMs });
61137
+ })], basename(path18), { type, lastModified: stat4.mtimeMs });
61108
61138
  BlobDataItem = class _BlobDataItem {
61109
61139
  #path;
61110
61140
  #start;
@@ -61127,7 +61157,7 @@ var init_from = __esm({
61127
61157
  });
61128
61158
  }
61129
61159
  async *stream() {
61130
- const { mtimeMs } = await stat(this.#path);
61160
+ const { mtimeMs } = await stat2(this.#path);
61131
61161
  if (mtimeMs > this.lastModified) {
61132
61162
  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");
61133
61163
  }
@@ -143293,10 +143323,17 @@ function sourceLabel(request, providerTaskId) {
143293
143323
  }
143294
143324
  async function finishWithAsset(input) {
143295
143325
  const { dataDir, projectId, store, mediaJob, request, providerTask, dependencies } = input;
143296
- let asset = mediaJob.assetId ? store.getAsset(projectId, mediaJob.assetId) : void 0;
143326
+ let activeJob = store.getJob(projectId, mediaJob.id);
143327
+ if (!activeJob || terminal(activeJob)) return { action: "complete" };
143328
+ let asset = activeJob.assetId ? store.getAsset(projectId, activeJob.assetId) : void 0;
143297
143329
  if (!asset) {
143298
143330
  if (!providerTask.downloadUrl) throw new Error("Succeeded MiniMax video task did not include a download URL");
143299
- store.updateJob(projectId, mediaJob.id, { status: "downloading", providerTaskId: providerTask.taskId });
143331
+ const downloading = store.updateJobIfNotCancelled(projectId, activeJob.id, {
143332
+ status: "downloading",
143333
+ providerTaskId: providerTask.taskId
143334
+ });
143335
+ if (!downloading) return { action: "complete" };
143336
+ activeJob = downloading;
143300
143337
  const bytes = await (dependencies.download ?? ((downloadInput) => downloadProviderMedia(downloadInput)))({
143301
143338
  url: providerTask.downloadUrl,
143302
143339
  maxBytes: request.maxBytes
@@ -143313,21 +143350,30 @@ async function finishWithAsset(input) {
143313
143350
  maxBytes: request.maxBytes
143314
143351
  });
143315
143352
  asset = imported.asset;
143353
+ const persistedAsset = store.updateJobIfNotCancelled(projectId, activeJob.id, {
143354
+ status: "downloading",
143355
+ providerTaskId: providerTask.taskId,
143356
+ assetId: asset.id
143357
+ });
143358
+ if (!persistedAsset) return { action: "complete" };
143359
+ activeJob = persistedAsset;
143316
143360
  }
143317
- if (mediaJob.attachOnComplete) {
143361
+ if (activeJob.attachOnComplete) {
143362
+ activeJob = store.getJob(projectId, activeJob.id);
143363
+ if (!activeJob || terminal(activeJob)) return { action: "complete" };
143318
143364
  const hasAttachment = store.listLinks(projectId, asset.id).some((link) => link.role === "attachment" && link.observationId !== void 0);
143319
143365
  if (!hasAttachment) {
143320
143366
  await (dependencies.attach ?? attachMediaAssetToObservation)({
143321
143367
  dataDir,
143322
143368
  projectId,
143323
143369
  asset,
143324
- title: mediaJob.observationTitle ?? `MiniMax video: ${asset.sourceLabel ?? asset.id}`,
143370
+ title: activeJob.observationTitle ?? `MiniMax video: ${asset.sourceLabel ?? asset.id}`,
143325
143371
  narrative: `Generated with ${request.provider}/${request.model}. Prompt: ${request.prompt}`,
143326
143372
  concepts: ["generated-video", "minimax", request.model]
143327
143373
  });
143328
143374
  }
143329
143375
  }
143330
- store.updateJob(projectId, mediaJob.id, {
143376
+ store.updateJobIfNotCancelled(projectId, activeJob.id, {
143331
143377
  status: "completed",
143332
143378
  providerTaskId: providerTask.taskId,
143333
143379
  assetId: asset.id
@@ -143337,10 +143383,12 @@ async function finishWithAsset(input) {
143337
143383
  function retryAfterProviderError(store, projectId, mediaJob, maintenanceJob, error) {
143338
143384
  const message = safeError2(error);
143339
143385
  if (maintenanceJob.attempts >= maintenanceJob.maxAttempts) {
143340
- store.updateJob(projectId, mediaJob.id, { status: "failed", lastError: message, incrementAttempts: true });
143386
+ store.updateJobIfNotCancelled(projectId, mediaJob.id, { status: "failed", lastError: message, incrementAttempts: true });
143387
+ return { action: "complete" };
143388
+ }
143389
+ if (!store.updateJobIfNotCancelled(projectId, mediaJob.id, { status: "retry", lastError: message, incrementAttempts: true })) {
143341
143390
  return { action: "complete" };
143342
143391
  }
143343
- store.updateJob(projectId, mediaJob.id, { status: "retry", lastError: message, incrementAttempts: true });
143344
143392
  return { action: "reschedule", delayMs: MINI_MAX_VIDEO_POLL_MS, status: "retry", lastError: message };
143345
143393
  }
143346
143394
  function queueMiniMaxVideoGeneration(input) {
@@ -143381,22 +143429,25 @@ async function runMiniMaxVideoGenerationJob(maintenanceJob, context, dependencie
143381
143429
  if (mediaJob.kind !== "minimax-video-generation") throw new Error(`Unsupported media job kind: ${mediaJob.kind}`);
143382
143430
  const request = parseStoredRequest(mediaJob.request);
143383
143431
  if (!mediaJob.providerTaskId) {
143384
- store.updateJob(context.projectId, mediaJob.id, { status: "submitting", incrementAttempts: true });
143432
+ if (!store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "submitting", incrementAttempts: true })) {
143433
+ return { action: "complete" };
143434
+ }
143385
143435
  let submitted;
143386
143436
  try {
143387
143437
  submitted = await (dependencies.createTask ?? createMiniMaxVideoTask)(toVideoRequest(request));
143388
143438
  } catch (error) {
143389
- store.updateJob(context.projectId, mediaJob.id, { status: "failed", lastError: safeError2(error) });
143439
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "failed", lastError: safeError2(error) });
143390
143440
  return { action: "complete" };
143391
143441
  }
143392
143442
  if (submitted.status === "failed") {
143393
- store.updateJob(context.projectId, mediaJob.id, { status: "failed", providerTaskId: submitted.taskId, lastError: submitted.error });
143443
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "failed", providerTaskId: submitted.taskId, lastError: submitted.error });
143394
143444
  return { action: "complete" };
143395
143445
  }
143396
- const pending = store.updateJob(context.projectId, mediaJob.id, {
143446
+ const pending = store.updateJobIfNotCancelled(context.projectId, mediaJob.id, {
143397
143447
  status: submitted.status === "succeeded" ? "downloading" : "provider-pending",
143398
143448
  providerTaskId: submitted.taskId
143399
143449
  });
143450
+ if (!pending) return { action: "complete" };
143400
143451
  if (submitted.status === "succeeded") {
143401
143452
  try {
143402
143453
  return await finishWithAsset({
@@ -143424,11 +143475,13 @@ async function runMiniMaxVideoGenerationJob(maintenanceJob, context, dependencie
143424
143475
  return retryAfterProviderError(store, context.projectId, mediaJob, maintenanceJob, error);
143425
143476
  }
143426
143477
  if (task.status === "pending") {
143427
- store.updateJob(context.projectId, mediaJob.id, { status: "provider-pending", providerTaskId: task.taskId });
143478
+ if (!store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "provider-pending", providerTaskId: task.taskId })) {
143479
+ return { action: "complete" };
143480
+ }
143428
143481
  return { action: "reschedule", delayMs: MINI_MAX_VIDEO_POLL_MS, resetAttempts: true, clearLastError: true };
143429
143482
  }
143430
143483
  if (task.status === "failed") {
143431
- store.updateJob(context.projectId, mediaJob.id, { status: "failed", providerTaskId: task.taskId, lastError: task.error, incrementAttempts: true });
143484
+ store.updateJobIfNotCancelled(context.projectId, mediaJob.id, { status: "failed", providerTaskId: task.taskId, lastError: task.error, incrementAttempts: true });
143432
143485
  return { action: "complete" };
143433
143486
  }
143434
143487
  try {
@@ -144060,7 +144113,7 @@ function indexFileLite(projectId, projectRoot, abs, indexedAt, fileStat) {
144060
144113
  const rel = normalizeCodePath(relative(projectRoot, abs));
144061
144114
  try {
144062
144115
  const text = readFileSync6(abs, "utf-8");
144063
- const stat3 = fileStat ?? statSync3(abs);
144116
+ const stat4 = fileStat ?? statSync3(abs);
144064
144117
  const file = {
144065
144118
  id: makeCodeFileId(projectId, rel),
144066
144119
  projectId,
@@ -144069,8 +144122,8 @@ function indexFileLite(projectId, projectRoot, abs, indexedAt, fileStat) {
144069
144122
  contentHash: hashText(text),
144070
144123
  // Preserve filesystem precision. Rounding here made same-size edits in
144071
144124
  // a short Windows timestamp window indistinguishable to refresh.
144072
- mtimeMs: Number(stat3.mtimeMs),
144073
- sizeBytes: Number(stat3.size),
144125
+ mtimeMs: Number(stat4.mtimeMs),
144126
+ sizeBytes: Number(stat4.size),
144074
144127
  indexedAt
144075
144128
  };
144076
144129
  return {
@@ -144097,18 +144150,18 @@ async function indexProjectLite(options) {
144097
144150
  let skippedOversizedFiles = 0;
144098
144151
  let unreadableFiles = 0;
144099
144152
  for (const abs of paths) {
144100
- let stat3;
144153
+ let stat4;
144101
144154
  try {
144102
- stat3 = statSync3(abs);
144155
+ stat4 = statSync3(abs);
144103
144156
  } catch {
144104
144157
  unreadableFiles++;
144105
144158
  continue;
144106
144159
  }
144107
- if (Number(stat3.size) > maxFileBytes) {
144160
+ if (Number(stat4.size) > maxFileBytes) {
144108
144161
  skippedOversizedFiles++;
144109
144162
  continue;
144110
144163
  }
144111
- const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat3);
144164
+ const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat4);
144112
144165
  if (!attempt.delta) {
144113
144166
  if (attempt.unreadable) unreadableFiles++;
144114
144167
  continue;
@@ -144136,16 +144189,16 @@ async function refreshProjectLite(store, options) {
144136
144189
  for (const abs of paths) {
144137
144190
  const rel = normalizeCodePath(relative(options.projectRoot, abs));
144138
144191
  seenPaths.add(rel);
144139
- let stat3;
144192
+ let stat4;
144140
144193
  try {
144141
- stat3 = statSync3(abs);
144194
+ stat4 = statSync3(abs);
144142
144195
  } catch {
144143
144196
  unreadableFiles++;
144144
144197
  continue;
144145
144198
  }
144146
144199
  const existing = existingByPath.get(rel);
144147
- const mtimeMs = Number(stat3.mtimeMs);
144148
- const sizeBytes = Number(stat3.size);
144200
+ const mtimeMs = Number(stat4.mtimeMs);
144201
+ const sizeBytes = Number(stat4.size);
144149
144202
  if (sizeBytes > maxFileBytes) {
144150
144203
  skippedOversizedFiles++;
144151
144204
  if (existing) oversizedExistingFileIds.add(existing.id);
@@ -144155,7 +144208,7 @@ async function refreshProjectLite(store, options) {
144155
144208
  unchangedFiles++;
144156
144209
  continue;
144157
144210
  }
144158
- const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat3);
144211
+ const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat4);
144159
144212
  if (!attempt.delta) {
144160
144213
  if (attempt.unreadable) unreadableFiles++;
144161
144214
  continue;
@@ -318,20 +318,20 @@ then document the 2026-07-28 migration boundary.
318
318
  - no personal Codex/Claude/OpenCode configuration mutation;
319
319
  - documentation and changelog reviewed.
320
320
 
321
- ### Phase G: 1.3.4 hardening
321
+ ### Phase G: 1.3.4 hardening (completed 2026-08-02)
322
322
 
323
- 1.3.4 is not a feature release. It exercises fresh install, Windows CLI
324
- windows-hide behavior, CLI/stdio/HTTP MCP routes, cancellation/restart of media
325
- jobs, quota cleanup, failure messages, and agent-facing UX. Only defects found
326
- through that work are fixed.
323
+ 1.3.4 exercised fresh install, Windows CLI windows-hide behavior,
324
+ CLI/stdio/HTTP MCP routes, cancellation/restart of media jobs, quota cleanup,
325
+ failure messages, and agent-facing UX. The resulting fixes are published in
326
+ the 1.3.4 release notes and passed Windows and Ubuntu CI.
327
327
 
328
- ### Phase H: 1.4 stable release
328
+ ### Phase H: 1.4 stable release (completed 2026-08-02)
329
329
 
330
- 1.4 is cut only when 1.3.3 and 1.3.4 gates are green, all supported paths have
331
- packaged CLI evidence, the compatibility matrix is updated, and there is no
332
- known high-severity data-loss, credential-retention, Windows-popup, or MCP
333
- binding regression. “No bug” means no known release blocker after these tests,
334
- not an impossible claim of mathematical perfection.
330
+ 1.4.0 was cut after the 1.3.3 and 1.3.4 gates were green, supported paths had
331
+ packaged CLI evidence, and no high-severity data-loss, credential-retention,
332
+ Windows-popup, or MCP binding regression was known. “Stable” means no known
333
+ release blocker after these tests, not an impossible claim of mathematical
334
+ perfection.
335
335
 
336
336
  ## Acceptance criteria
337
337
 
@@ -351,8 +351,8 @@ not an impossible claim of mathematical perfection.
351
351
  8. No generated asset enters project memory without an explicit attach action.
352
352
  9. The same media action works through CLI and MCP using an explicit resolved
353
353
  project binding, independent of a legacy HTTP session identifier.
354
- 10. 1.3.4 regression tests cover Windows, fresh package install, CLI, stdio,
355
- HTTP MCP, quota cleanup, and error UX before 1.4 is published.
354
+ 10. 1.3.4 regression tests covered Windows, fresh package install, CLI, stdio,
355
+ HTTP MCP, quota cleanup, and error UX before 1.4.0 was published.
356
356
 
357
357
  ## Research and implementation basis
358
358
 
@@ -404,10 +404,11 @@ At startup, `serve-http` seeds its default project root from:
404
404
 
405
405
  1. `--cwd`
406
406
  2. `MEMORIX_PROJECT_ROOT`
407
- 3. `~/.memorix/last-project-root`
408
- 4. `process.cwd()`
407
+ 3. `process.cwd()`
409
408
 
410
- That startup root is useful for dashboard and server boot, but it does not replace explicit session binding.
409
+ Memorix deliberately does not restore a previously remembered project root: a
410
+ server launched from an unrelated directory must wait for an explicit project
411
+ root or MCP/session binding rather than risk cross-project memory access.
411
412
 
412
413
  ### Step 4. Bind each HTTP session explicitly
413
414
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "memorix",
3
3
  "mcpName": "io.github.AVIDS2/memorix",
4
- "version": "1.3.3",
4
+ "version": "1.4.0",
5
5
  "description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memorix",
4
- "version": "1.3.3",
4
+ "version": "1.4.0",
5
5
  "description": "Shared workspace memory for Claude Code and other AI coding agents.",
6
6
  "author": {
7
7
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Shared workspace memory for Codex and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Shared local workspace memory for GitHub Copilot CLI and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "mcpServers": {
5
5
  "memorix": {
6
6
  "command": "memorix",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-omp-package",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Memorix shared workspace memory package for Oh-my-Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Shared workspace memory for OpenClaw and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-pi-package",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Memorix shared workspace memory package for Pi coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
package/server.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/AVIDS2/memorix",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.3.3",
10
+ "version": "1.4.0",
11
11
  "websiteUrl": "https://github.com/AVIDS2/memorix#readme",
12
12
  "icons": [
13
13
  {
@@ -22,7 +22,7 @@
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "memorix",
25
- "version": "1.3.3",
25
+ "version": "1.4.0",
26
26
  "runtimeHint": "npx",
27
27
  "packageArguments": [
28
28
  {
@@ -359,7 +359,13 @@ export default defineCommand({
359
359
  assetId,
360
360
  force: args.force === true,
361
361
  });
362
- emitResult({ project, ...result }, `Removed media asset ${assetId}`, asJson);
362
+ emitResult(
363
+ { project, ...result },
364
+ result.pendingDelete
365
+ ? `Removed media asset ${assetId}; its staged file is pending cleanup because another process still holds it.`
366
+ : `Removed media asset ${assetId}`,
367
+ asJson,
368
+ );
363
369
  return;
364
370
  }
365
371
  case 'embed': {
@@ -404,7 +410,9 @@ export default defineCommand({
404
410
  });
405
411
  emitResult(
406
412
  { project, ...result },
407
- `Media cleanup: ${result.removed.length} asset(s), ${result.beforeBytes} -> ${result.afterBytes} bytes`,
413
+ `Media cleanup: ${result.removed.length} asset(s), ${result.beforeBytes} -> ${result.afterBytes} active bytes` +
414
+ `${result.reclaimedTrashBytes > 0 ? `; reclaimed ${result.reclaimedTrashBytes} staged bytes` : ''}` +
415
+ `${result.pendingTrashFiles > 0 ? `; ${result.pendingTrashFiles} staged file(s) (${result.pendingTrashBytes} bytes) still pending` : ''}`,
408
416
  asJson,
409
417
  );
410
418
  return;