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.
- package/CHANGELOG.md +23 -0
- package/dist/cli/index.js +1162 -1098
- package/dist/index.d.ts +513 -0
- package/dist/index.js +14751 -14044
- package/dist/maintenance-runner.js +105 -52
- package/dist/media-video-runner.js +105 -52
- package/dist/memcode-runtime/CHANGELOG.md +23 -0
- package/dist/sdk.d.ts +5 -513
- package/dist/sdk.js +186 -75
- package/dist/vector-backfill-runner.js +105 -52
- package/docs/1.3.3-MULTIMODAL-MEDIA-AND-MCP-COMPAT.md +13 -13
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -3
- package/package.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/media.ts +10 -2
- package/src/cli/commands/serve-http.ts +7 -38
- package/src/cli/commands/serve-shared.ts +18 -46
- package/src/cli/commands/serve.ts +3 -41
- package/src/cli/commands/setup.ts +24 -21
- package/src/cli/mcp-root-path.ts +36 -0
- package/src/index.ts +37 -55
- package/src/media/asset-store.ts +74 -6
- package/src/media/attachment.ts +12 -9
- package/src/media/media-store.ts +38 -4
- package/src/media/video-jobs.ts +39 -14
- package/src/memory/observations.ts +21 -2
- package/src/orchestrate/adapters/spawn-helper.ts +2 -0
- package/src/server/tool-profile.ts +7 -4
- 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
|
|
1322
|
-
if (Date.now() -
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
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,
|
|
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) =>
|
|
61094
|
-
fileFrom = (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 = (
|
|
61126
|
+
fromBlob = (stat4, path18, type = "") => new fetch_blob_default([new BlobDataItem({
|
|
61097
61127
|
path: path18,
|
|
61098
|
-
size:
|
|
61099
|
-
lastModified:
|
|
61128
|
+
size: stat4.size,
|
|
61129
|
+
lastModified: stat4.mtimeMs,
|
|
61100
61130
|
start: 0
|
|
61101
61131
|
})], { type });
|
|
61102
|
-
fromFile = (
|
|
61132
|
+
fromFile = (stat4, path18, type = "") => new file_default([new BlobDataItem({
|
|
61103
61133
|
path: path18,
|
|
61104
|
-
size:
|
|
61105
|
-
lastModified:
|
|
61134
|
+
size: stat4.size,
|
|
61135
|
+
lastModified: stat4.mtimeMs,
|
|
61106
61136
|
start: 0
|
|
61107
|
-
})], basename(path18), { type, lastModified:
|
|
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
|
|
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
|
|
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.
|
|
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 (
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
144073
|
-
sizeBytes: Number(
|
|
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
|
|
144153
|
+
let stat4;
|
|
144101
144154
|
try {
|
|
144102
|
-
|
|
144155
|
+
stat4 = statSync3(abs);
|
|
144103
144156
|
} catch {
|
|
144104
144157
|
unreadableFiles++;
|
|
144105
144158
|
continue;
|
|
144106
144159
|
}
|
|
144107
|
-
if (Number(
|
|
144160
|
+
if (Number(stat4.size) > maxFileBytes) {
|
|
144108
144161
|
skippedOversizedFiles++;
|
|
144109
144162
|
continue;
|
|
144110
144163
|
}
|
|
144111
|
-
const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt,
|
|
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
|
|
144192
|
+
let stat4;
|
|
144140
144193
|
try {
|
|
144141
|
-
|
|
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(
|
|
144148
|
-
const sizeBytes = Number(
|
|
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,
|
|
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
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
331
|
-
packaged CLI evidence,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
355
|
-
HTTP MCP, quota cleanup, and error UX before 1.4
|
|
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.
|
|
408
|
-
4. `process.cwd()`
|
|
407
|
+
3. `process.cwd()`
|
|
409
408
|
|
|
410
|
-
|
|
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.
|
|
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,
|
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.
|
|
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.
|
|
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(
|
|
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;
|